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

ServiceLifetimeRole
Junction event publisherSingletonPublishes each step to the host's IJunctionEventHandlers and to the broadcaster's transport, when there is one
Junction run writerSingleton, and a hosted serviceStores steps in trax.junction_run from a background queue of 4,096; stopping the host drains it
An IDecisionObserverSingletonTurns 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.

ConstantValuePublished 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)";
}
FieldDescription
PositionWhere the step falls in the run, from 0. A junction's start and end share it.
KindJunction, Choice (Decide, Switch), Score (Scale), YesNo (Gate) or Route
NameThe junction's class name without its namespace, or a question's key; WithheldName when NameWithheld is set
StateInProgress, Completed, Failed or Cancelled
StartedAtWhen the junction started, or when the question was answered or the track taken (UTC)
EndedAt, DurationMsWhen the junction returned and how long it took; null for a start. A question or route has EndedAt equal to StartedAt and DurationMs 0.
FailureClassHow a failed junction's failure is classified; null unless it failed
FailureExceptionThe type name of the exception a junction failed or was cancelled with, never its message
QuestionKeyThe question's key, for a question or a track; null when NameWithheld is set
AnswerThe option, score or probability of yes the run acted on; null for a junction, a refused answer and a withheld one
ConfidenceThe decider's confidence, for a choice or score; null when withheld
ReplayedTrue when the answer came from an earlier run. Set on a question's step only; a Route step carries false.
DeciderThe full name of the decider's type; null when NameWithheld is set. Not stored.
AnswerWithheldTrue when the question is about a type marked [TraxSensitive], and for a question or route after a withheld route
AttemptWhich attempt of its manifest the run is, or null for a run with no manifest
NameWithheldTrue 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.
TrackPositionFor 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 TrainEventReceiverService for 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 InProgress after 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 ITrainEventBroadcaster is 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 FailureJunction as null; the run's own Failed event 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.Data

The extension is in namespace Trax.Effect.Data.Extensions.