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
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m14s
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user