Add CultMesh durable shard log defaults

This commit is contained in:
Meta Vens 2026-05-19 21:13:33 +01:00
commit 8fc548e452
5 changed files with 128 additions and 4 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
@ -31,6 +32,16 @@ namespace GameCult.Mesh
/// </summary>
public CultNetDatabaseOptions? DatabaseOptions { get; set; }
/// <summary>
/// Gets or sets whether CultMesh should attach a file-backed authoritative shard-log store when none is supplied.
/// </summary>
public bool EnableDurableShardLogs { get; set; }
/// <summary>
/// Gets or sets the directory used for file-backed authoritative shard logs.
/// </summary>
public string? ShardLogPath { get; set; }
/// <summary>
/// Gets or sets the database server bridge options used for shard routing and forwarding.
/// </summary>
@ -41,18 +52,54 @@ namespace GameCult.Mesh
/// </summary>
public Action<Server>? ConfigureServer { get; set; }
internal CultNetHostOptions ToCultNetOptions()
internal CultNetHostOptions ToCultNetOptions(string cachePath)
{
return new CultNetHostOptions
{
CacheOptions = CacheOptions,
Security = Security,
StartServer = StartServer,
DatabaseOptions = DatabaseOptions,
DatabaseOptions = CreateDatabaseOptions(cachePath),
DatabaseServerOptions = DatabaseServerOptions,
ConfigureServer = ConfigureServer
};
}
private CultNetDatabaseOptions? CreateDatabaseOptions(string cachePath)
{
if (!EnableDurableShardLogs)
{
return DatabaseOptions;
}
var source = DatabaseOptions ?? new CultNetDatabaseOptions();
if (source.MutationLogStore != null)
{
return source;
}
return new CultNetDatabaseOptions
{
RuntimeId = source.RuntimeId,
Shards = source.Shards,
ClientAuthorityScopes = source.ClientAuthorityScopes,
DocumentRegistry = source.DocumentRegistry,
MutationLogStore = new CultNetFileShardMutationLogStore(ResolveShardLogPath(cachePath))
};
}
private string ResolveShardLogPath(string cachePath)
{
if (!string.IsNullOrWhiteSpace(ShardLogPath))
{
return ShardLogPath!;
}
var fullPath = Path.GetFullPath(cachePath);
var directory = Path.GetDirectoryName(fullPath) ?? Directory.GetCurrentDirectory();
var name = Path.GetFileNameWithoutExtension(fullPath);
return Path.Combine(directory, name + ".cultmesh", "shard-logs");
}
}
/// <summary>
@ -127,7 +174,7 @@ namespace GameCult.Mesh
{
var host = await CultNetLocal.CreateHostAsync(
cachePath,
(options ?? new CultMeshNodeOptions()).ToCultNetOptions()).ConfigureAwait(false);
(options ?? new CultMeshNodeOptions()).ToCultNetOptions(cachePath)).ConfigureAwait(false);
return new CultMeshNode(host);
}

View file

@ -26,6 +26,19 @@ var player = await node.Database.GetAsync<PlayerData>(playerKey);
await node.Database.PutAsync(playerKey, player);
```
Enable durable authoritative shard logs when a node should serve replica
catch-up after restart:
```csharp
using var node = await CultMesh.StartNodeAsync("world.ccmp", new CultMeshNodeOptions
{
EnableDurableShardLogs = true
});
```
If `ShardLogPath` is omitted, CultMesh stores logs beside the cache under
`world.cultmesh/shard-logs`.
## Verses
A Verse is a rule-bearing consensus graph. Aetheria Main can be a

View file

@ -21,6 +21,11 @@ Namespace: `GameCult.Mesh`
- `Database`: the distributed realtime database facade
- `DatabaseServer`: the schema-v0 database bridge
`CultMeshNodeOptions.EnableDurableShardLogs` attaches a file-backed
authoritative shard-log store when `DatabaseOptions.MutationLogStore` is not
already supplied. `ShardLogPath` can override the location; otherwise CultMesh
uses a `.cultmesh/shard-logs` directory beside the cache file.
The package home and primary entrypoint are `GameCult.Mesh` / `CultMesh`.
Some lower-level database and wire-contract types still retain `CultNet` names
because they are shared with the transport package. Treat those as plumbing,

View file

@ -67,6 +67,8 @@ Implemented:
- shard-bounded snapshots with shard id, epoch, and represented log sequence
- replica snapshot recovery after compacted history, including local shard
replacement and cursor advancement
- CultMesh node option for file-backed authoritative shard logs with a default
`.cultmesh/shard-logs` path beside the cache file
- client-authority scopes for locally predicted input documents
- predicted and reconciled database change kinds for client-side prediction
- simulation witness observations for mesh-side opinions about frame facts
@ -119,7 +121,6 @@ Not implemented yet:
Add discovery fanout and operational polish:
- expose operator-facing defaults for durable shard-log paths in CultMesh nodes
- add Verse discovery over the schema-v0 mesh lane
- add snapshot throttling and size limits for public mesh edges
- after that, add simulation-frame rollback/resimulation helpers around

View file

@ -792,6 +792,64 @@ namespace GameCult.Networking.Tests
Assert.That(incompatible.CanTransferFrom(aetheria), Is.False);
}
[Test]
public async Task CultMeshNode_CanEnable_DurableShardLogs_WithDefaultPath()
{
var rootPath = Path.Combine(TestContext.CurrentContext.WorkDirectory, "cultmesh-node", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(rootPath);
var cachePath = Path.Combine(rootPath, "world.ccmp");
var cache = new CultCache();
var schemaId = cache.Registry.GetRequired<PlayerData>().SchemaId;
var registry = new CultNetDocumentRegistry(cache.Registry)
.Register(CultNetDocumentBinding.ForDocument<PlayerData>(
cache.Registry,
payloadSerializer: SerializePlayerDataPayload,
payloadDeserializer: DeserializePlayerDataPayload));
var options = new CultMeshNodeOptions
{
StartServer = false,
EnableDurableShardLogs = true,
DatabaseOptions = new CultNetDatabaseOptions
{
DocumentRegistry = registry,
Shards =
[
new CultNetShardDescriptor(
"players-mesh-default-log",
"mesh-runtime",
epoch: 1,
isPrimary: true,
schemaIds: [schemaId],
keyPrefix: "player:")
]
}
};
var key = new CultRecordKey("player:mesh-log");
var player = new PlayerData
{
PlayerId = Guid.NewGuid(),
Email = "mesh-log@example.test",
PasswordHash = "hash",
Username = "MeshLog"
};
using (var node = await CultMesh.CreateNodeAsync(cachePath, options))
{
await node.Database.PutAsync(key, player);
}
using var restarted = await CultMesh.CreateNodeAsync(cachePath, options);
var response = restarted.DatabaseServer.CreateShardLogResponse(new CultNetShardLogRequestMessage
{
ShardId = "players-mesh-default-log"
});
Assert.That(response.Entries, Has.Length.EqualTo(1));
Assert.That(response.Entries[0].Put, Is.Not.Null);
Assert.That(response.Entries[0].Put!.Document.RecordKey, Is.EqualTo(key.Value));
Assert.That(Directory.Exists(Path.Combine(rootPath, "world.cultmesh", "shard-logs")), Is.True);
}
[Test]
public async Task CultNetDatabase_Appends_PerShardMutationLog()
{