Class AsyncJobRegistry

java.lang.Object
org.apache.juneau.console.views.AsyncJobRegistry
All Implemented Interfaces:
AutoCloseable

public final class AsyncJobRegistry extends Object implements AutoCloseable
An in-memory, per-process registry of 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):

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 registry = new AsyncJobRegistry(); IdempotencyKey key = IdempotencyKey.mintSelfTargeted("create"); AsyncJob first = registry.create(key); AsyncJob again = registry.create(key); // Double-click: same job, nothing new started. assert first == again;

See Also:
Since:
10.0.0
  • Field Details

  • Constructor Details

    • AsyncJobRegistry

      Creates a registry with the production defaults: a system clock, the HARD_TIMEOUT (120s) timeout, MAX_OUTPUT_BYTES / MAX_SUBSCRIBERS_PER_JOB caps, 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

      public AsyncJobRegistry(Duration timeout)
      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 - If timeout is null, 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_JOBS concurrent jobs are already running.
    • create

      public AsyncJob create()
      Creates and registers a new job, throwing when the concurrency cap is reached.
      Returns:
      The new job.
      Throws:
      IllegalStateException - If MAX_CONCURRENT_JOBS concurrent jobs are already running.
    • tryCreate

      Returns the job registered under the specified idempotency key, or atomically creates and registers one.

      If the key's value is 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 like tryCreate(). Concurrent callers with one key observe exactly one job.

      Parameters:
      key - The idempotency key. Must not be null.
      Returns:
      The existing or new job, or empty if the key is unregistered and MAX_CONCURRENT_JOBS concurrent 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 be null.
      Returns:
      The existing or new job.
      Throws:
      IllegalStateException - If the key is unregistered and MAX_CONCURRENT_JOBS concurrent jobs are already running.
      See Also:
    • get

      public Optional<AsyncJob> get(String id)
      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 be null/blank (returns empty).
      Returns:
      The job, or empty if no such id is registered.
    • cancel

      Cancels the job with the given id (see AsyncJob.cancel()).
      Parameters:
      id - The capability id. Can be null/blank.
      Returns:
      The (now terminal) job, or empty if no such id is registered.
    • runningCount

      public int runningCount()
      Returns:
      The number of concurrent (non-terminal) jobs currently registered.
    • size

      public int size()
      Returns:
      The total number of jobs currently registered (running plus retained-terminal).
    • sweepTimeouts

      public void sweepTimeouts()
      Enforces the hard timeout on every job at the current clock instant and reaps terminal jobs past their retention window. Called on every tryCreate(), scheduled per-job by the private scheduler, and callable directly by deterministic tests (with a mutable clock) instead of waiting on wall time.
    • close

      public void close()
      Shuts down the private scheduler (if this registry created one). Registered jobs are left as-is.
      Specified by:
      close in interface AutoCloseable