Class SqlServerStorage
The SQL Server provider (ARCHITECTURE.md §11.2, §4.3).
public sealed class SqlServerStorage : IJobStorage, IWorkflowStorage, IMonitoringStorage
- Inheritance
-
SqlServerStorage
- Implements
- Inherited Members
Remarks
Claims with UPDLOCK, READPAST, ROWLOCK over an ordered CTE — SQL Server's equivalent of
FOR UPDATE SKIP LOCKED. Advertises no notification capability, because SQL Server has no
LISTEN/NOTIFY, so workers fall back to adaptive polling exactly as §4 P3 intends.
Three dialect differences drive most of what looks unusual here. SQL Server has no row-value
comparison, so keyset predicates are expanded by hand. Its unique indexes treat NULLs as equal,
which is what the untenanted idempotency scope wants anyway. And uniqueidentifier sorts
in an internal mixed-endian layout that matches neither RFC 4122 nor
CompareTo(Guid) — so wherever the contract breaks a tie on an id, this
provider orders by CAST(id AS char(36)), the canonical hex form whose lexicographic order
is RFC 4122. CAST(… AS binary(16)) looks right and is not.
Constructors
- SqlServerStorage(string, TimeProvider?, SqlServerStorageOptions?)
Creates the provider over a connection string.
Properties
- Capabilities
No push mechanism, so the engine polls (§4 P3).
Methods
- ActivateDueJobsAsync(DateTimeOffset, int, CancellationToken)
Moves due Scheduled/Failed jobs (
DueAt <= now) to Enqueued, clearing DueAt, inDueAt ASCorder (oldest first; ties by enqueue order), up tobatchSize; returns the number moved. Safe to run concurrently on every node — each job activates exactly once.
- AddBookmarkAsync(BookmarkRecord, CancellationToken)
Records that an instance is waiting for a signal.
- 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).
- ConsumeBookmarkAsync(string, string, CancellationToken)
Atomically consumes (removes and returns) the oldest matching bookmark — ordered by CreatedAt, then Id — or returns null when none match. At-most-once under arbitrary concurrency (§4.2.4): a signal resumes exactly one waiting instance.
- CreateInstanceAsync(WorkflowInstanceRecord, CancellationToken)
Stores a new instance with
Revision = 1; duplicate id throws MillraceConcurrencyException.
- 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, orderedNextFireTime ASC(most overdue first, so a backlog cannot starve old definitions), up tobatchSize.
- GetInstanceAsync(WorkflowInstanceId, CancellationToken)
Reads one instance, or null if it does not exist.
- GetJobAsync(JobId, CancellationToken)
Reads one job, or null if no such job exists.
- GetJobDetailsAsync(JobId, CancellationToken)
Full detail for 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.
- GetStatisticsAsync(TenantFilter, CancellationToken)
Aggregate counts for the overview, scoped by
tenant.
- InitializeAsync(CancellationToken)
Creates the schema and tables (idempotent). Called lazily unless disabled.
- QueryInstancesAsync(InstanceQuery, CancellationToken)
One page of workflow instances matching
query. Same paging, cursor and tenancy rules as QueryJobsAsync(JobQuery, CancellationToken).
- QueryJobsAsync(JobQuery, CancellationToken)
One page of jobs matching
query.
- QueryRecurringAsync(RecurringQuery, CancellationToken)
One page of recurring definitions matching
query, orderedNextFireTime ASC, Id ASC.
- 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 tonextFireTimeand setting LastFireTime = expected, insertingjobin 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: Failed ⇒ Enqueued with DueAt cleared, returning true; any other state, or unknown, ⇒ false with no mutation.
- UpdateInstanceAsync(WorkflowInstanceRecord, long, CancellationToken)
Optimistic-concurrency replace: throws MillraceConcurrencyException unless the stored revision equals
expectedRevision(a missing instance is the same failure — providers must not distinguish); on success storesRevision = expectedRevision + 1.
- 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.