Implement in-process multi-dataset sync isolation across core, network, persistence, and tests
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m14s

This commit is contained in:
Joseph Doherty
2026-02-22 11:58:34 -05:00
parent c06b56172a
commit 8e97061ab8
60 changed files with 4519 additions and 559 deletions

View File

@@ -0,0 +1,38 @@
using System.Text;
using Microsoft.Extensions.Configuration;
using ZB.MOM.WW.CBDDC.Network;
namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
public class MultiDatasetConfigParsingTests
{
[Fact]
public void MultiDatasetSection_ShouldBindRuntimeOptions()
{
const string json = """
{
"CBDDC": {
"MultiDataset": {
"EnableMultiDatasetSync": true,
"EnableDatasetPrimary": true,
"EnableDatasetLogs": true,
"EnableDatasetTimeseries": false
}
}
}
""";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
var config = new ConfigurationBuilder()
.AddJsonStream(stream)
.Build();
var options = config.GetSection("CBDDC:MultiDataset").Get<MultiDatasetRuntimeOptions>();
options.ShouldNotBeNull();
options.EnableMultiDatasetSync.ShouldBeTrue();
options.EnableDatasetPrimary.ShouldBeTrue();
options.EnableDatasetLogs.ShouldBeTrue();
options.EnableDatasetTimeseries.ShouldBeFalse();
}
}

View File

