jobmanager/ and jobs/.
Overview
JobManager is initialized once and accessible via AppDependencies.getJobManager(). All job submission goes through it.
The Job base class
Every job extendsJob (or the convenience subclass BaseJob). A job is a durable unit of work described by Job.Parameters:
BaseJob adds the onRun() / onShouldRetry() pattern and handles the Result dispatch:
Job lifecycle
1
Submission
The caller creates a job and calls
JobManager.add(job) or JobManager.startChain(job).then(nextJob).enqueue(). The job is immediately persisted to FastJobStorage and enqueued.2
Constraint check
Before a job runner picks up the job, all declared constraints must be satisfied (e.g., network available). If constraints are not met, the job stays in the queue until a
ConstraintObserver fires.3
onAdded()
Called on the worker thread as soon as the job enters the queue. Useful for optimistic local state updates (e.g., inserting a pending message into the database).
4
run()
The job’s main work is executed. Returns
Result.success(), Result.retry(backoffMs), or Result.failure().5
Success
Result.success() — the job is removed from storage. If there is a chain, the next job becomes eligible.6
Retry
Result.retry(backoffMs) — the job remains in storage with an updated nextBackoffInterval. It becomes eligible again after the backoff has elapsed. BaseJob.getNextRunAttemptBackoff() defaults to exponential backoff via BackoffUtil.7
Failure
Result.failure() — onFailure() is called. Dependent jobs in the same chain are also failed (cascading failure). The job is removed from storage.Result types
Job Parameters
Job.Parameters controls scheduling behavior:
Constraints
AConstraint is a condition that must be true before a job can run. All built-in constraints live in jobmanager/impl/:
Each
Constraint implementation has a corresponding ConstraintObserver that monitors system state and notifies JobManager when the constraint transitions to met, allowing waiting jobs to be picked up immediately.
To declare a constraint on a job:
Job queues and chaining
Jobs in the same queue run serially — a queue acts as a mutual exclusion key. Jobs in different queues (or with no queue) run concurrently up to the configuredmaxGeneralRunners limit.
Chaining ensures that a sequence of jobs runs in order, and that a failure in one job propagates forward:
setOutputData(byte[]) / getInputData().
FastJobStorage
FastJobStorage is the primary JobStorage implementation. It keeps the full job queue in memory for fast access while using JobDatabase (a separate SQLite database from signal.db) for persistence:
JobSpec (the persisted form) captures all scheduling state:
isMemoryOnly = true) never touch JobDatabase and are lost on process death. They are suitable for best-effort work that is acceptable to drop.
Key job types
IndividualSendJob
Sends an encrypted message to a single recipient. Usually the second step in a chain after
AttachmentUploadJob. Handles retry on network errors and re-encryption on session reset.GroupSendJob
Sends a message to a group. Fans out to individual sends and manages sender key distribution. Handles the group-send-endorsement flow.
AttachmentUploadJob
Uploads an attachment to Signal’s CDN and records the remote pointer in
AttachmentTable. Runs before any send job that requires an attachment.PushProcessMessageJob
Processes a single decrypted envelope received from the Signal server. Handles message insertion, receipts, group updates, story syncs, and call event processing.
PreKeysSyncJob
Generates new one-time prekeys and/or rotates signed prekeys when the supply on the server is running low. Runs on a
DecryptionsDrainedConstraint to avoid racing with message processing.RefreshAttributesJob
Re-registers the device’s attributes (capabilities, name, profile key) with the Signal server after they change.
Adding a new job
1
Create the job class
Extend
BaseJob in the jobs/ package. Declare a KEY constant for the factory and implement onRun(), onShouldRetry(), onFailure(), and serialize() / deserialize().2
Register the factory
Add an entry for
MyNewJob.KEY → new MyNewJob.Factory() in the JobManager.Configuration setup inside AppDependencies (look for the setJobFactories() call).3
Submit the job
Call
AppDependencies.getJobManager().add(new MyNewJob()) wherever you want to trigger the work.