Repository navigation
Releases: Back-Wave/BackWave
Release list
BackWave 1.8.0-preview.1
BackWave 1.8.0-preview.1
This is a prerelease. The headline is complex [Job] payload members: a payload can now carry lists, dictionaries, and nested records, and keep the generated registry, handler discovery, [Retry], and Labels. The release also adds a Retrying view, a filtered job count, and a SQL Server deadlock fix.
Install the prerelease:
dotnet add package BackWave --prereleaseUse the same version for every BackWave package in the app.
Added
Complex payload members
Before this release, a [Job] payload member could only be a scalar: string, bool, a number, an enum, Guid, DateTime, or DateTimeOffset. Any other type gave BW0004, and the job had to be registered by hand with JobRegistration.Create.
Now the generated codec hands each non-scalar member to System.Text.Json. It reads the metadata from a JsonSerializerContext in your own assembly, so the codec stays NativeAOT- and trim-safe. The codec still writes every scalar member itself, so the bytes of every job that compiles on 1.7 do not change.
To try it with a payload record, list the record in a context:
using System.Text.Json.Serialization;
using BackWave.Jobs;
[Job("tag-order")]
public sealed record TagOrder(Guid OrderId, List<string> Tags, Address ShipTo);
public sealed record Address(string Street, string City);
public sealed class TagOrderHandler : IJobHandler<TagOrder>
{
public Task HandleAsync(TagOrder job, JobContext context, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
// One listing of the payload serves every complex member, nested types included.
[JsonSerializable(typeof(TagOrder))]
internal sealed partial class AppJson : JsonSerializerContext;Then enqueue it as usual:
await client.EnqueueAsync(
new TagOrder(Guid.NewGuid(), ["gift", "rush"], new Address("1 Main St", "Springfield")),
dueTime: DateTimeOffset.UtcNow);For a [Job] method, list each parameter type. The STJ generator cannot see the payload record that BackWave generates for the method:
[Job("send-batch")]
public Task SendBatch(List<string> recipients, CancellationToken cancellationToken) => /* ... */;
[JsonSerializable(typeof(List<string>))]
internal sealed partial class AppJson : JsonSerializerContext;Things to know:
- The context must be in the same assembly as the
[Job]. A context in a referenced assembly is not visible to the generator. - The context options (for example
PropertyNamingPolicy) apply inside a complex member only. The payload member names and the scalar members do not change. - A class payload binds only through the listing of the payload type. A later listing of a member type in another context, for example for an API response, does not change the wire format of queued jobs.
- A JSON null or a missing complex member reads as
null. AJsonElementmember reads a JSON null as aJsonElementof kindNull. - A decode error names the member, for example
$.ShipTo.Street. - If you forget the listing, the build fails with BW0017. The lightbulb fix adds
[JsonSerializable(typeof(...))]to a context that the codec can use, or creates a new context. Fix All lists every missing type in one edit.
New build diagnostics:
| ID | Severity | When |
|---|---|---|
| BW0011 | Error | A System.Text.Json attribute (for example [JsonPropertyName]) is on a complex member. The codec does not read it. |
| BW0012 | Error | More than one JsonSerializerContext lists the type that the codec needs. BackWave does not pick one. |
| BW0013 | Error | The only listing is one the codec cannot use: a GenerationMode = Serialization listing or default, or a private, protected, or file-local context. |
| BW0014 | Error | A [Job] type is generic, or is nested in a generic type. |
| BW0016 | Error | A payload with complex members has a type-level [JsonConverter]. |
| BW0017 | Error | No JsonSerializerContext lists the type that the codec needs. |
| BW0018 | Warning | A System.Text.Json attribute is on a scalar member. The codec does not read it. |
| BW0019 | Warning | A payload with only scalar members has a type-level [JsonConverter]. The converter never runs. |
BW0004 now also rejects the member types that System.Text.Json writes but cannot read back the same: object and dynamic, an interface or abstract class that STJ cannot create, IReadOnlySet<T>, stacks, multi-dimensional arrays, non-generic collections, ConcurrentBag<T>, value tuples, and the types that STJ rejects by design (Type, delegates, nint). Each message names a type to use instead. BW0005 now also rejects a generic [Job] method.
Retrying jobs
Each job now records a Retry Cause: why it last went back to Scheduled because an attempt went wrong.
HandlerFailed: the handler failed and the retry policy scheduled another attempt.LeaseExpired: the lease expired before the worker reported an outcome.
A Scheduled job with a Retry Cause is Retrying. A new job, a requeued job, and a job that a stopping worker handed back are not Retrying, so a deploy does not look like a wave of failures.
To try it, enqueue a job whose handler throws once, then look at:
- The Monitor API:
JobQuery.Retrying = trueandJobSnapshot.RetryCause. - The dashboard: the Retrying tab on the Failures page, the Retrying option in the Jobs state filter, and the Retry cause row on the job detail page.
- The MCP server: the
retryingfilter onsearch_jobs.
Jobs that are Scheduled before the upgrade have no cause, so they do not show as Retrying.
Filtered job count
BackWaveMonitor.GetJobCountAsync(JobQuery) returns the number of jobs that match the state, queue, wire name, schedule id, and tag filters of a JobQuery. It ignores the paging fields, and the monitor page size does not cap it.
var failing = await monitor.GetJobCountAsync(new JobQuery { State = JobState.DeadLettered, Queue = "billing" });All five first-party stores answer with one COUNT(*). IJobStore.CountMatchingJobsAsync is a default interface method that pages through ListJobsAsync, so a custom store compiles and gives the correct count. The MCP server adds the read-only count_jobs tool.
Changed
- CAUTION: Make sure that no workflow output type or Workflow Input seed type is listed in two
JsonSerializerContextclasses. That case now fails the build with BW0012. Before, the type bound silently to the first context by name. List the type in one context. - CAUTION: Make sure that each constructor parameter of a scalar
[Job]member has the same type as its property. A wider property (for example anintparameter for alongproperty) now fails the build with BW0004. Make the two types the same. - A workflow output or seed type that only an unusable listing names (serialization-only, or a private, protected, or file-local context) now gives BW0013. Before, the build failed inside the generated code with CS0122, or every decode threw.
- Schema. Each SQL adapter adds one additive migration: a nullable
retry_causecolumn and an index for the Retrying query. Postgres and Oracle go to schema version 2, SQL Server and SQLite to version 3. WithAutoMigrateon, the store applies it on first use. The Oracle migrator now setsddl_lock_timeout, so a cold boot of several nodes does not fail with ORA-00054.
Fixed
- SQL Server: the claim and the outcome report deadlocked under concurrent load. Both now record their Transition Log entries before they take X locks on the job rows. The claim also uses one less statement (5 for a claim of 32 jobs).
- Dashboard: an open dashboard tab held the live stream open until the host shutdown timeout. The Worker Groups then had no time to hand back their Leases on a clean stop. The stream now ends when the application stops.
All 14 packages ship at 1.8.0-preview.1 on nuget.org.
BackWave 1.7.0
BackWave 1.7.0
Added
- Oracle storage adapter.
BackWave.Oracletargets Oracle 19c and later, passes the same Conformance Suite as the other adapters, and carries the same OpenTelemetrydb.*spans. Its hot paths are batched: a claim of 32 jobs costs 9 statements, an outcome report of 32 costs 5, and a lease sweep costs 4. The adapter measures 4.1x the throughput of a per-row design at a 1 ms round trip. SetOracleStoreOptions.EnableWakeUpHintsto wake an idle pump on enqueue throughDBMS_ALERT. The hint is off by default. With it on, pickup latency drops from the 5 s poll interval to about 138 ms. A missingEXECUTEgrant onSYS.DBMS_ALERTlogs one warning (EventId 1501) and the pump keeps polling. - Clean-stop lease hand-back. A Worker Group that stops cleanly now returns the Leases it holds, so a healthy node picks the work up at once instead of after a full Lease Duration.
IJobStoregainsRelinquishLeasesAsync, a default interface method that returns zero, so a custom store compiles and behaves as before. The hand-back and the final outcome report share oneShutdownBudget(default 5 s). A fail-stopped group writes nothing more and lets its Leases lapse. - Fail-stop trigger set.
InvariantTriggernames every impossible state BackWave detects.InvariantViolationExceptioncarries the tripped trigger. The pump and every adapter now halt on a trigger instead of executing past it: a claim that returns a terminal job, an outcome batch answered with a different row count, a short heartbeat answer, a claim wider than its bound, a workflow member cycle, and a live member with no workflow row. The trigger reaches log 2001 asinvariant_trigger, thebackwave.invariant.violationscounter taggedbackwave.invariant.triggerandbackwave.invariant.action, and the health check state. A fence rejection degrades and is counted where it was dropped before. - Tag length bounds.
StoreBounds.MaxTagKeyLengthandStoreBounds.MaxTagValueLength(both 200 by default) are enforced on every tag write. An over-limit tag is rejected withEnqueueResult.TagTooLongorWorkflowEnqueueResult.TagTooLongand is never truncated. The SQL Server and Oracle adapters store tags in fixed-width columns (200 and 256 characters), so they reject a wider bound when the store is created. - Paged Workflow list.
IJobStore.ListWorkflowsAsync(WorkflowListQuery)pages by a keyset cursor on(created_at, workflow_id), clamped toMaxMonitorPageSize. The default implementation pages in memory. All five first-party stores read one bounded page. The Pro Workflows page and the MCPlist_workflowstool use it, so a refresh no longer loads every workflow. - Observer report outcome.
IJobStore.TryReportObserverDeliveriesAsyncreports whether a delivery report was applied, fenced, or unknown. The default implementation forwards toReportObserverDeliveriesAsync. The hosting layer now acts on a fenced report instead of discarding it. - Rolling upgrade tolerance. Every adapter now accepts a schema version newer than its own, so an N-1 node keeps running while the fleet upgrades. A version older than expected still fail-stops.
- Conformance Suite capability manifest. A store that declares a capability whose hook still returns the default now fails the suite instead of passing silently.
Changed
- CAUTION:
BackWaveHealthChecknow reportsDegradedon a transient store fault or a partially halted group. It reportedHealthybefore. A readiness probe that fails onDegradedwill restart pods on faults it used to ride out. ASP.NET mapsDegradedto 200 OK, so a stockMapHealthChecksendpoint is unchanged. Only a wholly halted group reportsUnhealthy. The degraded description names the group and the exception type only, so provider text (host, database, login) no longer reaches an unauthenticated probe. RetryPolicy.MaxAttemptsis bounded to the shared attempt ceiling of 1000, the same ceiling[Retry]has.- Lease sweep round trips. The lease sweep on Postgres, SQL Server, and SQLite records its transitions in one batch. At the default claim batch of 32 a full sweep costs 5 statements on Postgres and 6 on SQL Server, down from 67 and 68. The per-row transition prune is skipped under the retention cap on all three.
JobStateordinals are pinned as the persisted wire format and guarded by a test. No value changed.- Schema. SQL Server adds
0002_lease_owner_index.sql(an index that lets the hand-back lock only its own rows). SQLite adds0002_transition_position.sql(a high-water mark so the Transition Log position never regresses after a retention purge). Both are additive. WithAutoMigrateon, the store applies them on first use.
Fixed
- A Worker Group admitted far more than
PoolSizehandlers under a burst (measured 160 to 400 withPoolSize20). A claim batch now reserves its slots when it is issued. - Idle-poll backoff ramped on the empty-poll count, so a drain tail saturated the delay at once and a job enqueued right after waited the whole ceiling. The ramp now follows elapsed idle time.
- A recurring schedule with a Skip or Coalesce policy stalled after a long outage: the planner walked 32 missed ticks per poll. The fresh tick now mints in the first poll.
- A handler that returned a Job Output larger than the store cap halted the whole Worker Group. The job now dead-letters with the rejection as its Failure Detail and the group keeps running.
- SQL Server: hot-path writes that lose a deadlock (error 1205) retry up to four times. The claim retries one Queue's transaction, not the whole claim. Heartbeat and relinquish ids travel through
OPENJSON. - SQLite: the Transition Log position regressed after a retention purge, so an Observer cursor missed transitions. The store's own connection pool is now cleared on dispose.
- SQLite and Oracle: an expired parent's transition is recorded before its children's cascade, so an Observer sees the parent go terminal first.
- Oracle: heartbeat ids, hand-back transitions, and tag reads are sent in slices under the
IN-list limit, so a wide batch or a large workflow stays readable. - A drained workflow is purged whole under its worst member's retention class.
- The outcome fence now allows the clock skew a fleet can carry, so one skewed node no longer trips the invariant counter.
- The process span records an Error status and the exception on a handler-side halt.
- The MCP queue-depth description names the real job states.
- The source generator's
BW0004names the payload type, not the Wire Name.
All 14 packages ship at 1.7.0 on nuget.org.
v1.7.0-preview.2
BackWave 1.7.0-preview.2
Prerelease. This preview makes the Oracle adapter much cheaper on the network, and it corrects three worker-group defects that affect every adapter.
Added
- The Oracle adapter can wake an idle worker on enqueue through
DBMS_ALERT. SetOracleStoreOptions.EnableWakeUpHintsto turn it on. It is off by default, and with it off noDBMS_ALERTcall runs. Polling stays the only correctness mechanism. A missingEXECUTEgrant onSYS.DBMS_ALERT, or a channel fault, logs one warning and keeps polling. Pickup latency drops from the 5 s poll interval to about 138 ms.
Changed
- The Oracle adapter costs far fewer database round trips on its hot paths. A claim of 32 jobs goes from 71 statements and 32 LOB reads to 9 statements and 0 LOB reads. A report of 32 outcomes goes from 97 statements to 5. A sweep of 32 expired leases goes from 65 statements to 4. A 200-job monitor page goes from 400 LOB reads to 0. Measured across a 1 ms network hop, throughput is 4.1 times the previous preview.
Fixed
- A worker group admitted many more jobs than
PoolSizeunder a burst. Measured live concurrency reached 160 to 400 handlers atPoolSize20. The driver now reserves a claim batch's pool slots when it issues the batch. - The idle-poll backoff saturated to
MaxPollIntervalwithin milliseconds after a drain. A job that became due right after that waited out the whole ceiling. The ramp now follows elapsed idle time. Pickup latency drops from about 30 s to about 1 s. - A worker group reported healthy while its store was down. A fault on a maintenance sweep also stranded a claim's pool reservation, which wedged the pool shut.
This is a preview package. Do not use it in production.
v1.7.0-preview.1
BackWave 1.7.0-preview.1
Prerelease.
Added
- BackWave.Oracle storage adapter for Oracle Database 19c and later. The adapter passes the full conformance suite. A
queue_locksanchor table gives each queue an advisory row-lock and replacesDBMS_LOCK, which needs a grant that many managed Oracle instances do not give.
Fixed
- The
BW0004generator diagnostic now names the payload type instead of the Wire Name. The message reads "Member '{0}' of job payload '{1}'", so{1}must be the type. A reader no longer maps a Wire Name back to the type by hand.
This is a preview package. Do not use it in production.
v1.6.0
BackWave 1.6.0
Added
- Per-job retry override with
[Retry(maxAttempts, backoffSeconds...)]. On the loud-failure path the node resolves the attribute to a retry disposition and computes the next attempt time, so the override wins over the Worker Group retry policy. Nothing new is stored andJobRecordgains no columns. A job with no attribute behaves as before. The source generator validates the attribute at compile time:BW0008for an attempt ceiling below 1 or above the 1000 cap,BW0009for an invalid backoff list, andBW0010for a[Retry]on a type with no[Job]. - Adaptive idle-poll backoff. An idle worker group no longer polls the store at the fixed
PollInterval. An opt-inMaxPollIntervalbounds a geometric idle backoff, and the group sleeps until the next scheduled job is due when the store can report it.MaxPollIntervaldefaults toTimeSpan.Zero(disabled), so the default path stays byte-identical and issues no extra query. A non-breakingClaimBatchAsyncdefault interface method onIJobStorereturns the claimed jobs plus an advisory next-due time. The advisory value is never a correctness input; polling stays the sole correctness mechanism.
All 13 packages ship at 1.6.0 on nuget.org.
v1.5.0
BackWave 1.5.0
Added
- The
sendspan now carries the enqueue call site as OpenTelemetrycode.*tags:code.function.name,code.file.path, andcode.line.number. The values come from compilerCallerInfoattributes, so they are Native-AOT safe and add no runtime cost. The testing harness forwards the call site, so it survives the harness wrapper.
Fixed
- W3C
tracestatenow survives the enqueue-to-execute hop. A job stored only thetraceparent, so vendor routing and sampling state intracestatewas dropped. The process span's link back to the send context now restores both parts. Atracestatethat would overflow the narrowest store column (SqlServertrace_context,nvarchar(450)) is dropped, so an enqueue never fails over a largetracestate.
All 13 packages ship at 1.5.0 on nuget.org.