AddJunctionEvents
Publishes each step of every run (each junction as it starts and ends, each question a routing
step asks and the answer the run acts on, each track it takes) and records it in
trax.junction_run. Off unless this is called. See Junction Events.
Signature
public static TraxEffectBuilderWithData AddJunctionEvents(
this TraxEffectBuilderWithData configurationBuilder
)Defined on TraxEffectBuilderWithData, so it comes after a data provider. Called before one, it
is a compile error: Call UsePostgres(...), UseSqlite(...) or UseInMemory(...) before AddJunctionEvents().
Returns
TraxEffectBuilderWithData, for continued chaining.
Example
services.AddTrax(trax => trax
.AddEffects(effects => effects
.UsePostgres(connectionString)
.AddDecisionRecording()
.AddJunctionEvents()
.UseBroadcaster(b => b
.UseRabbitMq(rabbitMqUrl)
.UseSignalRHub(opts => opts.WithJunctionEvents()))
)
);What it registers
| Service | Lifetime | Role |
|---|---|---|
| Junction event publisher | Singleton | Publishes each step to the host's IJunctionEventHandlers and to the broadcaster's transport, when there is one |
| Junction run writer | Singleton, and a hosted service | Stores steps in trax.junction_run from a background queue of 4,096; stopping the host drains it |
An IDecisionObserver | Singleton | Turns each decision and routing into a step. Told after required observers such as decision recording's (Other decision observers). Withholding a track depends on it hearing every routing, so an IDecisionObserver registered after AddTrax, which would replace it, refuses the host's start and every run. |
Calling AddJunctionEvents() more than once registers these once.
Event types
Each step is a TrainLifecycleEventMessage whose EventType is one of these constants, with the
step in Junction (a JunctionEventPayload). TrainLifecycleEventMessage.IsJunctionEvent(eventType)
says whether an event type is one of them.
| Constant | Value | Published when |
|---|---|---|
JunctionStartedEventType | "JunctionStarted" | A junction starts |
JunctionCompletedEventType | "JunctionCompleted" | A junction returns a result |
JunctionFailedEventType | "JunctionFailed" | A junction fails |
JunctionCancelledEventType | "JunctionCancelled" | A junction is stopped by a cancellation the run was asked for |
DecidedEventType | "Decided" | A routing step's question is answered and the run acts on the answer |
DecisionRefusedEventType | "DecisionRefused" | A decider's answer the run will not act on; the routing step fails |
RoutedEventType | "Routed" | A routing step sends the run down a track |
JunctionEventPayload
// Trax.Effect.Services.TrainEventBroadcaster
public sealed record JunctionEventPayload(
int Position,
JunctionRunKind Kind,
string Name,
JunctionRunState State,
DateTime StartedAt,
DateTime? EndedAt = default,
double? DurationMs = default,
FailureClass? FailureClass = default,
string? FailureException = null,
string? QuestionKey = null,
string? Answer = null,
double? Confidence = default,
bool Replayed = false,
string? Decider = null,
bool AnswerWithheld = false,
int? Attempt = default,
bool NameWithheld = false,
int? TrackPosition = default
)
{
public const string WithheldName = "(withheld)";
}| Field | Description |
|---|---|
Position | Where the step falls in the run, from 0. A junction's start and end share it. |
Kind | Junction, Choice (Decide, Switch), Score (Scale), YesNo (Gate) or Route |
Name | The junction's class name without its namespace, or a question's key; WithheldName when NameWithheld is set |
State | InProgress, Completed, Failed or Cancelled |
StartedAt | When the junction started, or when the question was answered or the track taken (UTC) |
EndedAt, DurationMs | When the junction returned and how long it took; null for a start. A question or route has EndedAt equal to StartedAt and DurationMs 0. |
FailureClass | How a failed junction's failure is classified; null unless it failed |
FailureException | The type name of the exception a junction failed or was cancelled with, never its message |
QuestionKey | The question's key, for a question or a track; null when NameWithheld is set |
Answer | The option, score or probability of yes the run acted on; null for a junction, a refused answer and a withheld one |
Confidence | The decider's confidence, for a choice or score; null when withheld |
Replayed | True when the answer came from an earlier run. Set on a question's step only; a Route step carries false. |
Decider | The full name of the decider's type; null when NameWithheld is set. Not stored. |
AnswerWithheld | True when the question is about a type marked [TraxSensitive], and for a question or route after a withheld route |
Attempt | Which attempt of its manifest the run is, or null for a run with no manifest |
NameWithheld | True for every step (a junction, a question or a route) after a route whose answer is withheld, because which steps ran would give the track away. A question's or route's key, answer, confidence and decider are left out with the name. |
TrackPosition | For any step, the position of the latest route the run took before it, or null before any route. Every step after a route counts as on its track. A consumer that does not show a reader answers should not show these steps' names or question keys either. |
It never carries a junction's input or output, the train's input or output, a failure's message, or the state, instructions or criteria of a question. On a withheld track the number, kinds, positions and timing of steps, a failed junction's exception type and failure class, the run's own failure junction and any train started on the track stay visible; see Withholding an answer.
IJunctionEventHandler
// Trax.Effect.Services.TrainEventBroadcaster
public interface IJunctionEventHandler
{
Task HandleAsync(TrainLifecycleEventMessage message, CancellationToken ct);
}Register implementations in the container. Each message has a junction event type and a non-null
Junction. Junction events reach only these handlers, never an ITrainEventHandler.
- On the host that runs the train, a handler is called on the run's path, resolved from the run's scope, right after the step is published. The next junction waits for it, so it must return quickly and queue anything slow.
- On other hosts it is called by
TrainEventReceiverServicefor steps arriving over the transport, resolved from a fresh scope per message. A step this host published is not delivered to it twice. - Whatever it throws is logged and does not reach the run or the other handlers.
JunctionRun and ForRun
Each stored step is a JunctionRun on IDataContext.JunctionRuns, with the payload's fields
except Decider, plus Id and MetadataId. Read one run's steps, ordered by position, with
ForRun:
// Trax.Effect.Data.JunctionEvents
public static IOrderedQueryable<JunctionRun> ForRun(
this IQueryable<JunctionRun> runs,
long metadataId
)var steps = await dataContext.JunctionRuns.AsNoTracking().ForRun(metadataId).ToListAsync(ct);The API's operations.junctionRuns and the dashboard's timeline read through it too.
Remarks
- Nothing it does can fail a run or change a junction's result: a failure to store, broadcast or hand out a step is logged and swallowed.
- A step is stored by a background writer, in order and in batches, through a data context of its
own, so the run never waits on the database. A full queue drops a step, counted and logged. A
junction whose end was dropped stays
InProgressafter its run has ended, so read a row's state together with its run's. - A run's rows are deleted with its metadata row, by the foreign key's cascade.
- Only
EffectJunctions are steps. A junction skipped because an earlier one failed is not a step, and a run that is not saved (no metadata row) publishes none. - A custom
ITrainEventBroadcasteris handed every junction event too, on the run's path; it should queue rather than wait, and may route steps apart from train events. - A run of a manifest carries its attempt: 1 plus the manifest's failed runs since its last completed or cancelled one, read once when the run begins from the manifest's 1000 most recent runs (a longer streak is reported as 1001) and waited on for at most a second. A failure to read it leaves it out.
- Junction events carry
FailureJunctionas null; the run's ownFailedevent names the junction. - With RabbitMQ, steps go to their own exchange, and after a failure there are dropped untried for a
backoff of one second doubling to a minute; see
UseBroadcaster: RabbitMQ. With
SignalR, they reach clients only after
WithJunctionEvents().
Package
dotnet add package Trax.Effect.DataThe extension is in namespace Trax.Effect.Data.Extensions.