Enqueue and Outcome Changes
The release that made Junctions() the only way to declare a chain also changed how work is
enqueued and how a run's outcome is recorded. None of it needs a version bump beyond the usual
1.x update, and most of it compiles unchanged, which is why it is listed here: several changes
only show up at runtime.
The chain declaration change has its own guide,
Removal of RunInternal and Activate. Read it
first if any train overrides RunInternal or calls Activate.
Projects scaffolded by an older Trax.Cli
trax generate from earlier Trax.Cli versions wrote trains that override RunInternal and a
trains project that references Trax.Effect, Trax.Effect.Data.InMemory, Trax.Mediator and
Trax.Scheduler with Version="1.*". A floating version restores the newest 1.x, so such a
project picks up this release on its next restore and stops compiling, with nothing changed in
the project itself.
Move each train to Junctions() as the
RunInternal guide
shows, or pin the Trax packages to an exact version until you do. Current Trax.Cli versions
scaffold Junctions().
ITrainExecutionService.QueueAsync
A new parameter before ct. The signature is now:
Task<QueueTrainResult> QueueAsync(
string trainName,
string? inputJson,
int priority = 0,
DateTime? scheduledAt = null,
CancellationToken ct = default
);A call that passes the token positionally after priority, such as
QueueAsync(name, json, 5, ct), no longer compiles, because ct now lands on scheduledAt.
Name it: QueueAsync(name, json, priority: 5, ct: ct). An assembly compiled against the old
signature fails with MissingMethodException until it is rebuilt, and a class that implements
ITrainExecutionService has to add the parameter.
A JSON null input throws JsonException. It used to throw InvalidOperationException. The
same applies to RunAsync. Code that caught InvalidOperationException to detect a bad input
should catch JsonException, which a malformed input already threw.
A null or blank input is read as {}. The parameter is now string?. Through the GraphQL
queueTrain mutation, a null input used to be stored as null and failed at dispatch; it is now
read as {}, and an input type that needs values is refused at enqueue with JsonException:
a constructor parameter with no default, as in a positional record, or a required member.
RunAsync reads a blank input the same way.
A deferred entry cancelled under its hook throws QueuedWorkCancelledException. It used to
throw a plain InvalidOperationException, the same type as an empty subject key. The new type
derives from InvalidOperationException, so existing catches still work, and carries
WorkQueueId and TrainName for a caller that has to compensate for the hook's side-effect.
See TrainExecution.
Every caller-built enqueue goes through the mediator
The GraphQL operations mutations and the dashboard used to write the work queue row
themselves. They now call QueueAsync, which changes what runs:
| Path | [TraxAuthorize] | OnQueue | QueueSubjectKey | Input size cap |
|---|---|---|---|---|
GraphQL queueTrain, requeueExecution | Now enforced | Now fires | Now stamped | Now applied |
| Dashboard queue dialog, execution Re-queue button | Not enforced (trusted scope; the dashboard is gated by its host) | Now fires | Now stamped | Now applied |
What to check:
- An
OnQueuehook now runs from these paths. A hook written on the assumption that only your own code enqueues (one that is not idempotent, or that expects a request context) now also runs when someone queues or re-queues the train from the API or the dashboard. - A caller of
queueTrainorrequeueExecutionmay now be refused with aTRAX_AUTHORIZATIONerror for a train whose requirements it does not meet. - An API-only host needs
AddMediator.OperationsServicetakesITrainExecutionServiceas a required constructor dependency, so a host that registersOperationsServiceitself withoutAddMediatorcannot resolve it. WhereAddTraxGraphQLexposes the operations surface, its startup validator refuses to start such a host rather than lettingqueueTrainfail at request time. IOperationsService.QueueTrainAsynccan throwUnauthorizedAccessException(TrainAuthorizationExceptionwhen the API's authorization is registered). Code calling it directly, such as a custom admin page, should handle it; it is not returned as a failedOperationResult.requeueExecutionrefuses a run with no saved input, or one whose input was stored as the truncation placeholder, rather than re-running it with an empty input.
Trax.Docs/adr/0017 records why, and
mutations lists the failure messages.
A cancelled caller can get a completed run
A train's terminal state is now saved on a token the caller cannot cancel. A train whose work
finishes after the caller cancelled (because a downstream call ignored the token) is recorded and
returned as Completed, where the caller used to get OperationCanceledException and the row
stayed InProgress. Code that treated a cancelled request as proof the work did not happen should
check the result instead. See
Cancellation Tokens;
effect/0005 in Trax.Effect records the decision.
Work queue entries are built only by WorkQueue.Create
WorkQueue's parameterless constructor is now protected, so new WorkQueue { ... } no longer
compiles. It used to compile and then fail silently: an entry built that way has a null
ConfirmedAt, which makes it a staged entry the dispatcher never claims and the stale-staged
sweep eventually cancels. Build entries with WorkQueue.Create(new CreateWorkQueue { ... }),
which stamps ConfirmedAt unless you set DeferPromotion.
WorkQueue.Create also checks CreateWorkQueue.SubjectKey now, the same way an enqueue through
the mediator does. An empty key, or one longer than WorkQueue.MaxSubjectKeyLength (512
characters), throws ArgumentException. Leave the key null when the entry should not be
serialized.
A later Trax.Effect makes WorkQueue.SubjectKey read-only, so Create is the only place a key is
set and every key passes its checks. Assigning entry.SubjectKey = ... no longer compiles; pass
the key in CreateWorkQueue.SubjectKey instead. That Trax.Effect needs Trax.Mediator 1.23.0 or
later: Trax.Mediator 1.22.x sets the key after Create, so it fails every enqueue with
MissingMethodException against it. Update the two packages together.
Records that gained parameters
These positional records gained optional trailing parameters:
| Record | Package | New parameters |
|---|---|---|
ExecutionSummary | Trax.Api | FailureClass |
ExecutionDetail | Trax.Api | FailureClass |
WorkQueueSummary | Trax.Api | ConfirmedAt, SubjectKey |
RemoteRunResponse | Trax.Scheduler | FailureClass |
Constructing one by position still compiles, because the new parameters have defaults.
Deconstructing one positionally (var (id, name, ...) = summary) does not, since the generated
Deconstruct gained the same parameters, and an assembly compiled against the old constructor
fails with MissingMethodException until rebuilt.
Rolling deploys
Two new train overrides change what a dispatcher must understand, so upgrade every host that runs the JobDispatcher before any train uses them:
DeferQueuePromotion: an older dispatcher does not checkconfirmed_atand would dispatch a staged entry before its hook returns. See Making the side-effect durable.QueueSubjectKey: an older dispatcher claims without the subject check, so two entries for one subject can run at once. See QueueSubjectKey.
Postgres migration 041 adds work_queue.confirmed_at and backfills it from created_at on every
row that can still be dispatched: queued and cancelled entries, and anything created in the last
day. Older dispatched entries are left null rather than rewritten, because work_queue keeps
every dispatched entry until metadata cleanup removes it, and rewriting them all locks and
rewrites the whole table. Nothing reads confirmed_at on a dispatched entry, but a report or
query of your own that does will see null there.