CultNetDatabase.Dispose publishes what a writer logged and had not yet published

The dispose test showed journal-first alone still lost the change: Dispose won the race against the writer's publication and cleared the stash. Dispose now drains the stash through the publisher after the journal is disposed (so nothing is stashed afterwards) and before the subject completes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 06:16:57 +02:00
commit 79f144d9ec
2 changed files with 18 additions and 3 deletions

View file

@ -1180,10 +1180,22 @@ namespace GameCult.Networking
}
_disposed = true;
// The journal first: disposing it waits for a journal in flight, so nothing is stashed after this line. What a
// writer has logged and not yet published is then published here, so a change the log holds is never dropped.
_cacheJournal.Dispose();
_cacheChanges.Dispose();
Publication[] unpublished;
lock (_logGate)
{
unpublished = _stash.OrderBy(entry => entry.Key).Select(entry => entry.Value).ToArray();
_stash.Clear();
}
foreach (var publication in unpublished)
{
Publish(publication);
}
_changes.Dispose();
}
@ -1288,6 +1300,10 @@ namespace GameCult.Networking
}
}
Publish(publication);
}
private void Publish(Publication publication) =>
PublishUntyped(
publication.DocumentType,
publication.Kind,
@ -1296,7 +1312,6 @@ namespace GameCult.Networking
publication.Shard,
publication.Document,
publication.PreviousDocument);
}
private void RecordReplicatedEntry(
CultNetShardDescriptor shard,

View file

@ -1022,8 +1022,8 @@ namespace GameCult.Networking.Tests
Assert.That(logger.Errors, Is.Empty);
}
// The journal is disposed before the observer: an admission the journal logs while the database is being disposed is
// still published, never logged and then dropped.
// A change the log holds is published, however Dispose interleaves with the write: Dispose waits for a journal in
// flight (disposing the journal first) and publishes whatever the writer had logged and not yet published.
[Test]
public async Task ADatabaseDisposedWhileAWriteIsInItsLogAppendStillPublishesWhatItLogged()
{