Files
CBDDC/samples/ZB.MOM.WW.CBDDC.Sample.Console/Program.cs
Joseph Doherty 9c2a77dc3c
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m21s
Replace BLite with Surreal embedded persistence
2026-02-22 05:21:53 -05:00

231 lines
9.6 KiB
C#
Executable File

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Text.Json;
using ZB.MOM.WW.CBDDC.Core.Network;
using ZB.MOM.WW.CBDDC.Core.Storage;
using ZB.MOM.WW.CBDDC.Core.Sync;
using ZB.MOM.WW.CBDDC.Network;
using ZB.MOM.WW.CBDDC.Persistence.Snapshot;
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
namespace ZB.MOM.WW.CBDDC.Sample.Console;
// Local User/Address classes removed in favor of Shared project
internal class Program
{
private static async Task Main(string[] args)
{
if (await TryRunMigrationAsync(args)) return;
var builder = Host.CreateApplicationBuilder(args);
// Configuration
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true);
// Logging
builder.Logging.ClearProviders();
builder.Services.AddSerilog((_, loggerConfiguration) =>
loggerConfiguration
.MinimumLevel.Information()
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "CBDDC.Sample.Console")
.WriteTo.Console());
int randomPort = new Random().Next(1000, 9999);
// Node ID
string nodeId = args.Length > 0 ? args[0] : "node-" + randomPort;
int tcpPort = args.Length > 1 ? int.Parse(args[1]) : randomPort;
// Conflict Resolution Strategy (can be switched at runtime via service replacement)
bool useRecursiveMerge = args.Contains("--merge");
if (useRecursiveMerge) builder.Services.AddSingleton<IConflictResolver, RecursiveNodeMergeConflictResolver>();
IPeerNodeConfigurationProvider peerNodeConfigurationProvider = new StaticPeerNodeConfigurationProvider(
new PeerNodeConfiguration
{
NodeId = nodeId,
TcpPort = tcpPort,
AuthToken = "Test-Cluster-Key"
//KnownPeers = builder.Configuration.GetSection("CBDDC:KnownPeers").Get<List<KnownPeerConfiguration>>() ?? new()
});
builder.Services.AddSingleton(peerNodeConfigurationProvider);
// Database path
string dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data");
Directory.CreateDirectory(dataPath);
string databasePath = Path.Combine(dataPath, $"{nodeId}.rocksdb");
string surrealDatabase = nodeId.Replace("-", "_", StringComparison.Ordinal);
// Register CBDDC services with embedded Surreal (RocksDB).
builder.Services.AddSingleton<ICBDDCSurrealSchemaInitializer, SampleSurrealSchemaInitializer>();
builder.Services.AddSingleton<SampleDbContext>();
builder.Services.AddCBDDCCore()
.AddCBDDCSurrealEmbedded<SampleDocumentStore>(_ => new CBDDCSurrealEmbeddedOptions
{
Endpoint = "rocksdb://local",
DatabasePath = databasePath,
Namespace = "cbddc_sample",
Database = surrealDatabase
})
.AddCBDDCNetwork<StaticPeerNodeConfigurationProvider>(); // useHostedService = true by default
builder.Services.AddHostedService<ConsoleInteractiveService>(); // Runs the Input Loop
var host = builder.Build();
System.Console.WriteLine($"? Node {nodeId} initialized on port {tcpPort}");
System.Console.WriteLine($"? Database: {databasePath}");
System.Console.WriteLine();
await host.RunAsync();
}
private static async Task<bool> TryRunMigrationAsync(string[] args)
{
int migrateIndex = Array.IndexOf(args, "--migrate-snapshot");
if (migrateIndex < 0) return false;
string snapshotPath = GetRequiredArgumentValue(args, migrateIndex, "--migrate-snapshot");
if (!File.Exists(snapshotPath))
throw new FileNotFoundException("Snapshot file not found.", snapshotPath);
string targetPath = GetOptionalArgumentValue(args, "--target-db")
?? Path.Combine(Directory.GetCurrentDirectory(), "data", "migration.rocksdb");
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(targetPath))!);
string nodeId = "migration-node";
var configProvider = new StaticPeerNodeConfigurationProvider(new PeerNodeConfiguration
{
NodeId = nodeId,
TcpPort = 0,
AuthToken = "migration"
});
string databaseName = $"migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton<IPeerNodeConfigurationProvider>(configProvider);
services.AddSingleton<ICBDDCSurrealSchemaInitializer, SampleSurrealSchemaInitializer>();
services.AddSingleton<SampleDbContext>();
services.AddCBDDCCore()
.AddCBDDCSurrealEmbedded<SampleDocumentStore>(_ => new CBDDCSurrealEmbeddedOptions
{
Endpoint = "rocksdb://local",
DatabasePath = targetPath,
Namespace = "cbddc_migration",
Database = databaseName
});
using var provider = services.BuildServiceProvider();
var snapshotService = provider.GetRequiredService<ISnapshotService>();
await using (var snapshotStream = File.OpenRead(snapshotPath))
{
await snapshotService.ReplaceDatabaseAsync(snapshotStream);
}
await VerifyMigrationAsync(provider, snapshotPath);
System.Console.WriteLine($"Migration completed successfully to: {targetPath}");
return true;
}
private static async Task VerifyMigrationAsync(IServiceProvider provider, string snapshotPath)
{
await using var snapshotStream = File.OpenRead(snapshotPath);
var source = await JsonSerializer.DeserializeAsync<SnapshotDto>(snapshotStream)
?? throw new InvalidOperationException("Unable to deserialize source snapshot.");
var documentStore = provider.GetRequiredService<IDocumentStore>();
var oplogStore = provider.GetRequiredService<IOplogStore>();
var peerStore = provider.GetRequiredService<IPeerConfigurationStore>();
var confirmationStore = provider.GetService<IPeerOplogConfirmationStore>();
int destinationDocuments = (await documentStore.ExportAsync()).Count();
int destinationOplog = (await oplogStore.ExportAsync()).Count();
int destinationPeers = (await peerStore.ExportAsync()).Count();
int destinationConfirmations = confirmationStore == null
? 0
: (await confirmationStore.ExportAsync()).Count();
if (destinationDocuments != source.Documents.Count ||
destinationOplog != source.Oplog.Count ||
destinationPeers != source.RemotePeers.Count ||
destinationConfirmations != source.PeerConfirmations.Count)
throw new InvalidOperationException("Snapshot parity verification failed after migration.");
if (source.Oplog.Count > 0)
{
string firstHash = source.Oplog[0].Hash;
string lastHash = source.Oplog[^1].Hash;
var firstEntry = await oplogStore.GetEntryByHashAsync(firstHash);
var lastEntry = await oplogStore.GetEntryByHashAsync(lastHash);
if (firstEntry == null || lastEntry == null)
throw new InvalidOperationException("Oplog hash spot-check failed after migration.");
}
}
private static string GetRequiredArgumentValue(string[] args, int optionIndex, string optionName)
{
if (optionIndex < 0 || optionIndex + 1 >= args.Length || args[optionIndex + 1].StartsWith("--"))
throw new ArgumentException($"Missing value for {optionName}.");
return args[optionIndex + 1];
}
private static string? GetOptionalArgumentValue(string[] args, string optionName)
{
int index = Array.IndexOf(args, optionName);
if (index < 0) return null;
if (index + 1 >= args.Length || args[index + 1].StartsWith("--"))
throw new ArgumentException($"Missing value for {optionName}.");
return args[index + 1];
}
private class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="StaticPeerNodeConfigurationProvider" /> class.
/// </summary>
/// <param name="configuration">The initial peer node configuration.</param>
public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration)
{
Configuration = configuration;
}
/// <summary>
/// Gets or sets the current peer node configuration.
/// </summary>
public PeerNodeConfiguration Configuration { get; }
/// <summary>
/// Occurs when the peer node configuration changes.
/// </summary>
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
/// <summary>
/// Gets the current peer node configuration.
/// </summary>
/// <returns>A task that returns the current configuration.</returns>
public Task<PeerNodeConfiguration> GetConfiguration()
{
return Task.FromResult(Configuration);
}
/// <summary>
/// Raises the configuration changed event.
/// </summary>
/// <param name="newConfig">The new configuration value.</param>
protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig)
{
ConfigurationChanged?.Invoke(this, newConfig);
}
}
}