Check the run is still live before a side effect
Source JunctionMetadata.cs, EffectJunction.cs
The reaper and startup recovery can mark a slow run Failed without telling it, and a run that keeps going after that can have its work overtaken by the next run for the same subject. A junction about to do something it cannot undo (send a payment, patch a customer) re-reads its own run's row first and refuses to act when the run is no longer InProgress.
1. Make the junction an EffectJunction
EffectJunction<TIn, TOut>.Metadata is the junction's JunctionMetadata, and its TrainMetadataId is the primary key of the run's trax.metadata row. A plain Junction<TIn, TOut> has neither (EffectJunction vs Junction).
Use TrainMetadataId, not TrainExternalId: metadata.external_id has no unique index, so a dispatch retry leaves several rows sharing one external id (the earlier ones Failed), and the column is unindexed, so looking a run up by it scans the largest table Trax writes. TrainMetadataId is the same on every execution path (a direct run through ITrainExecutionService.RunAsync, a queued run picked up by a local worker, a remote or Lambda run), because each runs under the row it was dispatched with. It is 0 when no data provider is registered, since no row exists.
2. Read the state immediately before the side effect
using Microsoft.EntityFrameworkCore;
using Trax.Core.Exceptions;
using Trax.Core.Functional;
using Trax.Effect.Data.Services.DataContext;
using Trax.Effect.Enums;
using Trax.Effect.Services.EffectJunction;
public class PatchCustomerJunction(IDataContext db, ICustomerApi customers)
: EffectJunction<PatchCustomer, Unit>
{
public override async Task<Unit> Run(PatchCustomer input)
{
var state = await db.Metadatas
.Where(m => m.Id == Metadata!.TrainMetadataId)
.Select(m => m.TrainState)
.FirstOrDefaultAsync();
if (state != TrainState.InProgress)
throw new TrainException("This run was already failed; refusing to send.");
await customers.SendAsync(input);
return Unit.Default;
}
}3. Register the junction scoped or transient
Read Metadata inside Run, never in a constructor or a field initializer. EffectJunction.Metadata is assigned once per execution. A junction registered with AddSingletonTraxJunction and reached through IChain is one object shared by every concurrent run, so whichever run assigned it last is the one the others read. For this check that is wrong in the worst direction: the junction reads a live run's id, concludes its own run is still going, and sends.