Soft flush: pin the host and node passthroughs with a held record (absent writes, readable skips, unreadable refuses); XML-doc the one meaning on the three FlushAsync(bool soft) overloads
Some checks failed
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
Some checks failed
CultMesh portability and performance / ubuntu-latest reactive semantics and leases (pull_request) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
80eed16c0d
commit
8df0c4b25a
4 changed files with 77 additions and 2 deletions
|
|
@ -2547,6 +2547,11 @@ namespace GameCult.Caching
|
|||
return true;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Flushes every dirty backing store. With <paramref name="soft"/> the flush has one meaning, decided by each store's
|
||||
/// own read: a store with nothing at its path is written; a store that reads (empty included) is skipped and its
|
||||
/// staged writes stay staged; a store that cannot be read refuses with <see cref="CultStoreUnreadableException"/>.
|
||||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
FlushAllBackingStores(soft);
|
||||
|
|
|
|||
|
|
@ -428,7 +428,10 @@ namespace GameCult.Mesh
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the durable cache.
|
||||
/// Flushes the durable cache. With <paramref name="soft"/> the flush has one meaning, decided by the store's own
|
||||
/// read and never by what the cache holds in memory: a store with nothing at its path is written; a store that reads
|
||||
/// (empty included) is skipped and its staged writes stay staged; a store that cannot be read refuses with
|
||||
/// <see cref="CultStoreUnreadableException"/> and is left untouched.
|
||||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@ namespace GameCult.Networking
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the durable cache.
|
||||
/// Flushes the durable cache. With <paramref name="soft"/> the flush has one meaning, decided by the store's own
|
||||
/// read and never by what the cache holds in memory: a store with nothing at its path is written; a store that reads
|
||||
/// (empty included) is skipped and its staged writes stay staged; a store that cannot be read refuses with
|
||||
/// <see cref="CultStoreUnreadableException"/> and is left untouched.
|
||||
/// </summary>
|
||||
public Task FlushAsync(bool soft = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7490,6 +7490,70 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(File.ReadAllBytes(cachePath), Is.Not.EqualTo(before), "a plain node flush after a skipped soft flush did not land the staged write");
|
||||
}
|
||||
|
||||
// The cache HOLDS a record in every step: a passthrough that decides soft from the cache's memory instead of
|
||||
// handing the store the flag fails the absent step (it skips and writes nothing) or the unreadable step (it
|
||||
// never reaches the reader that refuses).
|
||||
[Test]
|
||||
public async Task CultNetLocal_Host_SoftFlush_With_A_Held_Record_Writes_Absent_Skips_Readable_Refuses_Unreadable()
|
||||
{
|
||||
var filePath = Path.Combine(Path.GetTempPath(), $"cultnet-host-soft-held-{Guid.NewGuid():N}.msgpack");
|
||||
CultNetHost? host = null;
|
||||
try
|
||||
{
|
||||
host = await CultNetLocal.CreateHostAsync(filePath, new CultNetHostOptions { StartServer = false });
|
||||
await host.Cache.UpsertAsync(new PlayerData { PlayerId = Guid.NewGuid(), Email = "a@example.test", PasswordHash = "hash", Username = "First" });
|
||||
Assert.That(File.Exists(filePath), Is.False);
|
||||
|
||||
await host.FlushAsync(soft: true);
|
||||
Assert.That(File.Exists(filePath), Is.True, "a soft host flush over an absent store, with a record held, wrote nothing");
|
||||
var written = File.ReadAllBytes(filePath);
|
||||
|
||||
await host.Cache.UpsertAsync(new PlayerData { PlayerId = Guid.NewGuid(), Email = "b@example.test", PasswordHash = "hash", Username = "Second" });
|
||||
await host.FlushAsync(soft: true);
|
||||
Assert.That(File.ReadAllBytes(filePath), Is.EqualTo(written), "a soft host flush rewrote a readable store");
|
||||
|
||||
var junk = new byte[] { 0xC1, 0x01, 0x02, 0x03 };
|
||||
File.WriteAllBytes(filePath, junk);
|
||||
Assert.That(async () => await host.FlushAsync(soft: true), Throws.InstanceOf<CultStoreUnreadableException>());
|
||||
Assert.That(File.ReadAllBytes(filePath), Is.EqualTo(junk));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host?.Dispose();
|
||||
if (File.Exists(filePath)) File.Delete(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultMeshNode_SoftFlush_With_A_Held_Record_Writes_Absent_Skips_Readable_Refuses_Unreadable()
|
||||
{
|
||||
var rootPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "cultmesh-soft-held", Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(rootPath);
|
||||
var cachePath = Path.Combine(rootPath, "world.ccmp");
|
||||
var documents = new CultNetDocumentRegistry()
|
||||
.Register(CultNetDocumentBinding.ForDocument<MeshQuickstartNote>());
|
||||
using var node = await CultMesh.CreateNodeAsync(cachePath, new CultMeshNodeOptions
|
||||
{
|
||||
StartServer = false,
|
||||
DatabaseOptions = new CultNetDatabaseOptions { RuntimeId = "soft-held-local", DocumentRegistry = documents }
|
||||
});
|
||||
await node.Database.PutAsync(new CultRecordKey("note:one"), new MeshQuickstartNote { NoteId = "note:one", Body = "one" });
|
||||
Assert.That(File.Exists(cachePath), Is.False);
|
||||
|
||||
await node.FlushAsync(soft: true);
|
||||
Assert.That(File.Exists(cachePath), Is.True, "a soft node flush over an absent store, with a record held, wrote nothing");
|
||||
var written = File.ReadAllBytes(cachePath);
|
||||
|
||||
await node.Database.PutAsync(new CultRecordKey("note:two"), new MeshQuickstartNote { NoteId = "note:two", Body = "two" });
|
||||
await node.FlushAsync(soft: true);
|
||||
Assert.That(File.ReadAllBytes(cachePath), Is.EqualTo(written), "a soft node flush rewrote a readable store");
|
||||
|
||||
var junk = new byte[] { 0xC1, 0x01, 0x02, 0x03 };
|
||||
File.WriteAllBytes(cachePath, junk);
|
||||
Assert.That(async () => await node.FlushAsync(soft: true), Throws.InstanceOf<CultStoreUnreadableException>());
|
||||
Assert.That(File.ReadAllBytes(cachePath), Is.EqualTo(junk));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultNetLocal_CreateHostAsync_Wires_Durable_Cache_Without_Starting_Server()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue