Repository navigation
fix(#6351): say a recursive delete that failed after removing nodes is partial and retriable #6407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
758f910
4fb5b13
a4861c4
738bd76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| using System.Reactive.Subjects; | ||
| using System.Threading.Tasks; | ||
| using MeshWeaver.Fixture; | ||
| using Xunit; | ||
|
|
||
| namespace MeshWeaver.Graph.Test; | ||
|
|
||
| /// <summary> | ||
| /// Issue #6351 - a recursive delete that fails AFTER it removed nodes has left a torn subtree, and its | ||
| /// failure must say so, and say that asking again is meaningful. | ||
| /// | ||
| /// <para>Production shape: <c>[DeleteNode] unexpected path=Marketing partial-deleted=1383</c> - the operation | ||
| /// had removed 1,383 paths when one leaf's commit watchdog fired, and the failure the caller read was the | ||
| /// leaf's own sentence with nothing about the 1,383 nodes already gone. Here the store stops serving removals | ||
| /// after 40 of 160 (the write lane is held, whoever holds it), a leaf's no-progress watchdog fires, and the | ||
| /// caller is told the subtree is partially deleted and that the delete may be retried. A retry once the lane | ||
| /// serves again removes what is left.</para> | ||
| /// </summary> | ||
| public class PartialDeleteIsReportedAsRetriableTest(ITestOutputHelper output) | ||
| : WideDeleteOnASerialisedWriteLaneTestBase(output) | ||
| { | ||
| /// <inheritdoc /> | ||
| protected override int FanOutConcurrency => 8; | ||
|
|
||
| [Fact(Timeout = 120000)] | ||
| public async Task AFailureAfterRemovingNodes_SaysTheSubtreeIsPartial_AndThatARetryIsMeaningful() | ||
| { | ||
| var rootPath = await SeedWideTree("partial-reported"); | ||
| Storage.StallAfterDeletes = 40; | ||
|
|
||
| var failure = new AsyncSubject<Exception>(); | ||
| using var deleting = NodeFactory.DeleteNode(rootPath).Subscribe( | ||
| _ => { }, | ||
| ex => | ||
| { | ||
| failure.OnNext(ex); | ||
| failure.OnCompleted(); | ||
| }); | ||
|
|
||
| var reported = await failure.Should().Within(60.Seconds()).Emit( | ||
| "a lane that stops serving removals must end the delete with a named failure", | ||
| cancellationToken: TestContext.Current.CancellationToken); | ||
|
|
||
| reported.Message.Should().Contain("made no progress", | ||
| "the failure is still the leaf's own watchdog - the cause is not rewritten"); | ||
| (reported is UnauthorizedAccessException).Should().BeFalse( | ||
| $"an availability failure must not reach the caller as a permission denial, got {reported.GetType().Name}"); | ||
| reported.Message.Should().Contain("partially deleted", | ||
| "nodes were removed before the failure, so the caller has to be told the subtree is torn"); | ||
| reported.Message.Should().Contain("retrying", | ||
| "the delete is idempotent, so the caller is told that asking again is meaningful"); | ||
|
Comment on lines
+44
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed on the scope: the two tests pin the production shape of #6351 (a cascade leaf refused as |
||
|
|
||
| var left = await Storage.Inner.ListDescendantPaths(rootPath).Should().Within(10.Seconds()).Emit(); | ||
| left.Should().NotBeEmpty("the statement is only true if something really is left"); | ||
| left.Count.Should().BeLessThan(Leaves, "the statement is only true if something really was removed"); | ||
| } | ||
|
|
||
| [Fact(Timeout = 120000)] | ||
| public async Task ARetryOnceTheLaneServesAgain_RemovesWhatTheFailedDeleteLeftBehind() | ||
| { | ||
| var rootPath = await SeedWideTree("partial-retried"); | ||
| Storage.StallAfterDeletes = 40; | ||
|
|
||
| var failure = new AsyncSubject<Exception>(); | ||
| using var deleting = NodeFactory.DeleteNode(rootPath).Subscribe( | ||
| _ => { }, | ||
| ex => | ||
| { | ||
| failure.OnNext(ex); | ||
| failure.OnCompleted(); | ||
| }); | ||
| await failure.Should().Within(60.Seconds()).Emit( | ||
| "the first delete must fail while the lane is held", | ||
| cancellationToken: TestContext.Current.CancellationToken); | ||
|
|
||
| // The lane serves again. | ||
| Storage.StallAfterDeletes = null; | ||
|
|
||
| var deleted = await NodeFactory.DeleteNode(rootPath).Should().Within(90.Seconds()).Emit( | ||
| "a retry must remove what the failed delete left behind"); | ||
| deleted.Should().BeTrue(); | ||
|
|
||
| (await Storage.Inner.ListDescendantPaths(rootPath).Should().Within(10.Seconds()).Emit()) | ||
| .Should().BeEmpty("every leaf must be gone after the retry"); | ||
| (await Storage.Inner.Exists(rootPath).Should().Within(10.Seconds()).Emit()) | ||
| .Should().BeFalse("the root must be gone after the retry"); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 738bd76:
RecursiveDeleteDrain.mdgains the section "A failure after removals says the subtree is partial, and that a retry is meaningful (#6351)". It covers the two availability arms that get the sentence, the keyed Warning in the response Log, and the rule that permission/validation refusals get no retry advice.