Retry an unlocked directory load only when the manifest moved

A missing or hash-failing page under an unchanged manifest is corruption,
not a writer mid-commit, but every InvalidDataException was retried five
times and reported as "did not settle" with the real cause buried. The
retry now also requires the manifest bytes to have changed; otherwise the
corruption error is thrown on the first attempt. A vanished page directory
is a race under the same condition.

The reload test's writer now commits under every unleased read, so it only
settles if the retry takes the lease that commit created.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-14 14:33:43 +02:00
commit 838bbfffec
2 changed files with 42 additions and 6 deletions

View file

@ -68,7 +68,8 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
reports.Clear();
// Only pages named by the manifest read under the lease are loaded; orphaned pages are never loaded. With no
// lock to lease (none created yet, or a store copied without it) a writer may commit mid-load, so an unleased
// load stands only if its manifest is still current and every page it named read back; otherwise it reloads.
// load stands only if its manifest is still current; if the manifest moved it reloads, under the lease the
// writer created. A page that fails under an unmoved manifest is corruption, not a race.
using var lease = AcquireCommitLease(wait: true, create: false);
var manifestBytes = ReadManifestBytes();
var manifest = ParseManifest(manifestBytes);
@ -89,7 +90,7 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
break;
}
}
catch (Exception exception) when (lease == null && IsTornGeneration(exception))
catch (Exception exception) when (lease == null && IsTornGeneration(exception) && !SameBytes(manifestBytes, ReadManifestBytes()))
{
if (attempt == UnleasedLoadAttempts)
throw Unsettled(exception);
@ -385,11 +386,12 @@ public sealed class DirectoryMessagePackBackingStore : CacheBackingStore
private static bool SameBytes(byte[]? first, byte[]? second) =>
first == null ? second == null : second != null && first.SequenceEqual(second);
// A page the manifest named that vanished or was replaced: a writer committed mid-load.
// A page the manifest named that vanished or was replaced. It is a writer committing mid-load only when the manifest
// moved too; under an unchanged manifest the same failure is corruption and is thrown as it is.
private static bool IsTornGeneration(Exception exception) =>
exception is AggregateException aggregate
? aggregate.Flatten().InnerExceptions.All(IsTornGeneration)
: exception is InvalidDataException or FileNotFoundException;
: exception is InvalidDataException or FileNotFoundException or DirectoryNotFoundException;
private InvalidOperationException Unsettled(Exception? last) => new(
$"Directory store {_manifestFile.FullName} changed under every one of {UnleasedLoadAttempts} unlocked loads; it did not settle.", last);