diff --git a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
index c8a1e9a..1c1c90d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/Ipc/WorkerFrameWriter.cs
@@ -172,6 +172,12 @@ public sealed class WorkerFrameWriter
// caller still observes cancellation while the frame reaches the wire (documented
// above). Rethrow from the wait rather than from the completion, so a claimed frame's
// canceller is not held behind the very write it is abandoning.
+ //
+ // The wait ending without the lock IS cancellation in every reachable case — nothing
+ // disposes _writeLock — so the tombstone's TrySetCanceled is honest. A hypothetical
+ // faulted wait would take this same path and label the frame cancelled instead of
+ // faulted; that is internal state only, since the await below rethrows the fault itself
+ // to the caller.
TombstoneIfUnclaimed(frame, cancellationToken);
await lockWait.ConfigureAwait(false);
}
@@ -343,8 +349,23 @@ public sealed class WorkerFrameWriter
/// Frame whose completion may fault without an awaiter.
private static void ObserveAbandonedFault(PendingFrame frame)
{
- _ = frame.Completion.Task.ContinueWith(
- task => _ = task.Exception,
+ ObserveFault(frame.Completion.Task);
+ }
+
+ ///
+ /// Attaches the NEXT-04 fault-observing continuation to a task this writer starts and then
+ /// discards. Every such task must carry one: with no awaiter, a fault would otherwise reach
+ /// nobody and resurface as at finalization
+ /// — a detached, unattributable failure long after the code that caused it. The continuation
+ /// runs only on the faulted path and only touches , so it can
+ /// never fault itself; it runs inline because an already-faulted task would otherwise pay a
+ /// scheduling hop to do nothing.
+ ///
+ /// Discarded task whose fault would otherwise go unobserved.
+ private static void ObserveFault(Task task)
+ {
+ _ = task.ContinueWith(
+ faulted => _ = faulted.Exception,
CancellationToken.None,
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
@@ -367,11 +388,14 @@ public sealed class WorkerFrameWriter
/// Outstanding write-lock acquisition the caller has walked away from.
private void DetachLockWait(Task lockWait)
{
- _ = lockWait.ContinueWith(
+ // The continuation task is discarded, so it takes the NEXT-04 fault observer: nothing awaits
+ // it, and a throw out of OnDetachedLockWaitSettled would otherwise be an unobserved-task
+ // exception raised at finalization rather than an attributable failure here.
+ ObserveFault(lockWait.ContinueWith(
OnDetachedLockWaitSettled,
CancellationToken.None,
TaskContinuationOptions.None,
- TaskScheduler.Default);
+ TaskScheduler.Default));
}
///
@@ -394,7 +418,11 @@ public sealed class WorkerFrameWriter
return;
}
- _ = DrainDetachedAsync();
+ // Discarded, so it takes the NEXT-04 fault observer too. DrainDetachedAsync swallows the drain
+ // itself, but its release sits in a finally outside that catch: a Release that ever throws (a
+ // SemaphoreFullException from some future double-release regression, say) must fail somewhere
+ // attributable rather than at finalization.
+ ObserveFault(DrainDetachedAsync());
}
///