Class AsyncJobRegistry
- All Implemented Interfaces:
AutoCloseable
async jobs — the single-process job store a future
async write variant runs on.
Single-process, by decision
Job state lives only in this object's heap, which is enough for a single-user loopback console and a documented
limitation for anything else: a process restart drops every in-flight job, exactly as a restart invalidates
every SynchronizerToken. There is deliberately no store abstraction.
The id is the capability; the caps are load-bearing
Each job is keyed by an unguessable 256-bit SecureRandom id
(32 bytes, hex-encoded), matching
SynchronizerToken's width — the id is the stream's
access control. get(String) of an unknown id returns empty, so a miss is indistinguishable from
any other and the id space cannot be enumerated.
Because jobs keep running after the page navigates away and stream customer-adjacent content, the following caps are enforced server-side and are security controls, not tuning knobs (a data-egress disclosure bound):
MAX_CONCURRENT_JOBSconcurrent (non-terminal) jobs —tryCreate()refuses beyond it, and the check-and-insert is atomic, so concurrent callers cannot overshoot it;HARD_TIMEOUThard per-job timeout — scheduled here and also swept bysweepTimeouts();AsyncJob.maxOutputBytes()of streamed output per job — enforced inAsyncJob.progress(java.lang.String);AsyncJob.maxSubscribers()concurrent subscribers per job — enforced inAsyncJob.acquireSubscriber().
De-duplication by IdempotencyKey
tryCreate(IdempotencyKey) / create(IdempotencyKey) register a job under an idempotency key's
value; submitting again with a key that is already registered returns the
existing job instead of creating a second one, so a double-click, an impatient re-submit or a browser retry
collapses to one job. The lookup-or-create is atomic: concurrent submits with one key yield exactly one job.
A key whose job has finished replays that finished job (the same replay-on-duplicate contract as
IdempotencyKey): the duplicate gets the terminal job and its result back rather
than triggering a second run. This holds for the job's retention window; once the terminal job is reaped from the
heap (see sweepTimeouts()), its key is forgotten too, and the same key starts a fresh job. A duplicate
never counts against MAX_CONCURRENT_JOBS, since it creates nothing.
Example:
AsyncJobRegistry
See Also:
- Since:
- 10.0.0
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intWidth of a capability id, in bits — well beyond the ≥128-bit bar.static final intNumber of random bytes behind a capability id.static final DurationHard per-job timeout; caps how long triage keeps streaming customer-adjacent content.static final intMaximum number of concurrent (non-terminal) jobs;tryCreate()refuses beyond this.static final longMaximum streamed output per job, in bytes (1 MiB); a data-egress bound, not only a heap bound.static final intMaximum number of concurrent subscribers per job (a reload plus one straggler). -
Constructor Summary
ConstructorsConstructorDescriptionCreates a registry with the production defaults: a system clock, theHARD_TIMEOUT(120s) timeout,MAX_OUTPUT_BYTES/MAX_SUBSCRIBERS_PER_JOBcaps, and a private daemon scheduler that enforces each job's hard timeout.AsyncJobRegistry(Duration timeout) Creates a registry with a caller-chosen hard per-job timeout and the production caps / scheduler. -
Method Summary
Modifier and TypeMethodDescriptionCancels the job with the given id (seeAsyncJob.cancel()).voidclose()Shuts down the private scheduler (if this registry created one).create()Creates and registers a new job, throwing when the concurrency cap is reached.create(IdempotencyKey key) Returns the job registered under the specified idempotency key, or creates and registers one, throwing when the concurrency cap is reached.Looks up a job by its capability id.intintsize()voidEnforces the hard timeout on every job at the current clock instant and reaps terminal jobs past their retention window.Attempts to create and register a new job with a freshly-minted capability id.tryCreate(IdempotencyKey key) Returns the job registered under the specified idempotency key, or atomically creates and registers one.
-
Field Details
-
CAPABILITY_BYTES
Number of random bytes behind a capability id. 256 bits, matchingSynchronizerToken.- See Also:
-
CAPABILITY_BITS
Width of a capability id, in bits — well beyond the ≥128-bit bar.- See Also:
-
MAX_CONCURRENT_JOBS
Maximum number of concurrent (non-terminal) jobs;tryCreate()refuses beyond this.- See Also:
-
MAX_SUBSCRIBERS_PER_JOB
Maximum number of concurrent subscribers per job (a reload plus one straggler).- See Also:
-
MAX_OUTPUT_BYTES
Maximum streamed output per job, in bytes (1 MiB); a data-egress bound, not only a heap bound.- See Also:
-
HARD_TIMEOUT
Hard per-job timeout; caps how long triage keeps streaming customer-adjacent content.
-
-
Constructor Details
-
AsyncJobRegistry
public AsyncJobRegistry()Creates a registry with the production defaults: a system clock, theHARD_TIMEOUT(120s) timeout,MAX_OUTPUT_BYTES/MAX_SUBSCRIBERS_PER_JOBcaps, and a private daemon scheduler that enforces each job's hard timeout.The 120s default is a disclosure bound for jobs that are not long-running agent dispatches. Callers that need a longer bound (for example a Claude-length create) use
AsyncJobRegistry(Duration)rather than raising this global default. -
AsyncJobRegistry
Creates a registry with a caller-chosen hard per-job timeout and the production caps / scheduler.Use this when a job may run longer than
HARD_TIMEOUT(120s). The no-arg constructor keeps that default; this overload does not change it.- Parameters:
timeout- The hard per-job timeout. Must be a positive duration.- Throws:
IllegalArgumentException- Iftimeoutisnull , zero, or negative.
-
-
Method Details
-
tryCreate
Attempts to create and register a new job with a freshly-minted capability id.Sweeps timeouts first (so a slot held only by an expired job is freed), then refuses when the running-job cap is already reached — an unbounded set of keep-running jobs would be unbounded egress of customer-adjacent content, so this refusal is load-bearing.
- Returns:
- The new job, or empty if
MAX_CONCURRENT_JOBSconcurrent jobs are already running.
-
create
Creates and registers a new job, throwing when the concurrency cap is reached.- Returns:
- The new job.
- Throws:
IllegalStateException- IfMAX_CONCURRENT_JOBSconcurrent jobs are already running.
-
tryCreate
Returns the job registered under the specified idempotency key, or atomically creates and registers one.If the key's
valueis already registered, the existing job is returned — running or finished (see the class Javadoc for the finished-key behavior) — and nothing new is created, so a duplicate is never refused by the concurrency cap. Otherwise behaves liketryCreate(). Concurrent callers with one key observe exactly one job.- Parameters:
key- The idempotency key. Must not benull .- Returns:
- The existing or new job, or empty if the key is unregistered and
MAX_CONCURRENT_JOBSconcurrent jobs are already running.
-
create
Returns the job registered under the specified idempotency key, or creates and registers one, throwing when the concurrency cap is reached.- Parameters:
key- The idempotency key. Must not benull .- Returns:
- The existing or new job.
- Throws:
IllegalStateException- If the key is unregistered andMAX_CONCURRENT_JOBSconcurrent jobs are already running.- See Also:
-
get
Looks up a job by its capability id.An unknown id returns empty — a miss is indistinguishable from any other, so the unguessable id space cannot be enumerated.
- Parameters:
id- The capability id. Can benull /blank (returns empty).- Returns:
- The job, or empty if no such id is registered.
-
cancel
Cancels the job with the given id (seeAsyncJob.cancel()).- Parameters:
id- The capability id. Can benull /blank.- Returns:
- The (now terminal) job, or empty if no such id is registered.
-
runningCount
- Returns:
- The number of concurrent (non-terminal) jobs currently registered.
-
size
- Returns:
- The total number of jobs currently registered (running plus retained-terminal).
-
sweepTimeouts
Enforces the hard timeout on every job at the current clock instant and reaps terminal jobs past their retention window. Called on everytryCreate(), scheduled per-job by the private scheduler, and callable directly by deterministic tests (with a mutable clock) instead of waiting on wall time. -
close
Shuts down the private scheduler (if this registry created one). Registered jobs are left as-is.- Specified by:
closein interfaceAutoCloseable
-