@@ -54,6 +54,68 @@ public class SurrealOplogStoreContractTests
(await store.ExportAsync()).ShouldBeEmpty();
}
/// <summary>
/// Verifies oplog reads and writes are isolated by dataset identifier.
/// </summary>
[Fact]
public async Task OplogStore_DatasetIsolation_Works()
{
await using var harness = new SurrealTestHarness();
var store = harness.CreateOplogStore();
var primaryEntry = CreateOplogEntry("Users", "p1", "node-a", 100, 0, "");
var logsEntry = CreateOplogEntry("Users", "l1", "node-a", 100, 1, "");
await store.AppendOplogEntryAsync(primaryEntry, DatasetId.Primary);
await store.AppendOplogEntryAsync(logsEntry, DatasetId.Logs);
var primary = (await store.ExportAsync(DatasetId.Primary)).ToList();
var logs = (await store.ExportAsync(DatasetId.Logs)).ToList();
primary.Count.ShouldBe(1);
primary[0].DatasetId.ShouldBe(DatasetId.Primary);
logs.Count.ShouldBe(1);
logs[0].DatasetId.ShouldBe(DatasetId.Logs);
}
/// <summary>
/// Verifies legacy oplog rows without dataset id are treated as primary dataset.
/// </summary>
[Fact]
public async Task OplogStore_LegacyRowsWithoutDatasetId_MapToPrimaryOnly()
{
await using var harness = new SurrealTestHarness();
var store = harness.CreateOplogStore();
await harness.SurrealEmbeddedClient.InitializeAsync();
await harness.SurrealEmbeddedClient.RawQueryAsync(
"""
UPSERT type::thing($table, $id) CONTENT {
collection: "Users",
key: "legacy",
operation: 0,
payloadJson: "{}",
timestampPhysicalTime: 10,
timestampLogicalCounter: 0,
timestampNodeId: "node-legacy",
hash: "legacy-hash",
previousHash: ""
};
""",
new Dictionary<string, object?>
{
["table"] = CBDDCSurrealSchemaNames.OplogEntriesTable,
["id"] = "legacy-hash"
});
var primary = (await store.GetOplogAfterAsync(new HlcTimestamp(0, 0, ""), DatasetId.Primary)).ToList();
var logs = (await store.GetOplogAfterAsync(new HlcTimestamp(0, 0, ""), DatasetId.Logs)).ToList();
primary.Any(entry => entry.Hash == "legacy-hash").ShouldBeTrue();
logs.Any(entry => entry.Hash == "legacy-hash").ShouldBeFalse();
}
private static OplogEntry CreateOplogEntry(
string collection,
string key,
@@ -110,6 +172,34 @@ public class SurrealDocumentMetadataStoreContractTests
var exported = (await store.ExportAsync()).ToList();
exported.Count.ShouldBe(3);
}
/// <summary>
/// Verifies document metadata records do not leak across datasets.
/// </summary>
[Fact]
public async Task DocumentMetadataStore_DatasetIsolation_Works()
{
await using var harness = new SurrealTestHarness();
var store = harness.CreateDocumentMetadataStore();
await store.UpsertMetadataAsync(
new DocumentMetadata("Users", "doc-shared", new HlcTimestamp(100, 0, "node-a"), false, DatasetId.Primary),
DatasetId.Primary);
await store.UpsertMetadataAsync(
new DocumentMetadata("Users", "doc-shared", new HlcTimestamp(101, 0, "node-a"), false, DatasetId.Logs),
DatasetId.Logs);
var primary = await store.GetMetadataAsync("Users", "doc-shared", DatasetId.Primary);
var logs = await store.GetMetadataAsync("Users", "doc-shared", DatasetId.Logs);
primary.ShouldNotBeNull();
primary.DatasetId.ShouldBe(DatasetId.Primary);
primary.UpdatedAt.ShouldBe(new HlcTimestamp(100, 0, "node-a"));
logs.ShouldNotBeNull();
logs.DatasetId.ShouldBe(DatasetId.Logs);
logs.UpdatedAt.ShouldBe(new HlcTimestamp(101, 0, "node-a"));
}
}
public class SurrealPeerConfigurationStoreContractTests
@@ -206,6 +296,42 @@ public class SurrealPeerOplogConfirmationStoreContractTests
afterDeactivate.All(x => x.IsActive == false).ShouldBeTrue();
}
/// <summary>
/// Verifies peer confirmations are isolated by dataset.
/// </summary>
[Fact]
public async Task PeerOplogConfirmationStore_DatasetIsolation_Works()
{
await using var harness = new SurrealTestHarness();
var store = harness.CreatePeerOplogConfirmationStore();
await store.EnsurePeerRegisteredAsync("peer-a", "10.0.0.10:5050", PeerType.StaticRemote, DatasetId.Primary);
await store.EnsurePeerRegisteredAsync("peer-a", "10.0.0.10:5050", PeerType.StaticRemote, DatasetId.Logs);
await store.UpdateConfirmationAsync(
"peer-a",
"source-1",
new HlcTimestamp(100, 1, "source-1"),
"hash-primary",
DatasetId.Primary);
await store.UpdateConfirmationAsync(
"peer-a",
"source-1",
new HlcTimestamp(200, 1, "source-1"),
"hash-logs",
DatasetId.Logs);
var primary = (await store.GetConfirmationsForPeerAsync("peer-a", DatasetId.Primary)).ToList();
var logs = (await store.GetConfirmationsForPeerAsync("peer-a", DatasetId.Logs)).ToList();
primary.Count.ShouldBe(1);
primary[0].ConfirmedHash.ShouldBe("hash-primary");
primary[0].DatasetId.ShouldBe(DatasetId.Primary);
logs.Count.ShouldBe(1);
logs[0].ConfirmedHash.ShouldBe("hash-logs");
logs[0].DatasetId.ShouldBe(DatasetId.Logs);
}
/// <summary>
/// Verifies merge semantics prefer newer confirmations and preserve active-state transitions.
/// </summary>
@@ -343,6 +469,45 @@ public class SurrealSnapshotMetadataStoreContractTests
all[0].NodeId.ShouldBe("node-a");
all[1].NodeId.ShouldBe("node-b");
}
/// <summary>
/// Verifies snapshot metadata rows are isolated by dataset.
/// </summary>
[Fact]
public async Task SnapshotMetadataStore_DatasetIsolation_Works()
{
await using var harness = new SurrealTestHarness();
var store = harness.CreateSnapshotMetadataStore();
await store.InsertSnapshotMetadataAsync(new SnapshotMetadata
{
DatasetId = DatasetId.Primary,
NodeId = "node-a",
TimestampPhysicalTime = 100,
TimestampLogicalCounter = 0,
Hash = "hash-primary"
}, DatasetId.Primary);
await store.InsertSnapshotMetadataAsync(new SnapshotMetadata
{
DatasetId = DatasetId.Logs,
NodeId = "node-a",
TimestampPhysicalTime = 200,
TimestampLogicalCounter = 0,
Hash = "hash-logs"
}, DatasetId.Logs);
var primary = await store.GetSnapshotMetadataAsync("node-a", DatasetId.Primary);
var logs = await store.GetSnapshotMetadataAsync("node-a", DatasetId.Logs);
primary.ShouldNotBeNull();
primary.Hash.ShouldBe("hash-primary");
primary.DatasetId.ShouldBe(DatasetId.Primary);
logs.ShouldNotBeNull();
logs.Hash.ShouldBe("hash-logs");
logs.DatasetId.ShouldBe(DatasetId.Logs);
}
}
internal sealed class SurrealTestHarness : IAsyncDisposable
@@ -431,6 +596,11 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
NullLogger<SurrealSnapshotMetadataStore>.Instance);
}
/// <summary>
/// Gets the embedded Surreal client used by this harness.
/// </summary>
public ICBDDCSurrealEmbeddedClient SurrealEmbeddedClient => _client;
/// <inheritdoc />
public async ValueTask DisposeAsync()
{