Watch a queued run
A [TraxMutation] mutation called with
mode: QUEUE returns an externalId and a workQueueId, but no metadataId: the run does not
exist yet. The run the scheduler later dispatches carries the same externalId (see
Queued runs), so a client follows it
through the lifecycle subscriptions.
Subscribe first, then queue
- Open the socket and subscribe to
onTrainStateChangedbefore sending the mutation. There is no replay, and a short train can finish before a subscription opened afterwards is registered. - Send the mutation over HTTP and keep its
externalId. Buffer any events that arrive before the response does. - Keep only events whose
externalIdmatches. The subscription was opened before theexternalIdexisted, so it cannot carry the filter, and the client filters. - Stop at the first terminal state:
COMPLETED(readoutput),FAILED(readfailureReason) orCANCELLED.
subscription {
onTrainStateChanged {
externalId
metadataId
trainState
sequence
output
failureReason
}
}mutation -> { "externalId": "66b533589bf747f1aac661a5deeea356", "workQueueId": 2 }
event -> { "externalId": "66b5...a356", "metadataId": 55, "trainState": "IN_PROGRESS", "sequence": 1 }
event -> { "externalId": "66b5...a356", "metadataId": 55, "trainState": "COMPLETED", "sequence": 2, "output": { ... } }Subscribe after queueing
When the client cannot open the subscription first, it can subscribe after the mutation returns with the externalId as the filter, and receive only that run's events:
subscription Follow($externalId: String) {
onTrainStateChanged(externalId: $externalId) {
metadataId
trainState
sequence
output
failureReason
}
}A run that starts before that subscription is registered sends its earlier events to nobody, and a short run can finish in that window. Give the wait a timeout, and read the outcome another way when it expires: operations.executions(externalId:) for an operator, the app's own data otherwise.
Check what the subscriber needs
What the subscriber needs and what it should expect:
- The train carries
[TraxBroadcast]and a posture that admits the caller (for example the same[TraxAuthorize(Roles = ...)]that lets it call the mutation), unless the caller satisfies the operations authorization. Without it the subscription is refused withTRAX_AUTHORIZATION, as Who receives what says, even on a host that exposes operations. - A failed delivery that the dispatcher requeues (
MaxDispatchAttempts) creates a new execution row with the sameexternalIdand emits no event; the client sees events only from the attempt a runner started. A run that fails at dispatch for good, its deliveries exhausted or its stored input unreadable, emitsonTrainFailedand aFAILEDonTrainStateChangedwith itsexternalIdand noIN_PROGRESSbefore it, as a train's own failure does. ItsfailureExceptionisDispatchFailedand itsfailureReasona fixed sentence pointing at the run's record, so a broadcast subscriber sees the masked reason; the cause is on the execution row, which the operations queries return. Still give the wait a timeout: a lost event, or a host that stops mid-dispatch, leaves the run to the stale-pending reaper, whose failure (failureExceptionStalePendingTimeout) is emitted the same way but only after its timeout. - A
sequencethat skips a number means events were lost. An operator can look the run up inoperations.executions; a client without that access should read the outcome from the app's own data (a query model over what the train wrote). - A queued run that fails is not retried.
A runnable C# version of this loop over System.Net.WebSockets is the GraphQLWebSocketClient in tests/Trax.Samples.ChatService.E2E/Utilities of Trax.Samples; see Testing subscriptions.