JVM launch sequence¶
The JVM target serves desktop apps and server-side processes. There is no OS background scheduler to delegate to, so Backgrounder schedules with library-owned coroutines — the same in-process model as macOS, minus Foundation.
fun main() {
// 1. Construct.
val backgrounder = Backgrounder.create()
// 2. Register every worker factory.
backgrounder.register(SyncWorker.ID) {
SyncWorker(repo = appGraph.repository)
}
// 3. Start — sweeps ephemeral state and seals the registry.
backgrounder.start()
// 4. Tear down on exit (or from your UI framework's teardown hook).
Runtime.getRuntime().addShutdownHook(
Thread { backgrounder.shutdown() },
)
}
start() does just two things on the JVM:
- Sweep ephemeral state.
- Seal the
WorkerRegistryso furtherregister()calls throw.
What runs where¶
- Each scheduled request becomes one coroutine
Jobon aSupervisorJob+Dispatchers.Defaultscope owned by the scheduler. One-shotsdelay(initialDelay)then run; periodics loopdelay(interval)→ run. - Retry backoff is library-emulated, exactly like iOS and macOS: a one-shot returning
WorkResult.Retryre-runs afterbackoff.delayFor(attempt), bounded bymaxAttempts. cancel(taskId)cancels the liveJob— interrupts a running worker.cancelsInFlight = true.- Because the library is the scheduler here,
scheduled()reports exactnextRunHintvalues andWorkStarted.expectedAtis always populated — the JVM is the most introspectable platform.
Network constraints — library-managed gate¶
Same story as macOS: there is no OS constraint concept, so the library inserts a pre-execution reachability gate (powered by reachable) that waits a bounded window for WorkConstraints.networkRequired to be satisfied. On timeout the worker is short-circuited to WorkResult.Retry and rescheduled per the request's BackoffPolicy. See Recipes → Require a network connection.
Unmetered is honoured against ReachabilityStatus.isDataMetered == false. Power and idle constraints (requiresCharging, requiresDeviceIdle) are not enforced — there is no portable JVM power or device-idle API; workers that need either precondition should check inside execute() and return Retry.
Periodic is a coroutine loop¶
No emulation state machine, no OS coalescing. The scheduler fires once per interval, first fire after one full interval — matching macOS and Android cadence. flexWindow is ignored (there is no OS to coalesce with), and missed cycles coalesce into a single fire on resume, per the cross-platform contract (missed cycles).
Nothing survives the process¶
survivesProcessDeath / survivesReboot / survivesForceQuit = false — schedules live in your process and die with it, and nothing relaunches a JVM. Persistence is limited to the ephemeral-task mirror (stored via java.util.prefs.Preferences so the cold-start sweep works across launches). Re-schedule from your app's init path after backgrounder.start() at each launch — the same pattern as macOS.
Shutdown¶
backgrounder.shutdown() cancels the scheduler's SupervisorJob-rooted scope and the runNow runner's. Call it from a JVM shutdown hook (long-running services) or your UI framework's teardown (desktop apps). Without it, in-flight workers run until the JVM exits — harmless for a process that's quitting anyway, but a long-lived embedder (e.g. hosting Backgrounder inside a larger server) should always pair create() with shutdown().