Table of Contents

Interface IJobStorage

Namespace
Millrace.Storage
Assembly
Millrace.dll

The Layer 0 job storage contract (ARCHITECTURE.md §4). Providers implement a small surface with strict atomicity guarantees (§4.2) — everything clever (state machines, retry math, cron) lives in the engine. The conformance kit in Millrace.Storage.Verification enforces every normative statement in these docs; a provider that passes is a supported provider.

public interface IJobStorage

Remarks

Time. Providers take a TimeProvider at construction and use it for every now comparison (lease expiry, due checks, claims). They never read database server time. Multi-node deployments therefore require clock synchronization; configuration must satisfy LeaseDuration > HeartbeatInterval + clock skew + renewal margin (defaults tolerate ~4 minutes of skew). Excess skew degrades to duplicate execution or early recurring fires — never state corruption; the transition fence holds at any skew.

Properties

Capabilities

Optional powers this provider offers, which the engine adapts to.

Methods

ActivateDueJobsAsync(DateTimeOffset, int, CancellationToken)

Moves due Scheduled/Failed jobs (DueAt <= now) to Enqueued, clearing DueAt, in DueAt ASC order (oldest first; ties by enqueue order), up to batchSize; returns the number moved. Safe to run concurrently on every node — each job activates exactly once.

ApplyAsync(JobTransition, CancellationToken)

Applies an engine-computed transition atomically behind the fence (§4.2.3); false = fence rejected, nothing changed.

ClaimAsync(ClaimRequest, CancellationToken)

Exclusively claims up to MaxCount jobs (§4.2.1–2).

EnqueueAsync(IReadOnlyList<JobRecord>, CancellationToken)

Inserts jobs all-or-nothing and returns their effective ids positionally.

GetDueRecurringAsync(DateTimeOffset, int, CancellationToken)

Plain read of records with NextFireTime <= now, ordered NextFireTime ASC (most overdue first, so a backlog cannot starve old definitions), up to batchSize.

GetJobAsync(JobId, CancellationToken)

Reads one job, or null if no such job exists.

GetRecurringAsync(string, CancellationToken)

Reads one recurring definition, or null if none is registered under that id.

RemoveRecurringAsync(string, CancellationToken)

Removes a recurring definition.

RenewLeasesAsync(string, IReadOnlyList<JobId>, TimeSpan, CancellationToken)

Extends leases for in-flight jobs; returns the ids actually renewed.

TryCancelAsync(JobId, CancellationToken)

Cancels a job. Atomic: Scheduled/Enqueued/Failed/Awaiting ⇒ Cancelled (FinishedAt set, key released, transitive Awaiting-descendant cascade) and returns true; Processing ⇒ sets CancelRequested only (state and fence untouched) and returns true; terminal or unknown ⇒ false, no mutation. CancelRequested never blocks a fenced ApplyAsync(JobTransition, CancellationToken) — a completing worker may still win with Succeeded.

TryFireRecurringAsync(string, DateTimeOffset, DateTimeOffset, JobRecord, CancellationToken)

Fenced fire (§4.2.5, strengthened): compare-and-set on (id, expectedFireTime) advancing NextFireTime to nextFireTime and setting LastFireTime = expected, inserting job in the same atomic operation iff the CAS wins. Returns whether this caller won — exactly one node enqueues each occurrence, with no crash window between fence and enqueue.

TryRunNowAsync(JobId, CancellationToken)

Makes a job that is waiting out its retry backoff claimable immediately (§11.32). Atomic: FailedEnqueued with DueAt cleared, returning true; any other state, or unknown, ⇒ false with no mutation.

UpsertRecurringAsync(RecurringJobRecord, CancellationToken)

Single atomic upsert. Insert stores the record as given. Update overwrites Cron/Queue/Invocation/Retry/Priority/TenantId/UpdatedAt; takes NextFireTime from the record iff the stored Cron differs from the record's (else preserves the stored value — the engine always passes a freshly computed NextFireTime, conditionally unused); always preserves LastFireTime/CreatedAt.