Class JobsRest

java.lang.Object
jakarta.servlet.GenericServlet
All Implemented Interfaces:
jakarta.servlet.Servlet, jakarta.servlet.ServletConfig, Serializable, AsyncJobsMixin, BasicUniversalConfig, DefaultConfig, DefaultHtmlConfig, TerminalMixin

public class JobsRest extends PetstoreConsolePage implements AsyncJobsMixin, TerminalMixin
Jobs: the console's async-job reference. A restock submits once per idempotency key, answers 202 with an AsyncJobRef, and the views runtime follows its streamUrl until the job settles.

The registry de-duplicates on the idempotency key itself (AsyncJobRegistry.tryCreate(IdempotencyKey)), so a resubmit with the same key returns the same job and this resource keeps no key map of its own.

The page's job-log card posts the same restock in its flat form and follows the job's stream. A restock with slowMs waits that long before it touches the store (cancel it then and nothing changes), and every restock holds for holdMs (a second by default) after it has added its pets (cancel it then and the result says the change was made). The registry's hard timeout is 30 s, or whatever a JobsRest.HardTimeout bean on the host says; a JobsRest.GroomPeriod bean sets the groom step and a JobsRest.SpeciesHold bean the species-row hold.

The seeded groom run is created on the first groom request rather than at construction, so a JobsRest.GroomPeriod bean (injected after construction) can set its pace; every groom endpoint goes through it, so the page sees the same data.

The page also hosts the terminal demo: a "groom pet" job (GroomRuns) whose output is served by TerminalMixin and shown two ways — a FreeMarker terminal card over the in-memory output, and a second card over the same run's file-backed transcript.

Example:

// POST /console/ops/jobs/restock {DOG:2, slowMs:3000, holdMs:1000, idempotencyKey:'7f3c...'} var body = Restock.of(json); var job = asyncJobRegistry().tryCreate(IdempotencyKey.of(body.idempotencyKey(), "restock", "restock")).orElse(null); if (job == null) return ActionResult.refusal("app:job-limit"); // answered 429 pool.execute(() -> run(job, body, () -> store().restock(body.perSpecies(), "job:restock"))); return AsyncJobRef.of(job); // answered 202

