How-to guide

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

  1. Open the socket and subscribe to onTrainStateChanged before sending the mutation. There is no replay, and a short train can finish before a subscription opened afterwards is registered.
  2. Send the mutation over HTTP and keep its externalId. Buffer any events that arrive before the response does.
  3. Keep only events whose externalId matches. The subscription was opened before the externalId existed, so it cannot carry the filter, and the client filters.
  4. Stop at the first terminal state: COMPLETED (read output), FAILED (read failureReason) or CANCELLED.
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 with TRAX_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 same externalId and 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, emits onTrainFailed and a FAILED onTrainStateChanged with its externalId and no IN_PROGRESS before it, as a train's own failure does. Its failureException is DispatchFailed and its failureReason a 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 (failureException StalePendingTimeout) is emitted the same way but only after its timeout.
  • A sequence that skips a number means events were lost. An operator can look the run up in operations.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.

SDK Reference

Subscriptions | dispatch mutations | TraxBroadcast