Replace BLite with Surreal embedded persistence
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m21s

This commit is contained in:
Joseph Doherty
2026-02-22 05:21:53 -05:00
parent 7ebc2cb567
commit 9c2a77dc3c
56 changed files with 6613 additions and 3177 deletions

View File

@@ -3,10 +3,13 @@ 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.BLite;
using ZB.MOM.WW.CBDDC.Persistence.Snapshot;
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
namespace ZB.MOM.WW.CBDDC.Sample.Console;
@@ -16,6 +19,8 @@ internal class Program
{
private static async Task Main(string[] args)
{
if (await TryRunMigrationAsync(args)) return;
var builder = Host.CreateApplicationBuilder(args);
// Configuration
@@ -55,11 +60,20 @@ internal class Program
// Database path
string dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data");
Directory.CreateDirectory(dataPath);
string databasePath = Path.Combine(dataPath, $"{nodeId}.blite");
string databasePath = Path.Combine(dataPath, $"{nodeId}.rocksdb");
string surrealDatabase = nodeId.Replace("-", "_", StringComparison.Ordinal);
// Register CBDDC Services using Fluent Extensions with BLite, SampleDbContext, and SampleDocumentStore
// Register CBDDC services with embedded Surreal (RocksDB).
builder.Services.AddSingleton<ICBDDCSurrealSchemaInitializer, SampleSurrealSchemaInitializer>();
builder.Services.AddSingleton<SampleDbContext>();
builder.Services.AddCBDDCCore()
.AddCBDDCBLite<SampleDbContext, SampleDocumentStore>(sp => new SampleDbContext(databasePath))
.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
@@ -73,6 +87,107 @@ internal class Program
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>
@@ -112,4 +227,4 @@ internal class Program
ConfigurationChanged?.Invoke(this, newConfig);
}
}
}
}