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(); IPeerNodeConfigurationProvider peerNodeConfigurationProvider = new StaticPeerNodeConfigurationProvider( new PeerNodeConfiguration { NodeId = nodeId, TcpPort = tcpPort, AuthToken = "Test-Cluster-Key" //KnownPeers = builder.Configuration.GetSection("CBDDC:KnownPeers").Get>() ?? 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(); builder.Services.AddSingleton(); builder.Services.AddCBDDCCore() .AddCBDDCSurrealEmbedded(_ => new CBDDCSurrealEmbeddedOptions { Endpoint = "rocksdb://local", DatabasePath = databasePath, Namespace = "cbddc_sample", Database = surrealDatabase }) .AddCBDDCNetwork(); // useHostedService = true by default builder.Services.AddHostedService(); // 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 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(configProvider); services.AddSingleton(); services.AddSingleton(); services.AddCBDDCCore() .AddCBDDCSurrealEmbedded(_ => new CBDDCSurrealEmbeddedOptions { Endpoint = "rocksdb://local", DatabasePath = targetPath, Namespace = "cbddc_migration", Database = databaseName }); using var provider = services.BuildServiceProvider(); var snapshotService = provider.GetRequiredService(); 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(snapshotStream) ?? throw new InvalidOperationException("Unable to deserialize source snapshot."); var documentStore = provider.GetRequiredService(); var oplogStore = provider.GetRequiredService(); var peerStore = provider.GetRequiredService(); var confirmationStore = provider.GetService(); 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 { /// /// Initializes a new instance of the class. /// /// The initial peer node configuration. public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration) { Configuration = configuration; } /// /// Gets or sets the current peer node configuration. /// public PeerNodeConfiguration Configuration { get; } /// /// Occurs when the peer node configuration changes. /// public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged; /// /// Gets the current peer node configuration. /// /// A task that returns the current configuration. public Task GetConfiguration() { return Task.FromResult(Configuration); } /// /// Raises the configuration changed event. /// /// The new configuration value. protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig) { ConfigurationChanged?.Invoke(this, newConfig); } } }