See Also:
  • Field Details

  • Constructor Details

  • Method Details

    • asyncJobRegistry

      Description copied from interface: AsyncJobsMixin
      Returns the AsyncJobRegistry backing this resource's async jobs.
      Specified by:
      asyncJobRegistry in interface AsyncJobsMixin
      Returns:
      The registry. Never null.
    • closeJobs

      public void closeJobs()
      Stops the restock pool, the registry's timeout scheduler and the groom runs when the resource is destroyed.
    • page

      public View page(RestRequest req, @Query("run") String run)
      Renders the Jobs page.
      Parameters:
      req - The request; ?builder renders the builder twin.
      run - The groom run to show, or null for the newest.
      Returns:
      The page view.
      Throws:
      NotFound - If there is no groom run to show.
    • restockForm

      The restock dialog's form source: a read-only GET returning the modal with a freshly minted self-targeted key.

      The dialog is the plain restock, so it asks only for counts and always runs with no wait and the default 1000 ms hold. The timing knobs (slowMs, holdMs) belong to the job-log card, which exists to demonstrate cancelling before and after the effect.

      Returns:
      The modal.
    • rows

      public List<JsonMap> rows()
      Returns:
      Submitted jobs, newest first, with their current state.
    • restock

      Submits a restock.
      Parameters:
      req - The request.
      res - The response (202).
      json - The restock body (see JobsRest.Restock.of(JsonMap)).
      Returns:
      The job ref (202), or a 429 app:job-limit refusal whose message names the running count, so a JSON client such as the job-log card can show it.
      Throws:
      BadRequest - On a missing key, an out-of-range count, or a bad slowMs or holdMs (see JobsRest.Restock.of(JsonMap)).
    • job

      public JsonMap job(@Path("id") String id)
      Parameters:
      id - The job id.
      Returns:
      {state: RUNNING|DONE, result}.
      Throws:
      NotFound - If the job is unknown or has aged out.
    • terminalSource

      Description copied from interface: TerminalMixin
      Resolves the source for a terminal id, applying the application's own access rules.
      Specified by:
      terminalSource in interface TerminalMixin
      Parameters:
      id - The id from the URL path (already checked against the grammar).
      req - The in-flight request.
      Returns:
      The source, or empty when the id is unknown or the caller may not read it.
    • groomForm

      public ModalDef groomForm()
      The Groom dialog's form source.
      Returns:
      The modal.
    • groom

      Starts a groom run.

      Accepts the API form {pet, verbose} or the dialog form {action, targetId, idempotencyKey, fields:{pet, verbose}}; a resubmit with the same idempotencyKey returns the same run.

      Parameters:
      json - The body.
      Returns:
      The new run's row.
      Throws:
      IOException - If the run's temp file cannot be created.
    • groomRows

      public List<JsonMap> groomRows()
      Returns:
      The groom runs, newest first.
    • speciesRows

      Returns:
      One row per species: {id, species, available}.
    • speciesDetail

      public JsonMap speciesDetail(@Path("id") String id)
      A species row's detail panel.
      Parameters:
      id - The species name, for example DOG.
      Returns:
      {contractVersion, fields:{available, lastRestock}}.
      Throws:
      NotFound - If the species is unknown.
    • speciesKey

      public Map<String,Object> speciesKey(RestRequest req, @Path("id") String id)
      Issues a restock key bound to one species and the caller.

      A POST, so it sits behind the console's CSRF boundary like every other console write; it changes nothing but the key ledger.

      Parameters:
      req - The request.
      id - The species name.
      Returns:
      {contractVersion, action, targetId, idempotencyKey}.
      Throws:
      NotFound - If the species is unknown.
    • restockSpecies

      public Object restockSpecies(RestRequest req, RestResponse res, @Path("id") String id, @Content JsonMap json)
      Restocks one species from its row's detail panel.

      The species is the path's {id}, which must equal the body's targetId, which the key must be bound to. The body's fields supply only count; any other field (the panel's hidden species included) is ignored. A replay of a claimed key returns the same job, or a JobKeyLedger.SPENT_CODE refusal once the registry has aged the job out.

      Parameters:
      req - The request.
      res - The response (202, or 409 on a refusal).
      id - The species name.
      json - The action envelope {action, targetId, idempotencyKey, fields:{count}}.
      Returns:
      The job ref (202), a 409 refusal, or a 429 app:job-limit refusal.
      Throws:
      NotFound - If the species is unknown.
      BadRequest - If the envelope is malformed or count is not a whole number 1-50.
    • groomRunDetail

      public JsonMap groomRunDetail(@Path("id") String id)
      A groom run's detail panel.
      Parameters:
      id - The run id.
      Returns:
      {contractVersion, fields:<the run's row>}.
      Throws:
      NotFound - If the run is unknown.
    • groomRunKey

      public Map<String,Object> groomRunKey(RestRequest req, @Path("id") String id)
      Issues a regroom key bound to one groom run and the caller.
      Parameters:
      req - The request.
      id - The run id.
      Returns:
      {contractVersion, action, targetId, idempotencyKey}.
      Throws:
      NotFound - If the run is unknown.
    • regroom

      public ActionResult regroom(RestRequest req, RestResponse res, @Path("id") String id, @Content JsonMap json) throws IOException
      Grooms a run's pet again.

      Ledger keys only, bound to the path's run: the pet comes from the server's run record, never the request. /groom stays the dialog's self-targeted endpoint; one endpoint taking both kinds of key would be only as strong as the self-targeted one. A replay returns the same new run while it is kept, then a JobKeyLedger.SPENT_CODE refusal once the run list has evicted it; never a second run.

      Parameters:
      req - The request.
      res - The response (409 on a refusal).
      id - The run id.
      json - The action envelope {action, targetId, idempotencyKey}.
      Returns:
      The new run's row, or a 409 refusal.
      Throws:
      NotFound - If the key is unclaimed and the run is unknown.
      BadRequest - If the envelope is malformed.
      IOException - If the new run's temp file cannot be created.