Skip to content

Releases: Back-Wave/BackWave

BackWave 1.8.0-preview.1

Pre-release

Choose a tag to compare

@pdevito3 pdevito3 released this 09 Oct 21:21

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 --prerelease

Use 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. A JsonElement member reads a JSON null as a JsonElement of kind Null.
  • 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 = true and JobSnapshot.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 retrying filter on search_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 JsonSerializerContext classes. 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 an int parameter for a long property) 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_cause column and an index for the Retrying query. Postgres and Oracle go to schema version 2, SQL Server and SQLite to version 3. With AutoMigrate on, the store applies it on first use. The Oracle migrator now sets ddl_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

Choose a tag to compare

@pdevito3 pdevito3 released this 21 Sep 19:07

BackWave 1.7.0

Added

  • Oracle storage adapter. BackWave.Oracle targets Oracle 19c and later, passes the same Conformance Suite as the other adapters, and carries the same OpenTelemetry db.* 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. Set OracleStoreOptions.EnableWakeUpHints to wake an idle pump on enqueue through DBMS_ALERT. The hint is off by default. With it on, pickup latency drops from the 5 s poll interval to about 138 ms. A missing EXECUTE grant on SYS.DBMS_ALERT logs 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. IJobStore gains RelinquishLeasesAsync, 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 one ShutdownBudget (default 5 s). A fail-stopped group writes nothing more and lets its Leases lapse.
  • Fail-stop trigger set. InvariantTrigger names every impossible state BackWave detects. InvariantViolationException carries 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 as invariant_trigger, the backwave.invariant.violations counter tagged backwave.invariant.trigger and backwave.invariant.action, and the health check state. A fence rejection degrades and is counted where it was dropped before.
  • Tag length bounds. StoreBounds.MaxTagKeyLength and StoreBounds.MaxTagValueLength (both 200 by default) are enforced on every tag write. An over-limit tag is rejected with EnqueueResult.TagTooLong or WorkflowEnqueueResult.TagTooLong and 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 to MaxMonitorPageSize. The default implementation pages in memory. All five first-party stores read one bounded page. The Pro Workflows page and the MCP list_workflows tool use it, so a refresh no longer loads every workflow.
  • Observer report outcome. IJobStore.TryReportObserverDeliveriesAsync reports whether a delivery report was applied, fenced, or unknown. The default implementation forwards to ReportObserverDeliveriesAsync. 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: BackWaveHealthCheck now reports Degraded on a transient store fault or a partially halted group. It reported Healthy before. A readiness probe that fails on Degraded will restart pods on faults it used to ride out. ASP.NET maps Degraded to 200 OK, so a stock MapHealthChecks endpoint is unchanged. Only a wholly halted group reports Unhealthy. The degraded description names the group and the exception type only, so provider text (host, database, login) no longer reaches an unauthenticated probe.
  • RetryPolicy.MaxAttempts is 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.
  • JobState ordinals 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 adds 0002_transition_position.sql (a high-water mark so the Transition Log position never regresses after a retention purge). Both are additive. With AutoMigrate on, the store applies them on first use.

Fixed

  • A Worker Group admitted far more than PoolSize handlers under a burst (measured 160 to 400 with PoolSize 20). 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 BW0004 names the payload type, not the Wire Name.

All 14 packages ship at 1.7.0 on nuget.org.

v1.7.0-preview.2

v1.7.0-preview.2 Pre-release
Pre-release

Choose a tag to compare

@pdevito3 pdevito3 released this 26 Aug 02:48

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. Set OracleStoreOptions.EnableWakeUpHints to turn it on. It is off by default, and with it off no DBMS_ALERT call runs. Polling stays the only correctness mechanism. A missing EXECUTE grant on SYS.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 PoolSize under a burst. Measured live concurrency reached 160 to 400 handlers at PoolSize 20. The driver now reserves a claim batch's pool slots when it issues the batch.
  • The idle-poll backoff saturated to MaxPollInterval within 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

v1.7.0-preview.1 Pre-release
Pre-release

Choose a tag to compare

@pdevito3 pdevito3 released this 19 Aug 17:30

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_locks anchor table gives each queue an advisory row-lock and replaces DBMS_LOCK, which needs a grant that many managed Oracle instances do not give.

Fixed

  • The BW0004 generator 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

Choose a tag to compare

@pdevito3 pdevito3 released this 17 Aug 17:41

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 and JobRecord gains no columns. A job with no attribute behaves as before. The source generator validates the attribute at compile time: BW0008 for an attempt ceiling below 1 or above the 1000 cap, BW0009 for an invalid backoff list, and BW0010 for 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-in MaxPollInterval bounds a geometric idle backoff, and the group sleeps until the next scheduled job is due when the store can report it. MaxPollInterval defaults to TimeSpan.Zero (disabled), so the default path stays byte-identical and issues no extra query. A non-breaking ClaimBatchAsync default interface method on IJobStore returns 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

Choose a tag to compare

@pdevito3 pdevito3 released this 16 Aug 01:53

BackWave 1.5.0

Added

  • The send span now carries the enqueue call site as OpenTelemetry code.* tags: code.function.name, code.file.path, and code.line.number. The values come from compiler CallerInfo attributes, 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 tracestate now survives the enqueue-to-execute hop. A job stored only the traceparent, so vendor routing and sampling state in tracestate was dropped. The process span's link back to the send context now restores both parts. A tracestate that would overflow the narrowest store column (SqlServer trace_context, nvarchar(450)) is dropped, so an enqueue never fails over a large tracestate.

All 13 packages ship at 1.5.0 on nuget.org.