265 lines
11 KiB
C#
Executable File
265 lines
11 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;
|
|
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);
|
|
var multiDatasetOptions = builder.Configuration
|
|
.GetSection("CBDDC:MultiDataset")
|
|
.Get<MultiDatasetRuntimeOptions>()
|
|
?? new MultiDatasetRuntimeOptions
|
|
{
|
|
EnableMultiDatasetSync = true,
|
|
EnableDatasetPrimary = true,
|
|
EnableDatasetLogs = true,
|
|
EnableDatasetTimeseries = true
|
|
};
|
|
|
|
// 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
|
|
})
|
|
.AddCBDDCSurrealEmbeddedDataset(DatasetId.Primary, options =>
|
|
{
|
|
options.InterestingCollections = ["Users", "TodoLists"];
|
|
})
|
|
.AddCBDDCSurrealEmbeddedDataset(DatasetId.Logs, options =>
|
|
{
|
|
options.InterestingCollections = ["Logs"];
|
|
})
|
|
.AddCBDDCSurrealEmbeddedDataset(DatasetId.Timeseries, options =>
|
|
{
|
|
options.InterestingCollections = ["Timeseries"];
|
|
})
|
|
.AddCBDDCNetwork<StaticPeerNodeConfigurationProvider>(); // useHostedService = true by default
|
|
|
|
if (multiDatasetOptions.EnableMultiDatasetSync)
|
|
builder.Services.AddCBDDCMultiDataset(options =>
|
|
{
|
|
options.EnableMultiDatasetSync = multiDatasetOptions.EnableMultiDatasetSync;
|
|
options.EnableDatasetPrimary = multiDatasetOptions.EnableDatasetPrimary;
|
|
options.EnableDatasetLogs = multiDatasetOptions.EnableDatasetLogs;
|
|
options.EnableDatasetTimeseries = multiDatasetOptions.EnableDatasetTimeseries;
|
|
options.AdditionalDatasets = multiDatasetOptions.AdditionalDatasets.ToList();
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|