Initial import of the CBDDC codebase with docs and tests. Add a .NET-focused gitignore to keep generated artifacts out of source control.
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
Joseph Doherty
2026-02-20 13:03:21 -05:00
commit 08bfc17218
218 changed files with 33910 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using ZB.MOM.WW.CBDDC.Core;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
namespace ZB.MOM.WW.CBDDC.Network.Tests;
public class SyncOrchestratorConfirmationTests
{
/// <summary>
/// Verifies that merged peers are registered and the local node is skipped.
/// </summary>
[Fact]
public async Task EnsurePeersRegisteredAsync_ShouldRegisterMergedPeers_AndSkipLocalNode()
{
var oplogStore = Substitute.For<IOplogStore>();
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
var orchestrator = CreateOrchestrator(oplogStore, confirmationStore);
var now = DateTimeOffset.UtcNow;
var discoveredPeers = new List<PeerNode>
{
new("local", "127.0.0.1:9000", now, PeerType.LanDiscovered),
new("peer-a", "10.0.0.1:9000", now, PeerType.LanDiscovered)
};
var knownPeers = new List<PeerNode>
{
new("peer-a", "10.99.0.1:9000", now, PeerType.StaticRemote),
new("peer-b", "10.0.0.2:9010", now, PeerType.StaticRemote)
};
var mergedPeers = SyncOrchestrator.BuildMergedPeerList(discoveredPeers, knownPeers, "local");
mergedPeers.Count.ShouldBe(2);
await orchestrator.EnsurePeersRegisteredAsync(mergedPeers, "local", CancellationToken.None);
await confirmationStore.Received(1).EnsurePeerRegisteredAsync(
"peer-a",
"10.0.0.1:9000",
PeerType.LanDiscovered,
Arg.Any<CancellationToken>());
await confirmationStore.Received(1).EnsurePeerRegisteredAsync(
"peer-b",
"10.0.0.2:9010",
PeerType.StaticRemote,
Arg.Any<CancellationToken>());
await confirmationStore.DidNotReceive().EnsurePeerRegisteredAsync(
"local",
Arg.Any<string>(),
Arg.Any<PeerType>(),
Arg.Any<CancellationToken>());
}
/// <summary>
/// Verifies that a newly discovered node is auto-registered when peer lists are refreshed.
/// </summary>
[Fact]
public async Task EnsurePeersRegisteredAsync_WhenNewNodeJoins_ShouldAutoRegisterJoinedNode()
{
var oplogStore = Substitute.For<IOplogStore>();
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
var orchestrator = CreateOrchestrator(oplogStore, confirmationStore);
var now = DateTimeOffset.UtcNow;
var knownPeers = new List<PeerNode>
{
new("peer-static", "10.0.0.10:9000", now, PeerType.StaticRemote)
};
var firstDiscovered = new List<PeerNode>
{
new("peer-static", "10.0.0.10:9000", now, PeerType.StaticRemote)
};
var firstMerged = SyncOrchestrator.BuildMergedPeerList(firstDiscovered, knownPeers, "local");
await orchestrator.EnsurePeersRegisteredAsync(firstMerged, "local", CancellationToken.None);
var secondDiscovered = new List<PeerNode>
{
new("peer-static", "10.0.0.10:9000", now, PeerType.StaticRemote),
new("peer-new", "10.0.0.25:9010", now, PeerType.LanDiscovered)
};
var secondMerged = SyncOrchestrator.BuildMergedPeerList(secondDiscovered, knownPeers, "local");
await orchestrator.EnsurePeersRegisteredAsync(secondMerged, "local", CancellationToken.None);
await confirmationStore.Received(1).EnsurePeerRegisteredAsync(
"peer-new",
"10.0.0.25:9010",
PeerType.LanDiscovered,
Arg.Any<CancellationToken>());
}
/// <summary>
/// Verifies that confirmations advance only for nodes where remote vector-clock entries are at or ahead.
/// </summary>
[Fact]
public async Task AdvanceConfirmationsFromVectorClockAsync_ShouldAdvanceOnlyForRemoteAtOrAhead()
{
var oplogStore = Substitute.For<IOplogStore>();
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
var orchestrator = CreateOrchestrator(oplogStore, confirmationStore);
var local = new VectorClock();
local.SetTimestamp("node-equal", new HlcTimestamp(100, 1, "node-equal"));
local.SetTimestamp("node-ahead", new HlcTimestamp(200, 0, "node-ahead"));
local.SetTimestamp("node-behind", new HlcTimestamp(300, 0, "node-behind"));
local.SetTimestamp("node-local-only", new HlcTimestamp(150, 0, "node-local-only"));
var remote = new VectorClock();
remote.SetTimestamp("node-equal", new HlcTimestamp(100, 1, "node-equal"));
remote.SetTimestamp("node-ahead", new HlcTimestamp(250, 0, "node-ahead"));
remote.SetTimestamp("node-behind", new HlcTimestamp(299, 9, "node-behind"));
remote.SetTimestamp("node-remote-only", new HlcTimestamp(900, 0, "node-remote-only"));
oplogStore.GetLastEntryHashAsync("node-equal", Arg.Any<CancellationToken>())
.Returns("hash-equal");
oplogStore.GetLastEntryHashAsync("node-ahead", Arg.Any<CancellationToken>())
.Returns((string?)null);
await orchestrator.AdvanceConfirmationsFromVectorClockAsync("peer-1", local, remote, CancellationToken.None);
await confirmationStore.Received(1).UpdateConfirmationAsync(
"peer-1",
"node-equal",
new HlcTimestamp(100, 1, "node-equal"),
"hash-equal",
Arg.Any<CancellationToken>());
await confirmationStore.Received(1).UpdateConfirmationAsync(
"peer-1",
"node-ahead",
new HlcTimestamp(200, 0, "node-ahead"),
string.Empty,
Arg.Any<CancellationToken>());
await confirmationStore.DidNotReceive().UpdateConfirmationAsync(
"peer-1",
"node-behind",
Arg.Any<HlcTimestamp>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
await confirmationStore.DidNotReceive().UpdateConfirmationAsync(
"peer-1",
"node-local-only",
Arg.Any<HlcTimestamp>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
await confirmationStore.DidNotReceive().UpdateConfirmationAsync(
"peer-1",
"node-remote-only",
Arg.Any<HlcTimestamp>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
}
/// <summary>
/// Verifies that pushed-batch confirmation uses the maximum timestamp and its matching hash.
/// </summary>
[Fact]
public async Task AdvanceConfirmationForPushedBatchAsync_ShouldUseMaxTimestampAndHash()
{
var oplogStore = Substitute.For<IOplogStore>();
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
var orchestrator = CreateOrchestrator(oplogStore, confirmationStore);
var pushedChanges = new List<OplogEntry>
{
CreateEntry("source-1", 100, 0, "hash-100"),
CreateEntry("source-1", 120, 1, "hash-120"),
CreateEntry("source-1", 110, 5, "hash-110")
};
await orchestrator.AdvanceConfirmationForPushedBatchAsync("peer-1", "source-1", pushedChanges, CancellationToken.None);
await confirmationStore.Received(1).UpdateConfirmationAsync(
"peer-1",
"source-1",
new HlcTimestamp(120, 1, "source-1"),
"hash-120",
Arg.Any<CancellationToken>());
}
/// <summary>
/// Verifies that no confirmation update occurs when a pushed batch is empty.
/// </summary>
[Fact]
public async Task AdvanceConfirmationForPushedBatchAsync_ShouldSkipEmptyBatch()
{
var oplogStore = Substitute.For<IOplogStore>();
var confirmationStore = Substitute.For<IPeerOplogConfirmationStore>();
var orchestrator = CreateOrchestrator(oplogStore, confirmationStore);
await orchestrator.AdvanceConfirmationForPushedBatchAsync(
"peer-1",
"source-1",
Array.Empty<OplogEntry>(),
CancellationToken.None);
await confirmationStore.DidNotReceive().UpdateConfirmationAsync(
Arg.Any<string>(),
Arg.Any<string>(),
Arg.Any<HlcTimestamp>(),
Arg.Any<string>(),
Arg.Any<CancellationToken>());
}
private static SyncOrchestrator CreateOrchestrator(IOplogStore oplogStore, IPeerOplogConfirmationStore confirmationStore)
{
var discovery = Substitute.For<IDiscoveryService>();
discovery.GetActivePeers().Returns(Array.Empty<PeerNode>());
var documentStore = Substitute.For<IDocumentStore>();
documentStore.InterestedCollection.Returns(Array.Empty<string>());
var snapshotMetadataStore = Substitute.For<ISnapshotMetadataStore>();
var snapshotService = Substitute.For<ISnapshotService>();
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration { NodeId = "local" });
return new SyncOrchestrator(
discovery,
oplogStore,
documentStore,
snapshotMetadataStore,
snapshotService,
configProvider,
NullLoggerFactory.Instance,
confirmationStore);
}
private static OplogEntry CreateEntry(string nodeId, long wall, int logic, string hash)
{
return new OplogEntry(
"users",
$"{nodeId}-{wall}-{logic}",
OperationType.Put,
payload: null,
timestamp: new HlcTimestamp(wall, logic, nodeId),
previousHash: string.Empty,
hash: hash);
}
}