jobs
v0.1.58
KickSide durable leased-job engine and projection scheduler — jobs service/api/persist/workers, effect-intent machinery, and the leased cursor scheduler
Run
wippy run kickside/jobskickside/jobs
Durable leased-job engine: slots, claims, leases, supervised execution and wake demand.
kickside.core.jobs— shared job vocabulary, the component-facing durable job API, the runtime policy and the atomic thread-fact emitter.kickside.core.jobs.persist— claim/lease/lifecycle and wake demand overkickside_job.kickside.core.jobs.service— the supervised worker pool that claims and executes jobs.kickside.core.jobs.migrations— thekickside_jobschema. The retiredkickside_effect_intentandkickside_effect_attempttables remain until the contract-phase migration drops them; nothing writes them.
A Hub update that changes the worker code upgrades the pool in place: the pool
keeps its PID, slots and assignments and keeps claiming on the new code. A slot
running an execution lets it finish under the code it started with, then exits
and is replaced by a slot on the new code; idle slots are replaced at once. The
drain of an in-flight execution is bounded by that execution's own declared
max_execution ceiling (payload max_execution_ms, else policy
proc.max_execution); past it the ordinary timeout termination and retry policy
apply. An upgrade never terminates or re-runs an execution.
A job carries no execution identity: a worker_ref target runs under the worker frame and
resolves the identity held by its own owner record.
Sits below kickside/core: core depends on jobs (thread append publishes demand through the jobs wake
library); jobs depends on no core entry.
PostgreSQL migration 04 creates the Jobs-owned updated_at trigger function, so
Jobs can initialize its database without Core migrations. Migration 23 rebinds
legacy installations to that function without changing Core's function. Undoing
23 retains the Jobs trigger; undoing 04 removes its tables and function.
Dependency interface
The host must supply target_db; it is injected into both the jobs migration and the
module-owned db_config. The worker, APIs, job client and lease logic
all resolve that config, so runtime SQL always follows the host's database mapping.
jobs.get_job(id) reads the exact job returned by enqueue and returns (row, nil), (nil, nil) when absent, or (nil, err) on failure. The owning caller authorizes its resource before reading the job. Thread listings remain scoped to canonical thread owners.
jobs.get_job_statuses(ids) reads an exact set of job IDs in one query and returns an ID-to-status map; absent or malformed UUIDs are omitted. jobs.get_job_statuses_tx(tx, ids) reads through the caller's transaction on the configured Jobs database, including its uncommitted writes. It neither commits nor rolls back that transaction. Both are internal-owner APIs: callers authorize their resource before reading job state.
jobs.wake_job(id, { control = true, ensure_due = true }) ensures dispatch without rescheduling an already due pending job or requesting another execution of a running job. It retains their retry, lease, schedule and demand state and still notifies the worker pool. Parked, disabled and future pending jobs receive the ordinary control wake. A ready job returns already_due = true alongside the existing result fields. Omitting ensure_due retains the existing control and demand wake behavior.
jobs.enqueue and jobs.enqueue_tx accept optional demand = true with slot = true. Enrollment and control demand commit together: parked work becomes due, and a running attempt retains a follow-up. Reusing an active one-shot identity with demand returns a conflict. Omitting demand retains the existing enqueue behavior.
After committing a transaction containing jobs.enqueue_tx(tx, dbtype, params),
call jobs.nudge_workers(): (integer, error?). Never nudge before commit or after
rollback. The result counts notified pools (1 when delivered, 0 when no pool
is registered); an absent pool leaves committed demand for boot recovery. Return
or handle notification errors so delivery can be retried. The facade calls the
public kickside.core.jobs:nudge_workers entry using ordinary Jobs runtime
access. Jobs owns its transport permission and sends under its own identity;
callers need no persistence call grant or process-send authority.
jobs.purge_slot(params: table): (integer?, error?) removes a retired owner's
exact slot, selected by the required nonempty strings namespace, scope, and
key, with its settled run history. The owning caller authorizes retirement
before invoking this operation. Each call deletes at most 100 settled runs;
it returns 1 when the slot is removed or 0 when absent. A retryable conflict
with details.cleanup_pending preserves the existing continuation contract:
needs_continue = true requests another page, while wait_for = "lease_release"
or "run_history_settlement" requires settlement before retry. Running leases
and unsettled history are retained. Callers import only kickside.core.jobs:jobs.
Worker capacity declarations
A worker entry can declare a Settings-backed capacity shared with other worker entries:
meta:
job_capacity: app.capacity
job_capacity_settings:
namespace: app.capacity
global_limit_key: global_limit
concurrency_key_limit_key: concurrency_key_limit
Workers in one group declare the same Settings values. Jobs for a declared
worker require a nonempty concurrency_key. Claims enforce the group-wide
global limit and the limit for each concurrency key. Both values are read from
the declared Settings namespace at claim time. Jobs for workers without this
declaration retain the ordinary single-flight behavior for a concurrency key.
Declarations and limits are observed once per claim/backlog operation; they are not cached between operations. Invalid or unavailable declared Settings fail admission rather than removing the limit. The declared group must remain stable while its jobs are active. Enable declarations only after every Jobs claimant sharing that queue has been upgraded to support capacity enforcement.
jobs.complete_tx accepts optional demand_satisfied = true when the worker proves under its durable source lock that the completed run covers the source head. It consumes ordinary demand received during that run; independent control wakes still rearm the slot. Omission retains the existing demand behavior.
Failure transitions accept an optional final { retry_after_ms = milliseconds }
argument on fail_tx and fail_job. The structured floor can exceed the policy
cap. Slots use slot_retry; one-shot jobs use retry. Both retain their configured
base as the lower bound of jitter. Worker failures carry native error details to
this argument before rendering the history error text. Malformed provider retry
hints are ignored so they cannot prevent failure settlement; explicit failure
transition arguments remain validated.
Completion options accept park = true independently of record.status. Parking
suppresses ordinary wake demand while preserving the requested history status;
a pending control wake retains priority. Without park, completion keeps its
existing record-based parking behavior.
wake_multi(tx, keys, dbtype?, completion_job_id?) accepts an optional completing
job ID. When fan-out locks jobs, it includes that row in the globally ordered
prelock without waking it unless it independently qualifies for a supplied key.
With no fan-out job locks, completion takes its sole job lock itself. Wake keys
are published before any job row is locked.