Skip to main content

Petstore: Jobs

The Jobs page, served at /console/ops/jobs, is the petstore's reference for asynchronous work. A restock adds new pets in the background: the browser submits it once, the server answers 202 with a job reference, and the page follows the job's progress until it settles. The page also hosts a terminal demo, a "groom pet" job whose output is streamed into a terminal card. If a write in your app takes longer than a request, copy this page.

What this page shows​

The jobs page: restock form, job and species tables, and the grooming job streamed into terminal cards

  • The mixin. JobsRest implements AsyncJobsMixin and TerminalMixin. AsyncJobsMixin serves the stream and cancel operations under /juneau-jobs; the resource supplies the AsyncJobRegistry through asyncJobRegistry().
  • Submit once. POST /console/ops/jobs/restock builds an IdempotencyKey from the body's idempotencyKey and calls AsyncJobRegistry.tryCreate(key). A resubmit with the same key returns the same job, so the resource keeps no key map of its own.
  • Self-targeted keys. The ribbon "Restock" dialog has no row to bind a key to, so restockForm() returns a ModalDef carrying IdempotencyKey.mintSelfTargeted("restock") and the selfTargeted flag. The two must travel together; see Rich Table Views: Actions for the pairing rule.
  • Completion. The job finishes with a success ActionResult (ActionResult.success(null), which ActionResult.ok() is the readability alias for) and a message such as "Restocked 3 pets".
  • A job-log card. The "Restock inventory" card is a job-log card: a small submit form (counts per species, plus two timing knobs) that posts the same restock in its flat form, shows the job's progress lines and offers Cancel. slowMs waits before touching the store, so a cancel changes nothing; holdMs holds after the pets are added, so a late cancel reports that the change was made.
  • A per-instance hard timeout. The registry's hard timeout is 30 seconds. A host overrides it by declaring a JobsRest.HardTimeout bean, which is per resource instance, so one test host's short timeout does not leak into another. A job that exceeds it settles with unknown and app:hard-timeout-exceeded.
  • A concurrency limit. At most two restocks run at once (system property petstore.jobs.limit; the registry's own cap is 8). A replay of an existing key never counts against the limit. When the limit is hit the server answers 429 with the refusal app:job-limit, and the message names the running count.
  • A hold knob for tests. System property petstore.jobs.speciesHoldMs (read on every request, at most 10000) sets how long a species row's restock holds after its effect, so a test can land a cancel in that window.
  • A terminal card. The groom job (GroomRuns) streams its output through TerminalMixin, shown twice: a card over the in-memory output and a second over the same run's file-backed transcript.
  • Row jobs. The "species" table has one row per species with its available count. Expanding a row opens a job-log region that restocks that species only: the panel fetches a key bound to the row from POST /console/ops/jobs/species/{id}/key, and POST /console/ops/jobs/species/{id}/restock verifies it with RowJobSubmit and JobKeyLedger. The species comes from the verified targetId; the panel's hidden species field is sent but ignored, so editing it changes nothing. When the job ends, the panel's "Available" value is updated from the result's row. While the open panel's job runs, the table's poll waits and its pill shows Paused. A replay of the same key returns the same job, and a key issued for one species is refused for another with 409.
  • A job button. Each groom run's detail panel has a "Groom again" button, painted by the petstore's petstore-regroom populator with helpers.jobButton. It posts to POST /console/ops/jobs/groom-runs/{id}/regroom with a key from that run's key endpoint, and the endpoint answers synchronously with an ActionResult, so no stream opens. The pet comes from the server's run record, not from the request. The dialog's /groom endpoint keeps its self-targeted key; the row endpoint accepts ledger keys only.

Template​

The template is jobs.ftlh. The job-log card is configured entirely by its submit block.

<@card type="job-log" id="restock" title="Restock inventory">{
submit: {
endpoint: '/console/ops/jobs/restock',
submitLabel: 'Restock',
fields: [
{ name: 'DOG', label: 'Dogs (0-50)', type: 'number', default: 1 },
{ name: 'slowMs', label: 'Wait before restocking (ms, 0-10000)', type: 'number', default: 0 },
{ name: 'holdMs', label: 'Hold after restocking (ms, 0-10000)', type: 'number', default: 1000 }
]
},
urlParam: 'job'
}</@card>

The species table declares its job-log region in the card body:

<@card type="datatables" id="species">{
dataMode: 'client', dataUrl: '/console/ops/jobs/species', pollIntervalMs: 2000,
columns: [ { key: 'species', label: 'Species' }, { key: 'available', label: 'Available' } ],
detail: {
endpoint: '/console/ops/jobs/species/{id}',
region: {
id: 'restock-species', populate: 'job-log',
config: {
submit: {
endpoint: '/console/ops/jobs/species/{id}/restock', envelope: 'action', action: 'restock-species',
keyUrl: '/console/ops/jobs/species/{id}/key', submitLabel: 'Restock',
fields: [ { name: 'count', label: 'How many (1-50)', type: 'number', default: 1, required: true },
{ name: 'species', type: 'hidden', source: 'row.species' } ]
},
detail: { renderer: 'fieldGrid', fields: [ { data: 'available', label: 'Available' } ] },
output: { target: { field: 'available' }, from: 'row.available' }
}
}
}
}</@card>

Builder twin​

OpsPageBuilders.jobs takes the two TerminalDef values and builds the same cards. The species table's region is built with RowDetail.region(JobLogRegion.create("restock-species")...toRegion()), and the groom runs' detail with a RegionDef that allows the petstore-regroom populator.

.card(CardSpec.of("job-log", "restock").title("Restock inventory")
.body("submit", Map.of("endpoint", "/console/ops/jobs/restock", "submitLabel", "Restock", "fields", List.of(
Map.of("name", "DOG", "label", "Dogs (0-50)", "type", "number", "default", 1),
Map.of("name", "holdMs", "label", "Hold after restocking (ms, 0-10000)", "type", "number", "default", 1000))))
.body("urlParam", "job"))
.card(CardSpec.of("terminal", "groom-output").title("Groom run").body("terminal", groomOutput.toMap()))

Source and tests​

Source: juneau-petstore/juneau-petstore-core, classes JobsRest and GroomRuns (in the ops package), OpsPageBuilders (in the pages package), and the template jobs.ftlh.

Tests: JobsRest_Test (same key same job, the 429 limit, cancel before and after the effect, the hard timeout, the streamed progress lines, and the row jobs: a key bound to one row, replays, a retargeted key refused, a tampered preset ignored, a spent key after the job ages out), PetstoreConsoleBoundary_Test (the key endpoint behind CSRF), PetstoreRowJob_BrowserTest (stream, Paused pill, result written into the panel, re-attach, cancel, "Groom again"), GroomRuns_Test and PetstoreTwins_Test.

See also​