Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,10 @@ looks completed and a caller that waits out its whole budget.
earlier version of this table judged each leg by *"does it survive the owner being disposed under
it?"* — a real question, pinned by a real test. But the leg that answers "yes" to that can still
answer NOBODY when its source **completes empty**, which is a different termination reached by a
different route. The legs are now composed through `DetachedReplyOutcome.Of`, which converts value /
fault / empty into exactly one emission, so a leg cannot be silent whatever its source does:
different route. **Every leg now handles all three of Rx's terminations** — the read leg and the
inner create by their own explicit third arm, the other three by composing through
`DetachedReplyOutcome.Of`, which converts value / fault / empty into exactly one emission so the
subscriber cannot fail to answer:

| leg | trail stage | its source's empty completion |
|---|---|---|
Expand All @@ -462,6 +464,13 @@ fault / empty into exactly one emission, so a leg cannot be silent whatever its
| update — `WriteThroughStream` | `UPSERT_WRITE_THROUGH_STREAM` | posts a refusal naming the unconfirmed write. Disposal is separately covered: `RegisterOwnerDisposingNack` mints `OwnerDisposing`, the writer re-enqueues against the fresh activation, the caller is answered `success=True` (`UpsertAnswersWhenTheOwnerGoesAwayTest`) |
| create — `DispatchInnerCreate` | `UPSERT_READ absent → create` | posts a refusal (`UPSERT_CREATE_COMPLETED_EMPTY`), and an `InnerCreateVerdictBound` bounds the no-answer case |

🚨 **None of this bounds a source that NEVER terminates.** `DetachedReplyOutcome.Of` is about the
three terminations Rx *delivers*; the fourth outcome — never emits, never completes, the section
"The FOURTH outcome" above — reaches no arm at all, and no composition over the source can invent
one. A liveness bound stays the leg's own responsibility and is separately present where the
Comment on lines +469 to +470

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same finding, same fix — see the thread on DetachedReplyOutcome.cs. The doc's caveat now reads: every operator Of applies reacts to a termination and none manufactures one; Timeout manufactures one and is separately present where the source can starve (InnerCreateVerdictBound, NodeOpForwardTimeout); compose Of around that bound and both properties hold. Committed as e7ecf98a8, landing in the follow-up (this PR was already queued when the review arrived).

source can starve: `InnerCreateVerdictBound` on the inner create, `NodeOpForwardTimeout` inside the
no-op probe. Totality and liveness are different properties; this section is about the first.

### #3674: the no-op probe is the one that was actually silent, and it is a RE-INSTALL

[#3674](https://github.com/Systemorph/MeshWeaver/issues/3674) reported a
Expand Down
28 changes: 20 additions & 8 deletions src/MeshWeaver.Mesh.Contract/DetachedReplyOutcome.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@ namespace MeshWeaver.Mesh;
///
/// <para><b>Totality by construction, not by discipline.</b> Remembering to write a third arm at
/// each site is exactly the discipline that failed: <c>MeshExtensions</c> states the rule twice in
/// its own comments and still left the upsert's UPDATE leg on two arms. Composing through
/// <c>DetachedReplyOutcome.Of</c> makes the source emit <b>exactly one</b> outcome on every path, so the
/// site cannot be silent whatever the source does — and the leg becomes drivable without a mesh,
/// which is what makes the empty-completion case testable at all (a live mesh cannot arrange it on
/// demand — the same reason <c>PresentationScreenExtensions.LastKnownOnFault</c> is observable-in /
/// observable-out).</para>
/// its own comments and still left three of its upsert legs on two arms. Composing through
/// <c>DetachedReplyOutcome.Of</c> turns each of the three terminations Rx DELIVERS into exactly one
/// outcome, so the site cannot be silent when its source terminates — and the leg becomes drivable
/// without a mesh, which is what makes the empty-completion case testable at all (a live mesh
/// cannot arrange it on demand — the same reason
/// <c>PresentationScreenExtensions.LastKnownOnFault</c> is observable-in / observable-out).</para>
///
/// <para>🚨 <b>TOTALITY IS NOT LIVENESS, and this type gives only the first.</b> A source that
/// never emits AND never completes — <c>Observable.Never</c>, a request whose response is lost, the
/// starvation shape behind <see href="https://github.com/Systemorph/MeshWeaver/issues/2742">#2742</see>
/// — reaches no arm at all, and no composition OVER a source can invent a termination it never
/// produces. Read the guarantee strictly: <i>if the source terminates, exactly one outcome is
Comment on lines +32 to +33

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct — and it is a genuine self-contradiction, not just breadth: the same paragraph names Timeout two sentences later as the thing that supplies liveness.

Fixed, with the reason made explicit rather than the claim merely narrowed: every operator Of applies — Take(1), Select, Catch, DefaultIfEmpty — reacts to a termination; none manufactures one. Timeout manufactures one, which is exactly what makes it the right tool for the fourth outcome. So the guidance is now positive: compose Of around a Timeout and both properties hold — the timeout supplies liveness, Of turns whichever termination arrives into an answer.

Applied at all three sites you named (the type's remarks, the Of summary, and the doc page's caveat).

🚨 This PR was already in the merge queue when your review landed, and a queued branch cannot be updated — the same race that put these fixes in a follow-up rather than in #3915. The fix is committed as e7ecf98a8 and lands in the next PR, which I am opening as a draft so review precedes queueing and this loop stops repeating.

/// emitted.</i> A leg whose source can starve still owes its own <c>Timeout</c>, exactly as
/// <c>DispatchInnerCreate</c> (<c>InnerCreateVerdictBound</c>) and the no-op probe
/// (<c>NodeOpForwardTimeout</c>) carry one — and composing through this type neither adds nor
/// excuses that bound. See <c>Doc/Architecture/WriteVerdictTotality</c> → "The FOURTH outcome".</para>
///
/// <para>🚨 <b>Empty is NOT a value.</b> <c>DefaultIfEmpty()</c> on the source itself would emit
/// <c>default(T)</c> — <c>null</c> for a reference type — which the value arm then reads as a real
Expand Down Expand Up @@ -53,8 +63,10 @@ internal readonly record struct DetachedReplyOutcome<T>(bool HasValue, T? Value,
internal static class DetachedReplyOutcome
{
/// <summary>
/// <c>value → HasValue</c>, <c>fault → Error</c>, <c>completed empty → CompletedEmpty</c> —
/// one emission on every path, so a subscriber cannot fail to answer.
/// <c>value → HasValue</c>, <c>fault → Error</c>, <c>completed empty → CompletedEmpty</c> — one
/// emission for each of the three terminations Rx delivers, so a subscriber cannot fail to
/// answer a source that TERMINATES. It adds no deadline: a source that never terminates still
/// reaches nobody, and bounding that is the caller's own job (see the type's remarks).
///
/// <para><c>Take(1)</c> comes FIRST so the outcome is the source's own first terminal: a
/// source that emits and then faults has already answered, and its late fault must not
Expand Down
Loading