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 @@ -709,14 +709,28 @@ RequestTimeout").

| Handler | Bound | What the stalled verdict says |
|---|---|---|
| Create | `Timeout` from handler entry, over the chain up to the written row | `Unavailable`, naming the stage: `authorship-source`, `existence-read`, `partition-bootstrap`, `write-guards`, `validators`, `nodetype-resolution`, `write` |
| Create | `Timeout` from handler entry, over the chain up to the written row | `Unavailable`, naming the stage: `authorship-source`, `existence-read`, `partition-bootstrap`, `write-guards`, `validators: <validator> (i/n)` (or `validators: resolving`), `nodetype-resolution`, `write` |
| Create, after the write | the same deadline, as a VERDICT only — a post-creation handler still running is neither cancelled nor rolled back, because it may yet land its own writes | `Unavailable`: "written, a post-creation step had not finished, the outcome is unknown" — a later completion or compensation is not answered twice |
| Copy | `Timeout` from handler entry | `Unknown` with the `Copy reached no verdict:` prefix, naming the stage and, for the create leg, the TARGET paths still outstanding |
| Move | ONE move-wide deadline, `Timeout` from entry; its delete pre-flight's ladder moved one rung inside it (`NestedTimeout` stage, then leg, then absence probe) so the pre-flight can still name the silent descendant first | `Unavailable` naming the stage (`delete-preflight`, `copy`) when nothing at the source was touched; `Unknown` with "the copy had already landed" when it stalls in `delete-source`; a copy that ANSWERED a failure carries the copy's own reason and transcript, never re-derived from the wording |

Each stage is also stamped on the request's fate trail (`CREATE_STAGE …`, `COPY_STAGE …`), so a trail
that ends early names the leg that was running instead of ending at `CREATE_CHAIN_SUBSCRIBED`.

**The validators stage names the validator it is waiting on** (MeshWeaver#6391). The create runs its
`INodeValidator`s one after another (`Concat`), so `stalled at stage 'validators'` named every
registered validator at once. On memex.systemorph.com it fired ten
times in three minutes on one freshly booted pod, for System-written creates
(`Admin/_Notification/…`, `Admin/PlatformVersion/_Activity/startup-…`, `Doc/_Activity/import-…`),
and every core validator answers SYNCHRONOUSLY for a System write — so the reading could not even say
whether a validator was silent or the chain stalled before any was asked. The stage now reads
`validators: resolving` while the scoped validators are constructed (DI), then
`validators: <TypeName> (i/n)` from the moment the chain subscribes to each one, on both the stalled
verdict and the trail. A stall in `resolving` is a blocked construction, not a silent validator; a
stall on a named validator is that validator's read. `LifecycleRequestsAlwaysReachAVerdictTest`
pins it: the silent validator's type name is in the verdict, and the assertion fails on the code
before the change, which said only `validators`.

**The deadline must release the hub after a prompt create.** Rx's absolute `Timeout` schedules a
`LocalScheduler.WorkItem` that can remain in its priority queue until the deadline even after the
subscription completes and disposes. Its fallback observable therefore captures only a small
Expand Down
25 changes: 22 additions & 3 deletions src/MeshWeaver.Mesh.Contract/MeshExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,13 @@ collision is not null
return Observable.Return<(LocalizableText? Refusal, NodeCreationRejectionReason Reason)?>(
(grantRejection, NodeCreationRejectionReason.ValidationFailed));
EnterCreateStage(CreateStageValidators);
return RunCreationValidatorsObs(hub, node, capturedRequest);
// 🚨 The stage is refined per validator (#6391): "validators" alone named
// every registered validator, and the core ones all answer synchronously
// for a System write, so a stalled verdict could not say which one — or whether the
// chain stalled before ANY validator was asked (resolving them).
return RunCreationValidatorsObs(
hub, node, capturedRequest,
onStage: detail => EnterCreateStage($"{CreateStageValidators}: {detail}"));
})
.SelectMany(validationError =>
{
Expand Down Expand Up @@ -6373,8 +6379,12 @@ internal static bool GovernedActivityExecuting(
IMessageHub hub,
MeshNode node,
CreateNodeRequest request,
AccessContext? accessContext = null)
AccessContext? accessContext = null,
Action<string>? onStage = null)
{
// Named BEFORE the validators are resolved: resolving is DI construction of every scoped
// validator, and a construction that blocks would otherwise read as a silent validator.
onStage?.Invoke("resolving");
var accessService = hub.ServiceProvider.GetService<AccessService>();
var context = new NodeValidationContext
{
Expand All @@ -6392,8 +6402,17 @@ internal static bool GovernedActivityExecuting(
if (validators.Count == 0)
return Observable.Return<(LocalizableText?, NodeCreationRejectionReason)?>(null);

// Each validator is named at the moment Concat SUBSCRIBES to it (Defer), i.e. when it is
// the one the chain is waiting on — so a stalled verdict reads
// "validators: RlsNodeValidator (i/n)", never a bare stage name (#6391).
return validators
.Select(v => v.Validate(context))
.Select((v, i) => onStage is null
? v.Validate(context)
: Observable.Defer(() =>
{
onStage($"{v.GetType().Name} ({i + 1}/{validators.Count})");
return v.Validate(context);
}))
.Concat()
.Where(result => !result.IsValid)
.Select(result =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public async Task ACreateWhoseValidatorNeverAnswers_IsAnsweredUnavailable_Naming
response.Error.Should().Contain(path);
response.Error.Should().Contain("validators",
"the verdict names the stage that went silent, which is what the trail could not say");
response.Error.Should().Contain($"validators: {nameof(SilentCreationValidator)} (",
"the verdict names the VALIDATOR the chain was waiting on — 'validators' alone named "
+ "every registered validator at once (#6391)");
silent.Asked.Should().Contain(path, "the stall really was the armed validator");
(await StoredAt(path, ct)).Should().BeNull("the stall came before the write");
}
Expand Down