Reference

ChainGraph

Package Trax.CoreNamespace Trax.Core.MonadSource ChainGraph.cs

A train's declared chain as an immutable graph: every step it declares, the types that flow through each one, every track a routing step declares, and an id for each node. It is built from DeclaredChain, so nothing runs to draw it. It is the shape a tool draws, diffs or checks in a test; ChainRecorder stays the input to ChainVerification.Verify.

Signatures

namespace Trax.Core.Monad;
 
public sealed record ChainGraph(
    string Train,
    string Input,
    string Output,
    IReadOnlyList<ChainGraphNode> Nodes,
    IReadOnlyList<string> Refusals
)
{
    public static ChainGraph From(ChainRecorder chain, Type train, Type input, Type output);
    public static string? CurrentNodeId { get; }
    public static string? CurrentBranchPath { get; }
    public string Hash { get; }
    public string ToJson();
}
 
public sealed record ChainGraphNode(
    string Id,
    ChainStepKind Kind,
    string? Junction,
    string? In,
    string? Out,
    bool Opaque,
    IReadOnlyList<ChainGraphTrack> Tracks
);
 
public sealed record ChainGraphTrack(
    string Name,
    string? Description,
    bool IsFallback,
    IReadOnlyList<ChainGraphNode> Nodes
);

Members

ChainGraph memberDescription
From(chain, train, input, output)Builds the graph of a recording from DeclaredChain(). train, input and output are the train type and its TInput and TReturn
TrainThe train's full type name. A generic train's type arguments are written by their full names, without an assembly version, so the name and the hash stay the same across builds
Input, OutputThe train's input and return types, by their readable names (List<Order>, not the CLR's backtick form)
NodesThe chain's steps, in the order it declares them
RefusalsWhat the declaration did that no step can express, as ChainRecorder.Refusals lists it
ToJson()The graph as canonical JSON. See Canonical JSON and Hash
HashSHA-256 of ToJson(), as 64 lowercase hex digits
CurrentNodeIdThe id of the node a run is executing on this async flow, or null. See CurrentNodeId
CurrentBranchPathThe path of the Parallel branch the current step runs in, such as Parallel#0/cocitation, or null outside a branch

Two graphs are equal when their names, nodes, tracks and refusals are, in the same order: two reads of an unchanged train give equal graphs.

ChainGraphNode memberDescription
IdThe node's id, unique within its graph. See Node ids
KindWhich chain primitive declared the step (ChainStepKind)
JunctionThe junction type, the decider (IDecider) for a Decide step, or null for a step that names neither
In, OutThe types the step consumes from and contributes to Memory, or null
OpaqueTrue when what runs is decided only at run time. See Opaque nodes
TracksThe tracks of a Switch, Gate or Scale step, in declared order; empty for any other step
ChainGraphTrack memberDescription
NameAn enum member's name, or Yes, No, Unsure or Otherwise
DescriptionWhat the track is for, as offered to the decider, or null
IsFallbackTrue for the Otherwise or Unsure track
NodesThe track's own steps, in declared order

Behavior

Node ids

An id names the step, not its position. It is the step's key, then # and how many earlier steps in the same chain or track share that key:

StepKeyExample id
Chain, IChain, ShortCircuitThe junction type (the interface, for IChain)SearchWeb#0
DecideThe decision it puts in MemoryDecide<ChoiceDecision<Source>>#0
Switch, Gate, ScaleThe kind and the type it routes onSwitch<Source>#0
ExtractBoth typesExtract<Int32, Int64>#0
SeedThe seeded typeSeed<IDecider>#0
ResolveResolveResolve#0

A node inside a track is prefixed with its routing step's id and the track's name, and numbered within that track: Switch<Source>#0/Papers/SearchPapers#0.

Because an id names the step, inserting a step leaves every other node's id unchanged; only a second step with the same key takes a new ordinal, and only the occurrences after it move.

A key uses a type's short name, so two different junctions (or checkpoint states) a chain names by the same short name, Billing.Fetch and Search.Fetch say, would be told apart only by their position, and swapping one for the other would change neither the ids nor the hash. The declaration refuses that and names both types; rename one. A Parallel branch whose name is refused (empty, repeated, or containing / or #) is left out of the graph, so its steps do not repeat another branch's ids.

Opaque nodes

An IChain<TJunction>() step names an interface, and the junction behind it is whatever Memory or the container holds when the run gets there. The graph cannot say what that step does, so its node is marked Opaque, with the interface as its Junction. Every other node names the type that runs.

Canonical JSON and Hash

ToJson() writes the graph with its properties in a fixed order, no insignificant whitespace, and absent values written as null rather than left out, so the same graph always gives the same text. Generic names are written as they read (<, > are not escaped). Hash is computed over that text, so two reads of an unchanged train give the same hash, and any change to a step, a type or a track gives another. Compare hashes to tell whether a train's shape changed; compare the JSON to see how.

{"id":"Switch<Source>#0","kind":"Switch","junction":null,"in":"ChoiceDecision<Source>","out":"TrackTaken<Source>","opaque":false,"tracks":[{"name":"Web","description":null,"isFallback":false,"nodes":[{"id":"Switch<Source>#0/Web/SearchWeb#0", ...}]}, ...]}

CurrentNodeId

While a run executes a junction (Chain, IChain or ShortCircuit), asks a question (Decide) or routes (Switch, Gate, Scale), CurrentNodeId holds the id the graph drew for that step. It is set for the duration of the step on the step's async flow, so anything the step calls can say which node it belongs to: the junction itself, the junction effects around an EffectJunction, or an IDecisionObserver hearing a decision or a routing. Outside a step it is null, and Extract, Seed and Resolve set nothing. A train run inside a junction sets its own ids for its own steps, and the outer id returns when the inner run does.

The run numbers its steps the same way the graph does, so a running step's id is always the id of a node in that train's graph.

Reading it from a host

A host serves the graph of each registered train through ITrainChainGraphs.Find(name), by its canonical name, reading each chain once and keeping it. The operations API draws one run on it as runGraph, matching each recorded step to its node by the nodeId junction events record; the dashboard's run page shows the same run graph.

ITrainChainGraphs.FindDeclared(name) looks a train up the same way and returns the ChainRecorder the graph was drawn from, with the train's class and its input and output types, as a DeclaredTrainChain. It is what Trax's resume check needs to decide whether a failed run can resume from a checkpoint; the scheduler's retries and the operator's resume ask through it.

Example

A check that fails when a train's shape changes, by comparing its graph to a committed copy:

using Trax.Core.Monad;
 
var chain = new OrderTrain().DeclaredChain();
var graph = ChainGraph.From(chain, typeof(OrderTrain), typeof(OrderInput), typeof(OrderResult));
 
if (graph.Refusals.Count > 0 || graph.ToJson() != File.ReadAllText("OrderTrain.chain.json").TrimEnd())
    throw new InvalidOperationException($"OrderTrain's chain changed: {graph.Hash}");

See also