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 mixin.
JobsRestimplementsAsyncJobsMixinandTerminalMixin.AsyncJobsMixinserves the stream and cancel operations under/juneau-jobs; the resource supplies theAsyncJobRegistrythroughasyncJobRegistry(). - Submit once.
POST /console/ops/jobs/restockbuilds anIdempotencyKeyfrom the body'sidempotencyKeyand callsAsyncJobRegistry.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 aModalDefcarryingIdempotencyKey.mintSelfTargeted("restock")and theselfTargetedflag. 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), whichActionResult.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-logcard: 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.slowMswaits before touching the store, so a cancel changes nothing;holdMsholds 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.HardTimeoutbean, which is per resource instance, so one test host's short timeout does not leak into another. A job that exceeds it settles withunknownandapp: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 answers429with the refusalapp: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 throughTerminalMixin, 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-logregion that restocks that species only: the panel fetches a key bound to the row fromPOST /console/ops/jobs/species/{id}/key, andPOST /console/ops/jobs/species/{id}/restockverifies it withRowJobSubmitandJobKeyLedger. The species comes from the verifiedtargetId; the panel's hiddenspeciesfield 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 with409. - A job button. Each groom run's detail panel has a "Groom again" button, painted by the petstore's
petstore-regroompopulator withhelpers.jobButton. It posts toPOST /console/ops/jobs/groom-runs/{id}/regroomwith a key from that run's key endpoint, and the endpoint answers synchronously with anActionResult, so no stream opens. The pet comes from the server's run record, not from the request. The dialog's/groomendpoint 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
- Twin pattern: see 19.17. Template and Builder Twins.
- Async Jobs:
AsyncJobsMixin,AsyncJobRegistryandAsyncJobRef. - Job Log Card: the
job-logcard, cancel and reload, and its row-detail region. - Row Jobs:
JuneauViews.jobs,helpers.jobButton,JobKeyLedgerandRowJobSubmit. - Terminal Region: the terminal card and
TerminalMixin. - Rich Table Views: Actions:
IdempotencyKeyand action results. - Petstore: Audit: where
job:restockwrites show up.