Skip to content

BackWave 1.8.0-preview.1

Pre-release
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.