Reformat/cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m10s
This commit is contained in:
42
CBDDC.slnx
42
CBDDC.slnx
@@ -1,23 +1,23 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Configurations>
|
<Configurations>
|
||||||
<Platform Name="Any CPU" />
|
<Platform Name="Any CPU"/>
|
||||||
<Platform Name="x64" />
|
<Platform Name="x64"/>
|
||||||
<Platform Name="x86" />
|
<Platform Name="x86"/>
|
||||||
</Configurations>
|
</Configurations>
|
||||||
<Folder Name="/samples/">
|
<Folder Name="/samples/">
|
||||||
<Project Path="samples/ZB.MOM.WW.CBDDC.Sample.Console/ZB.MOM.WW.CBDDC.Sample.Console.csproj" />
|
<Project Path="samples/ZB.MOM.WW.CBDDC.Sample.Console/ZB.MOM.WW.CBDDC.Sample.Console.csproj"/>
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/ZB.MOM.WW.CBDDC.Hosting/ZB.MOM.WW.CBDDC.Hosting.csproj" />
|
<Project Path="src/ZB.MOM.WW.CBDDC.Hosting/ZB.MOM.WW.CBDDC.Hosting.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.CBDDC.Core/ZB.MOM.WW.CBDDC.Core.csproj" />
|
<Project Path="src/ZB.MOM.WW.CBDDC.Core/ZB.MOM.WW.CBDDC.Core.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.CBDDC.Network/ZB.MOM.WW.CBDDC.Network.csproj" />
|
<Project Path="src/ZB.MOM.WW.CBDDC.Network/ZB.MOM.WW.CBDDC.Network.csproj"/>
|
||||||
<Project Path="src/ZB.MOM.WW.CBDDC.Persistence/ZB.MOM.WW.CBDDC.Persistence.csproj" />
|
<Project Path="src/ZB.MOM.WW.CBDDC.Persistence/ZB.MOM.WW.CBDDC.Persistence.csproj"/>
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/ZB.MOM.WW.CBDDC.Core.Tests/ZB.MOM.WW.CBDDC.Core.Tests.csproj" />
|
<Project Path="tests/ZB.MOM.WW.CBDDC.Core.Tests/ZB.MOM.WW.CBDDC.Core.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.CBDDC.E2E.Tests/ZB.MOM.WW.CBDDC.E2E.Tests.csproj" />
|
<Project Path="tests/ZB.MOM.WW.CBDDC.E2E.Tests/ZB.MOM.WW.CBDDC.E2E.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.CBDDC.Hosting.Tests/ZB.MOM.WW.CBDDC.Hosting.Tests.csproj" />
|
<Project Path="tests/ZB.MOM.WW.CBDDC.Hosting.Tests/ZB.MOM.WW.CBDDC.Hosting.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.CBDDC.Network.Tests/ZB.MOM.WW.CBDDC.Network.Tests.csproj" />
|
<Project Path="tests/ZB.MOM.WW.CBDDC.Network.Tests/ZB.MOM.WW.CBDDC.Network.Tests.csproj"/>
|
||||||
<Project Path="tests/ZB.MOM.WW.CBDDC.Sample.Console.Tests/ZB.MOM.WW.CBDDC.Sample.Console.Tests.csproj" />
|
<Project Path="tests/ZB.MOM.WW.CBDDC.Sample.Console.Tests/ZB.MOM.WW.CBDDC.Sample.Console.Tests.csproj"/>
|
||||||
</Folder>
|
</Folder>
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
||||||
<AnalysisLevel>latest</AnalysisLevel>
|
<AnalysisLevel>latest</AnalysisLevel>
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,37 +1,33 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using Serilog.Context;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Cache;
|
using ZB.MOM.WW.CBDDC.Core.Cache;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
using ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network;
|
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
|
||||||
using Microsoft.Extensions.DependencyInjection; // For IServiceProvider if needed
|
|
||||||
using Serilog.Context;
|
|
||||||
using ZB.MOM.WW.CBDDC.Sample.Console;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
// For IServiceProvider if needed
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
|
|
||||||
public class ConsoleInteractiveService : BackgroundService
|
public class ConsoleInteractiveService : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConsoleInteractiveService> _logger;
|
|
||||||
private readonly SampleDbContext _db;
|
|
||||||
private readonly ICBDDCNode _node;
|
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
|
||||||
|
|
||||||
|
|
||||||
// Auxiliary services for status/commands
|
// Auxiliary services for status/commands
|
||||||
private readonly IDocumentCache _cache;
|
private readonly IDocumentCache _cache;
|
||||||
private readonly IOfflineQueue _queue;
|
|
||||||
private readonly ICBDDCHealthCheck _healthCheck;
|
|
||||||
private readonly ISyncStatusTracker _syncTracker;
|
|
||||||
private readonly IServiceProvider _serviceProvider;
|
|
||||||
private readonly IPeerNodeConfigurationProvider _configProvider;
|
private readonly IPeerNodeConfigurationProvider _configProvider;
|
||||||
|
private readonly SampleDbContext _db;
|
||||||
|
private readonly ICBDDCHealthCheck _healthCheck;
|
||||||
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
|
private readonly ILogger<ConsoleInteractiveService> _logger;
|
||||||
|
private readonly ICBDDCNode _node;
|
||||||
|
private readonly IOfflineQueue _queue;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ISyncStatusTracker _syncTracker;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ConsoleInteractiveService"/> class.
|
/// Initializes a new instance of the <see cref="ConsoleInteractiveService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger used by the interactive service.</param>
|
/// <param name="logger">The logger used by the interactive service.</param>
|
||||||
/// <param name="db">The sample database context.</param>
|
/// <param name="db">The sample database context.</param>
|
||||||
@@ -72,7 +68,7 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
{
|
{
|
||||||
var config = await _configProvider.GetConfiguration();
|
var config = await _configProvider.GetConfiguration();
|
||||||
|
|
||||||
System.Console.WriteLine($"--- Interactive Console ---");
|
System.Console.WriteLine("--- Interactive Console ---");
|
||||||
System.Console.WriteLine($"Node ID: {config.NodeId}");
|
System.Console.WriteLine($"Node ID: {config.NodeId}");
|
||||||
PrintHelp();
|
PrintHelp();
|
||||||
|
|
||||||
@@ -85,7 +81,7 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var input = System.Console.ReadLine();
|
string? input = System.Console.ReadLine();
|
||||||
if (string.IsNullOrEmpty(input)) continue;
|
if (string.IsNullOrEmpty(input)) continue;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -118,42 +114,53 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
System.Console.WriteLine(" [n]ew (auto), [s]pam (5x), [c]ount, [t]odos");
|
System.Console.WriteLine(" [n]ew (auto), [s]pam (5x), [c]ount, [t]odos");
|
||||||
System.Console.WriteLine(" [h]ealth, cac[h]e");
|
System.Console.WriteLine(" [h]ealth, cac[h]e");
|
||||||
System.Console.WriteLine(" [r]esolver [lww|merge], [demo] conflict");
|
System.Console.WriteLine(" [r]esolver [lww|merge], [demo] conflict");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleInput(string input)
|
private async Task HandleInput(string input)
|
||||||
{
|
{
|
||||||
var config = await _configProvider.GetConfiguration();
|
var config = await _configProvider.GetConfiguration();
|
||||||
if (input.StartsWith("n"))
|
if (input.StartsWith("n"))
|
||||||
{
|
{
|
||||||
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
var user = new User { Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90), Address = new Address { City = "AutoCity" } };
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90),
|
||||||
|
Address = new Address { City = "AutoCity" }
|
||||||
|
};
|
||||||
await _db.Users.InsertAsync(user);
|
await _db.Users.InsertAsync(user);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
System.Console.WriteLine($"[+] Created {user.Name} with Id: {user.Id}...");
|
System.Console.WriteLine($"[+] Created {user.Name} with Id: {user.Id}...");
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("s"))
|
else if (input.StartsWith("s"))
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 5; i++)
|
for (var i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
var user = new User { Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90), Address = new Address { City = "SpamCity" } };
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90),
|
||||||
|
Address = new Address { City = "SpamCity" }
|
||||||
|
};
|
||||||
await _db.Users.InsertAsync(user);
|
await _db.Users.InsertAsync(user);
|
||||||
System.Console.WriteLine($"[+] Created {user.Name} with Id: {user.Id}...");
|
System.Console.WriteLine($"[+] Created {user.Name} with Id: {user.Id}...");
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("c"))
|
else if (input.StartsWith("c"))
|
||||||
{
|
{
|
||||||
var userCount = _db.Users.FindAll().Count();
|
int userCount = _db.Users.FindAll().Count();
|
||||||
var todoCount = _db.TodoLists.FindAll().Count();
|
int todoCount = _db.TodoLists.FindAll().Count();
|
||||||
System.Console.WriteLine($"Collection 'Users': {userCount} documents");
|
System.Console.WriteLine($"Collection 'Users': {userCount} documents");
|
||||||
System.Console.WriteLine($"Collection 'TodoLists': {todoCount} documents");
|
System.Console.WriteLine($"Collection 'TodoLists': {todoCount} documents");
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("p"))
|
else if (input.StartsWith("p"))
|
||||||
{
|
{
|
||||||
var alice = new User { Id = Guid.NewGuid().ToString(), Name = "Alice", Age = 30, Address = new Address { City = "Paris" } };
|
var alice = new User
|
||||||
var bob = new User { Id = Guid.NewGuid().ToString(), Name = "Bob", Age = 25, Address = new Address { City = "Rome" } };
|
{ Id = Guid.NewGuid().ToString(), Name = "Alice", Age = 30, Address = new Address { City = "Paris" } };
|
||||||
|
var bob = new User
|
||||||
|
{ Id = Guid.NewGuid().ToString(), Name = "Bob", Age = 25, Address = new Address { City = "Rome" } };
|
||||||
await _db.Users.InsertAsync(alice);
|
await _db.Users.InsertAsync(alice);
|
||||||
await _db.Users.InsertAsync(bob);
|
await _db.Users.InsertAsync(bob);
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
@@ -162,17 +169,19 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
else if (input.StartsWith("g"))
|
else if (input.StartsWith("g"))
|
||||||
{
|
{
|
||||||
System.Console.Write("Enter user Id: ");
|
System.Console.Write("Enter user Id: ");
|
||||||
var id = System.Console.ReadLine();
|
string? id = System.Console.ReadLine();
|
||||||
if (!string.IsNullOrEmpty(id))
|
if (!string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
var u = _db.Users.FindById(id);
|
var u = _db.Users.FindById(id);
|
||||||
System.Console.WriteLine(u != null ? $"Got: {u.Name}, Age {u.Age}, City: {u.Address?.City}" : "Not found");
|
System.Console.WriteLine(u != null
|
||||||
|
? $"Got: {u.Name}, Age {u.Age}, City: {u.Address?.City}"
|
||||||
|
: "Not found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("d"))
|
else if (input.StartsWith("d"))
|
||||||
{
|
{
|
||||||
System.Console.Write("Enter user Id to delete: ");
|
System.Console.Write("Enter user Id to delete: ");
|
||||||
var id = System.Console.ReadLine();
|
string? id = System.Console.ReadLine();
|
||||||
if (!string.IsNullOrEmpty(id))
|
if (!string.IsNullOrEmpty(id))
|
||||||
{
|
{
|
||||||
await _db.Users.DeleteAsync(id);
|
await _db.Users.DeleteAsync(id);
|
||||||
@@ -183,8 +192,8 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
else if (input.StartsWith("l"))
|
else if (input.StartsWith("l"))
|
||||||
{
|
{
|
||||||
var peers = _node.Discovery.GetActivePeers();
|
var peers = _node.Discovery.GetActivePeers();
|
||||||
var handshakeSvc = _serviceProvider.GetService<ZB.MOM.WW.CBDDC.Network.Security.IPeerHandshakeService>();
|
var handshakeSvc = _serviceProvider.GetService<IPeerHandshakeService>();
|
||||||
var secureIcon = handshakeSvc != null ? "🔒" : "🔓";
|
string secureIcon = handshakeSvc != null ? "🔒" : "🔓";
|
||||||
|
|
||||||
System.Console.WriteLine($"Active Peers ({secureIcon}):");
|
System.Console.WriteLine($"Active Peers ({secureIcon}):");
|
||||||
foreach (var p in peers)
|
foreach (var p in peers)
|
||||||
@@ -203,7 +212,7 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
{
|
{
|
||||||
var health = await _healthCheck.CheckAsync();
|
var health = await _healthCheck.CheckAsync();
|
||||||
var syncStatus = _syncTracker.GetStatus();
|
var syncStatus = _syncTracker.GetStatus();
|
||||||
var handshakeSvc = _serviceProvider.GetService<ZB.MOM.WW.CBDDC.Network.Security.IPeerHandshakeService>();
|
var handshakeSvc = _serviceProvider.GetService<IPeerHandshakeService>();
|
||||||
|
|
||||||
System.Console.WriteLine("=== Health Check ===");
|
System.Console.WriteLine("=== Health Check ===");
|
||||||
System.Console.WriteLine($"Database: {(health.DatabaseHealthy ? "✓" : "✗")}");
|
System.Console.WriteLine($"Database: {(health.DatabaseHealthy ? "✓" : "✗")}");
|
||||||
@@ -216,17 +225,18 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
if (health.Errors.Any())
|
if (health.Errors.Any())
|
||||||
{
|
{
|
||||||
System.Console.WriteLine("Errors:");
|
System.Console.WriteLine("Errors:");
|
||||||
foreach (var err in health.Errors.Take(3)) System.Console.WriteLine($" - {err}");
|
foreach (string err in health.Errors.Take(3)) System.Console.WriteLine($" - {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("ch") || input == "cache")
|
else if (input.StartsWith("ch") || input == "cache")
|
||||||
{
|
{
|
||||||
var stats = _cache.GetStatistics();
|
var stats = _cache.GetStatistics();
|
||||||
System.Console.WriteLine($"=== Cache Stats ===\nSize: {stats.Size}\nHits: {stats.Hits}\nMisses: {stats.Misses}\nRate: {stats.HitRate:P1}");
|
System.Console.WriteLine(
|
||||||
|
$"=== Cache Stats ===\nSize: {stats.Size}\nHits: {stats.Hits}\nMisses: {stats.Misses}\nRate: {stats.HitRate:P1}");
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("r") && input.Contains("resolver"))
|
else if (input.StartsWith("r") && input.Contains("resolver"))
|
||||||
{
|
{
|
||||||
var parts = input.Split(' ');
|
string[] parts = input.Split(' ');
|
||||||
if (parts.Length > 1)
|
if (parts.Length > 1)
|
||||||
{
|
{
|
||||||
var newResolver = parts[1].ToLower() switch
|
var newResolver = parts[1].ToLower() switch
|
||||||
@@ -240,7 +250,7 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
{
|
{
|
||||||
// Note: Requires restart to fully apply. For demo, we inform user.
|
// Note: Requires restart to fully apply. For demo, we inform user.
|
||||||
System.Console.WriteLine($"⚠️ Resolver changed to {parts[1].ToUpper()}. Restart node to apply.");
|
System.Console.WriteLine($"⚠️ Resolver changed to {parts[1].ToUpper()}. Restart node to apply.");
|
||||||
System.Console.WriteLine($" (Current session continues with previous resolver)");
|
System.Console.WriteLine(" (Current session continues with previous resolver)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -262,7 +272,7 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
System.Console.WriteLine($"📋 {list.Name} ({list.Items.Count} items)");
|
System.Console.WriteLine($"📋 {list.Name} ({list.Items.Count} items)");
|
||||||
foreach (var item in list.Items)
|
foreach (var item in list.Items)
|
||||||
{
|
{
|
||||||
var status = item.Completed ? "✓" : " ";
|
string status = item.Completed ? "✓" : " ";
|
||||||
System.Console.WriteLine($" [{status}] {item.Task}");
|
System.Console.WriteLine($" [{status}] {item.Task}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,8 +291,8 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
Name = "Shopping List",
|
Name = "Shopping List",
|
||||||
Items = new List<TodoItem>
|
Items = new List<TodoItem>
|
||||||
{
|
{
|
||||||
new TodoItem { Task = "Buy milk", Completed = false },
|
new() { Task = "Buy milk", Completed = false },
|
||||||
new TodoItem { Task = "Buy bread", Completed = false }
|
new() { Task = "Buy bread", Completed = false }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,24 +335,20 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
System.Console.WriteLine($" List: {merged.Name}");
|
System.Console.WriteLine($" List: {merged.Name}");
|
||||||
foreach (var item in merged.Items)
|
foreach (var item in merged.Items)
|
||||||
{
|
{
|
||||||
var status = item.Completed ? "✓" : " ";
|
string status = item.Completed ? "✓" : " ";
|
||||||
System.Console.WriteLine($" [{status}] {item.Task}");
|
System.Console.WriteLine($" [{status}] {item.Task}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolver = _serviceProvider.GetRequiredService<IConflictResolver>();
|
var resolver = _serviceProvider.GetRequiredService<IConflictResolver>();
|
||||||
var resolverType = resolver.GetType().Name;
|
string resolverType = resolver.GetType().Name;
|
||||||
System.Console.WriteLine($"\nℹ️ Resolution Strategy: {resolverType}");
|
System.Console.WriteLine($"\nℹ️ Resolution Strategy: {resolverType}");
|
||||||
|
|
||||||
if (resolverType.Contains("Recursive"))
|
if (resolverType.Contains("Recursive"))
|
||||||
{
|
|
||||||
System.Console.WriteLine(" → Items merged by 'id', both edits preserved");
|
System.Console.WriteLine(" → Items merged by 'id', both edits preserved");
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
System.Console.WriteLine(" → Last write wins, Node B changes override Node A");
|
System.Console.WriteLine(" → Last write wins, Node B changes override Node A");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
System.Console.WriteLine("\n✓ Demo complete. Run 'todos' to see all lists.\n");
|
System.Console.WriteLine("\n✓ Demo complete. Run 'todos' to see all lists.\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,26 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Cache;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Resilience;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
|
||||||
using ZB.MOM.WW.CBDDC.Sample.Console;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
|
|
||||||
// Local User/Address classes removed in favor of Shared project
|
// Local User/Address classes removed in favor of Shared project
|
||||||
|
|
||||||
class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
static async Task Main(string[] args)
|
private static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
|
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
.AddJsonFile("appsettings.json", true, true);
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
@@ -38,39 +31,36 @@ class Program
|
|||||||
.Enrich.WithProperty("Application", "CBDDC.Sample.Console")
|
.Enrich.WithProperty("Application", "CBDDC.Sample.Console")
|
||||||
.WriteTo.Console());
|
.WriteTo.Console());
|
||||||
|
|
||||||
var randomPort = new Random().Next(1000, 9999);
|
int randomPort = new Random().Next(1000, 9999);
|
||||||
// Node ID
|
// Node ID
|
||||||
string nodeId = args.Length > 0 ? args[0] : ("node-" + randomPort);
|
string nodeId = args.Length > 0 ? args[0] : "node-" + randomPort;
|
||||||
int tcpPort = args.Length > 1 ? int.Parse(args[1]) : randomPort;
|
int tcpPort = args.Length > 1 ? int.Parse(args[1]) : randomPort;
|
||||||
|
|
||||||
|
|
||||||
// Conflict Resolution Strategy (can be switched at runtime via service replacement)
|
// Conflict Resolution Strategy (can be switched at runtime via service replacement)
|
||||||
var useRecursiveMerge = args.Contains("--merge");
|
bool useRecursiveMerge = args.Contains("--merge");
|
||||||
if (useRecursiveMerge)
|
if (useRecursiveMerge) builder.Services.AddSingleton<IConflictResolver, RecursiveNodeMergeConflictResolver>();
|
||||||
{
|
|
||||||
builder.Services.AddSingleton<IConflictResolver, RecursiveNodeMergeConflictResolver>();
|
|
||||||
}
|
|
||||||
|
|
||||||
IPeerNodeConfigurationProvider peerNodeConfigurationProvider = new StaticPeerNodeConfigurationProvider(
|
IPeerNodeConfigurationProvider peerNodeConfigurationProvider = new StaticPeerNodeConfigurationProvider(
|
||||||
new PeerNodeConfiguration
|
new PeerNodeConfiguration
|
||||||
{
|
{
|
||||||
NodeId = nodeId,
|
NodeId = nodeId,
|
||||||
TcpPort = tcpPort,
|
TcpPort = tcpPort,
|
||||||
AuthToken = "Test-Cluster-Key",
|
AuthToken = "Test-Cluster-Key"
|
||||||
//KnownPeers = builder.Configuration.GetSection("CBDDC:KnownPeers").Get<List<KnownPeerConfiguration>>() ?? new()
|
//KnownPeers = builder.Configuration.GetSection("CBDDC:KnownPeers").Get<List<KnownPeerConfiguration>>() ?? new()
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddSingleton<IPeerNodeConfigurationProvider>(peerNodeConfigurationProvider);
|
builder.Services.AddSingleton(peerNodeConfigurationProvider);
|
||||||
|
|
||||||
// Database path
|
// Database path
|
||||||
var dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data");
|
string dataPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "data");
|
||||||
Directory.CreateDirectory(dataPath);
|
Directory.CreateDirectory(dataPath);
|
||||||
var databasePath = Path.Combine(dataPath, $"{nodeId}.blite");
|
string databasePath = Path.Combine(dataPath, $"{nodeId}.blite");
|
||||||
|
|
||||||
// Register CBDDC Services using Fluent Extensions with BLite, SampleDbContext, and SampleDocumentStore
|
// Register CBDDC Services using Fluent Extensions with BLite, SampleDbContext, and SampleDocumentStore
|
||||||
builder.Services.AddCBDDCCore()
|
builder.Services.AddCBDDCCore()
|
||||||
.AddCBDDCBLite<SampleDbContext, SampleDocumentStore>(sp => new SampleDbContext(databasePath))
|
.AddCBDDCBLite<SampleDbContext, SampleDocumentStore>(sp => new SampleDbContext(databasePath))
|
||||||
.AddCBDDCNetwork<StaticPeerNodeConfigurationProvider>(); // useHostedService = true by default
|
.AddCBDDCNetwork<StaticPeerNodeConfigurationProvider>(); // useHostedService = true by default
|
||||||
|
|
||||||
builder.Services.AddHostedService<ConsoleInteractiveService>(); // Runs the Input Loop
|
builder.Services.AddHostedService<ConsoleInteractiveService>(); // Runs the Input Loop
|
||||||
|
|
||||||
@@ -86,12 +76,7 @@ class Program
|
|||||||
private class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
|
private class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current peer node configuration.
|
/// Initializes a new instance of the <see cref="StaticPeerNodeConfigurationProvider" /> class.
|
||||||
/// </summary>
|
|
||||||
public PeerNodeConfiguration Configuration { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="StaticPeerNodeConfigurationProvider"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The initial peer node configuration.</param>
|
/// <param name="configuration">The initial peer node configuration.</param>
|
||||||
public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration)
|
public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration)
|
||||||
@@ -100,12 +85,17 @@ class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the peer node configuration changes.
|
/// Gets or sets the current peer node configuration.
|
||||||
|
/// </summary>
|
||||||
|
public PeerNodeConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when the peer node configuration changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current peer node configuration.
|
/// Gets the current peer node configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that returns the current configuration.</returns>
|
/// <returns>A task that returns the current configuration.</returns>
|
||||||
public Task<PeerNodeConfiguration> GetConfiguration()
|
public Task<PeerNodeConfiguration> GetConfiguration()
|
||||||
@@ -114,7 +104,7 @@ class Program
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raises the configuration changed event.
|
/// Raises the configuration changed event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="newConfig">The new configuration value.</param>
|
/// <param name="newConfig">The new configuration value.</param>
|
||||||
protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig)
|
protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig)
|
||||||
@@ -122,5 +112,4 @@ class Program
|
|||||||
ConfigurationChanged?.Invoke(this, newConfig);
|
ConfigurationChanged?.Invoke(this, newConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -5,21 +5,25 @@ This sample demonstrates the core features of CBDDC, a distributed peer-to-peer
|
|||||||
## Features Demonstrated
|
## Features Demonstrated
|
||||||
|
|
||||||
### 🔑 Primary Keys & Auto-Generation
|
### 🔑 Primary Keys & Auto-Generation
|
||||||
|
|
||||||
- Automatic GUID generation for entities
|
- Automatic GUID generation for entities
|
||||||
- Convention-based key detection (`Id` property)
|
- Convention-based key detection (`Id` property)
|
||||||
- `[PrimaryKey]` attribute support
|
- `[PrimaryKey]` attribute support
|
||||||
|
|
||||||
### 🎯 Generic Type-Safe API
|
### 🎯 Generic Type-Safe API
|
||||||
|
|
||||||
- `Collection<T>()` for compile-time type safety
|
- `Collection<T>()` for compile-time type safety
|
||||||
- Keyless `Put(entity)` with auto-key extraction
|
- Keyless `Put(entity)` with auto-key extraction
|
||||||
- IntelliSense-friendly operations
|
- IntelliSense-friendly operations
|
||||||
|
|
||||||
### 🔍 LINQ Query Support
|
### 🔍 LINQ Query Support
|
||||||
|
|
||||||
- Expression-based queries
|
- Expression-based queries
|
||||||
- Paging and sorting
|
- Paging and sorting
|
||||||
- Complex predicates (>, >=, ==, !=, nested properties)
|
- Complex predicates (>, >=, ==, !=, nested properties)
|
||||||
|
|
||||||
### 🌐 Network Synchronization
|
### 🌐 Network Synchronization
|
||||||
|
|
||||||
- UDP peer discovery
|
- UDP peer discovery
|
||||||
- TCP synchronization
|
- TCP synchronization
|
||||||
- Automatic conflict resolution (Last-Write-Wins)
|
- Automatic conflict resolution (Last-Write-Wins)
|
||||||
@@ -35,16 +39,19 @@ dotnet run
|
|||||||
### Multi-Node (Peer-to-Peer)
|
### Multi-Node (Peer-to-Peer)
|
||||||
|
|
||||||
Terminal 1:
|
Terminal 1:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run -- --node-id node1 --tcp-port 5001 --udp-port 6001
|
dotnet run -- --node-id node1 --tcp-port 5001 --udp-port 6001
|
||||||
```
|
```
|
||||||
|
|
||||||
Terminal 2:
|
Terminal 2:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run -- --node-id node2 --tcp-port 5002 --udp-port 6002
|
dotnet run -- --node-id node2 --tcp-port 5002 --udp-port 6002
|
||||||
```
|
```
|
||||||
|
|
||||||
Terminal 3:
|
Terminal 3:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run -- --node-id node3 --tcp-port 5003 --udp-port 6003
|
dotnet run -- --node-id node3 --tcp-port 5003 --udp-port 6003
|
||||||
```
|
```
|
||||||
@@ -53,20 +60,20 @@ Changes made on any node will automatically sync to all peers!
|
|||||||
|
|
||||||
## Available Commands
|
## Available Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|----------------------------------------|
|
||||||
| `p` | Put Alice and Bob (auto-generated IDs) |
|
| `p` | Put Alice and Bob (auto-generated IDs) |
|
||||||
| `g` | Get user by ID (prompts for ID) |
|
| `g` | Get user by ID (prompts for ID) |
|
||||||
| `d` | Delete user by ID (prompts for ID) |
|
| `d` | Delete user by ID (prompts for ID) |
|
||||||
| `n` | Create new user with auto-generated ID |
|
| `n` | Create new user with auto-generated ID |
|
||||||
| `s` | Spam 5 users with auto-generated IDs |
|
| `s` | Spam 5 users with auto-generated IDs |
|
||||||
| `c` | Count total documents |
|
| `c` | Count total documents |
|
||||||
| `f` | Demo various Find queries |
|
| `f` | Demo various Find queries |
|
||||||
| `f2` | Demo Find with paging (skip/take) |
|
| `f2` | Demo Find with paging (skip/take) |
|
||||||
| `a` | Demo auto-generated primary keys |
|
| `a` | Demo auto-generated primary keys |
|
||||||
| `t` | Demo generic typed API |
|
| `t` | Demo generic typed API |
|
||||||
| `l` | List active peers |
|
| `l` | List active peers |
|
||||||
| `q` | Quit |
|
| `q` | Quit |
|
||||||
|
|
||||||
## Example Session
|
## Example Session
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,13 @@
|
|||||||
using BLite.Core.Metadata;
|
using BLite.Core.Metadata;
|
||||||
using BLite.Core.Storage;
|
using BLite.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
|
|
||||||
public partial class SampleDbContext : CBDDCDocumentDbContext
|
public class SampleDbContext : CBDDCDocumentDbContext
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the users collection.
|
/// Initializes a new instance of the SampleDbContext class using the specified database file path.
|
||||||
/// </summary>
|
|
||||||
public DocumentCollection<string, User> Users { get; private set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the todo lists collection.
|
|
||||||
/// </summary>
|
|
||||||
public DocumentCollection<string, TodoList> TodoLists { get; private set; } = null!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the SampleDbContext class using the specified database file path.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
|
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
|
||||||
public SampleDbContext(string databasePath) : base(databasePath)
|
public SampleDbContext(string databasePath) : base(databasePath)
|
||||||
@@ -31,8 +16,8 @@ public partial class SampleDbContext : CBDDCDocumentDbContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the SampleDbContext class using the specified database file path and page file
|
/// Initializes a new instance of the SampleDbContext class using the specified database file path and page file
|
||||||
/// configuration.
|
/// configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
|
/// <param name="databasePath">The file system path to the database file. Cannot be null or empty.</param>
|
||||||
/// <param name="config">The configuration settings for the page file. Cannot be null.</param>
|
/// <param name="config">The configuration settings for the page file. Cannot be null.</param>
|
||||||
@@ -40,6 +25,16 @@ public partial class SampleDbContext : CBDDCDocumentDbContext
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the users collection.
|
||||||
|
/// </summary>
|
||||||
|
public DocumentCollection<string, User> Users { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the todo lists collection.
|
||||||
|
/// </summary>
|
||||||
|
public DocumentCollection<string, TodoList> TodoLists { get; private set; } = null!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -52,4 +47,4 @@ public partial class SampleDbContext : CBDDCDocumentDbContext
|
|||||||
.ToCollection("TodoLists")
|
.ToCollection("TodoLists")
|
||||||
.HasKey(t => t.Id);
|
.HasKey(t => t.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core;
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
using ZB.MOM.WW.CBDDC.Persistence.BLite;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Document store implementation for CBDDC Sample using BLite persistence.
|
/// Document store implementation for CBDDC Sample using BLite persistence.
|
||||||
/// Extends BLiteDocumentStore to automatically handle Oplog creation via CDC.
|
/// Extends BLiteDocumentStore to automatically handle Oplog creation via CDC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
||||||
{
|
{
|
||||||
@@ -18,7 +17,7 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
private const string TodoListsCollection = "TodoLists";
|
private const string TodoListsCollection = "TodoLists";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SampleDocumentStore"/> class.
|
/// Initializes a new instance of the <see cref="SampleDocumentStore" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The sample database context.</param>
|
/// <param name="context">The sample database context.</param>
|
||||||
/// <param name="configProvider">The peer node configuration provider.</param>
|
/// <param name="configProvider">The peer node configuration provider.</param>
|
||||||
@@ -37,6 +36,16 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
private static JsonElement? SerializeEntity<T>(T? entity) where T : class
|
||||||
|
{
|
||||||
|
if (entity == null) return null;
|
||||||
|
return JsonSerializer.SerializeToElement(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Abstract Method Implementations
|
#region Abstract Method Implementations
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -49,12 +58,10 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntitiesBatchAsync(
|
protected override async Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents, CancellationToken cancellationToken)
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (var (collection, key, content) in documents)
|
foreach ((string collection, string key, var content) in documents) UpsertEntity(collection, key, content);
|
||||||
{
|
|
||||||
UpsertEntity(collection, key, content);
|
|
||||||
}
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +98,10 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
protected override Task<JsonElement?> GetEntityAsJsonAsync(
|
protected override Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection, string key, CancellationToken cancellationToken)
|
string collection, string key, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.FromResult<JsonElement?>(collection switch
|
return Task.FromResult(collection switch
|
||||||
{
|
{
|
||||||
UsersCollection => SerializeEntity(_context.Users.Find(u => u.Id == key).FirstOrDefault()),
|
UsersCollection => SerializeEntity(_context.Users.Find(u => u.Id == key).FirstOrDefault()),
|
||||||
TodoListsCollection => SerializeEntity(_context.TodoLists.Find(t => t.Id == key).FirstOrDefault()),
|
TodoListsCollection => SerializeEntity(_context.TodoLists.Find(t => t.Id == key).FirstOrDefault()),
|
||||||
_ => null
|
_ => null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -111,10 +118,7 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
protected override async Task RemoveEntitiesBatchAsync(
|
protected override async Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken)
|
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (var (collection, key) in documents)
|
foreach ((string collection, string key) in documents) DeleteEntity(collection, key);
|
||||||
{
|
|
||||||
DeleteEntity(collection, key);
|
|
||||||
}
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,25 +144,15 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
{
|
{
|
||||||
return await Task.Run(() => collection switch
|
return await Task.Run(() => collection switch
|
||||||
{
|
{
|
||||||
UsersCollection => _context.Users.FindAll()
|
UsersCollection => _context.Users.FindAll()
|
||||||
.Select(u => (u.Id, SerializeEntity(u)!.Value)),
|
.Select(u => (u.Id, SerializeEntity(u)!.Value)),
|
||||||
|
|
||||||
TodoListsCollection => _context.TodoLists.FindAll()
|
TodoListsCollection => _context.TodoLists.FindAll()
|
||||||
.Select(t => (t.Id, SerializeEntity(t)!.Value)),
|
.Select(t => (t.Id, SerializeEntity(t)!.Value)),
|
||||||
|
|
||||||
_ => Enumerable.Empty<(string, JsonElement)>()
|
_ => Enumerable.Empty<(string, JsonElement)>()
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
}
|
||||||
#region Helper Methods
|
|
||||||
|
|
||||||
private static JsonElement? SerializeEntity<T>(T? entity) where T : class
|
|
||||||
{
|
|
||||||
if (entity == null) return null;
|
|
||||||
return JsonSerializer.SerializeToElement(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,22 @@
|
|||||||
using System.Collections.Generic;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
|
||||||
|
|
||||||
public class TodoList
|
public class TodoList
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the document identifier.
|
/// Gets or sets the document identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
public string Id { get; set; } = Guid.NewGuid().ToString();
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list name.
|
/// Gets or sets the list name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the todo items in the list.
|
/// Gets or sets the todo items in the list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<TodoItem> Items { get; set; } = new();
|
public List<TodoItem> Items { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -25,17 +24,17 @@ public class TodoList
|
|||||||
public class TodoItem
|
public class TodoItem
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the task description.
|
/// Gets or sets the task description.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Task { get; set; } = string.Empty;
|
public string Task { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether the task is completed.
|
/// Gets or sets a value indicating whether the task is completed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Completed { get; set; }
|
public bool Completed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the UTC creation timestamp.
|
/// Gets or sets the UTC creation timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
namespace ZB.MOM.WW.CBDDC.Sample.Console;
|
||||||
|
|
||||||
public class User
|
public class User
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique user identifier.
|
/// Gets or sets the unique user identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
public string Id { get; set; } = "";
|
public string Id { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user name.
|
/// Gets or sets the user name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user age.
|
/// Gets or sets the user age.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Age { get; set; }
|
public int Age { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the user address.
|
/// Gets or sets the user address.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Address? Address { get; set; }
|
public Address? Address { get; set; }
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ public class User
|
|||||||
public class Address
|
public class Address
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the city value.
|
/// Gets or sets the city value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? City { get; set; }
|
public string? City { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,41 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Lifter.Core" Version="1.1.0" />
|
<PackageReference Include="Lifter.Core" Version="1.1.0"/>
|
||||||
<PackageReference Include="BLite.SourceGenerators" Version="1.3.1">
|
<PackageReference Include="BLite.SourceGenerators" Version="1.3.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj" />
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj"/>
|
||||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Network\ZB.MOM.WW.CBDDC.Network.csproj" />
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Network\ZB.MOM.WW.CBDDC.Network.csproj"/>
|
||||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Persistence\ZB.MOM.WW.CBDDC.Persistence.csproj" />
|
<ProjectReference Include="..\..\src\ZB.MOM.WW.CBDDC.Persistence\ZB.MOM.WW.CBDDC.Persistence.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0"/>
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0"/>
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.json">
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>ZB.MOM.WW.CBDDC.Sample.Console</AssemblyName>
|
<AssemblyName>ZB.MOM.WW.CBDDC.Sample.Console</AssemblyName>
|
||||||
<RootNamespace>ZB.MOM.WW.CBDDC.Sample.Console</RootNamespace>
|
<RootNamespace>ZB.MOM.WW.CBDDC.Sample.Console</RootNamespace>
|
||||||
<PackageId>ZB.MOM.WW.CBDDC.Sample.Console</PackageId>
|
<PackageId>ZB.MOM.WW.CBDDC.Sample.Console</PackageId>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft": "Warning",
|
"Microsoft": "Warning",
|
||||||
"System": "Warning",
|
"System": "Warning",
|
||||||
"CBDDC": "Information",
|
"CBDDC": "Information",
|
||||||
"ZB.MOM.WW.CBDDC.Network.SyncOrchestrator": "Information",
|
"ZB.MOM.WW.CBDDC.Network.SyncOrchestrator": "Information",
|
||||||
"ZB.MOM.WW.CBDDC.Core.Storage.OplogCoordinator": "Warning",
|
"ZB.MOM.WW.CBDDC.Core.Storage.OplogCoordinator": "Warning",
|
||||||
"ZB.MOM.WW.CBDDC.Persistence": "Warning"
|
"ZB.MOM.WW.CBDDC.Persistence": "Warning"
|
||||||
}
|
|
||||||
},
|
|
||||||
"CBDDC": {
|
|
||||||
"Network": {
|
|
||||||
"TcpPort": 5001,
|
|
||||||
"UdpPort": 6000,
|
|
||||||
"AuthToken": "demo-secret-key",
|
|
||||||
"ConnectionTimeoutMs": 5000,
|
|
||||||
"RetryAttempts": 3,
|
|
||||||
"RetryDelayMs": 1000,
|
|
||||||
"LocalhostOnly": false
|
|
||||||
},
|
|
||||||
"Persistence": {
|
|
||||||
"DatabasePath": "data/cbddc.db",
|
|
||||||
"EnableWalMode": true,
|
|
||||||
"CacheSizeMb": 50,
|
|
||||||
"EnableAutoBackup": true,
|
|
||||||
"BackupPath": "backups/",
|
|
||||||
"BusyTimeoutMs": 5000
|
|
||||||
},
|
|
||||||
"Sync": {
|
|
||||||
"SyncIntervalMs": 5000,
|
|
||||||
"BatchSize": 100,
|
|
||||||
"EnableOfflineQueue": true,
|
|
||||||
"MaxQueueSize": 1000
|
|
||||||
},
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": "Information",
|
|
||||||
"LogFilePath": "logs/cbddc.log",
|
|
||||||
"MaxLogFileSizeMb": 10,
|
|
||||||
"MaxLogFiles": 5
|
|
||||||
},
|
|
||||||
"KnownPeers": [
|
|
||||||
{
|
|
||||||
"NodeId": "AspNetSampleNode",
|
|
||||||
"Host": "localhost",
|
|
||||||
"Port": 6001
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CBDDC": {
|
||||||
|
"Network": {
|
||||||
|
"TcpPort": 5001,
|
||||||
|
"UdpPort": 6000,
|
||||||
|
"AuthToken": "demo-secret-key",
|
||||||
|
"ConnectionTimeoutMs": 5000,
|
||||||
|
"RetryAttempts": 3,
|
||||||
|
"RetryDelayMs": 1000,
|
||||||
|
"LocalhostOnly": false
|
||||||
|
},
|
||||||
|
"Persistence": {
|
||||||
|
"DatabasePath": "data/cbddc.db",
|
||||||
|
"EnableWalMode": true,
|
||||||
|
"CacheSizeMb": 50,
|
||||||
|
"EnableAutoBackup": true,
|
||||||
|
"BackupPath": "backups/",
|
||||||
|
"BusyTimeoutMs": 5000
|
||||||
|
},
|
||||||
|
"Sync": {
|
||||||
|
"SyncIntervalMs": 5000,
|
||||||
|
"BatchSize": 100,
|
||||||
|
"EnableOfflineQueue": true,
|
||||||
|
"MaxQueueSize": 1000
|
||||||
|
},
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": "Information",
|
||||||
|
"LogFilePath": "logs/cbddc.log",
|
||||||
|
"MaxLogFileSizeMb": 10,
|
||||||
|
"MaxLogFiles": 5
|
||||||
|
},
|
||||||
|
"KnownPeers": [
|
||||||
|
{
|
||||||
|
"NodeId": "AspNetSampleNode",
|
||||||
|
"Host": "localhost",
|
||||||
|
"Port": 6001
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,76 +1,75 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Threading.Tasks;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Logging;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Cache;
|
namespace ZB.MOM.WW.CBDDC.Core.Cache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// LRU cache entry with linked list node.
|
/// LRU cache entry with linked list node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class CacheEntry
|
internal class CacheEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cached document.
|
/// Initializes a new instance of the <see cref="CacheEntry" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Document Document { get; }
|
/// <param name="document">The cached document.</param>
|
||||||
|
/// <param name="node">The linked-list node used for LRU tracking.</param>
|
||||||
/// <summary>
|
public CacheEntry(Document document, LinkedListNode<string> node)
|
||||||
/// Gets the linked-list node used for LRU tracking.
|
{
|
||||||
/// </summary>
|
Document = document;
|
||||||
public LinkedListNode<string> Node { get; }
|
Node = node;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CacheEntry"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="document">The cached document.</param>
|
|
||||||
/// <param name="node">The linked-list node used for LRU tracking.</param>
|
|
||||||
public CacheEntry(Document document, LinkedListNode<string> node)
|
|
||||||
{
|
|
||||||
Document = document;
|
|
||||||
Node = node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the cached document.
|
||||||
|
/// </summary>
|
||||||
|
public Document Document { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the linked-list node used for LRU tracking.
|
||||||
|
/// </summary>
|
||||||
|
public LinkedListNode<string> Node { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// In-memory LRU cache for documents.
|
/// In-memory LRU cache for documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocumentCache : IDocumentCache
|
public class DocumentCache : IDocumentCache
|
||||||
{
|
{
|
||||||
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
private readonly Dictionary<string, CacheEntry> _cache = new();
|
||||||
private readonly Dictionary<string, CacheEntry> _cache = new();
|
|
||||||
private readonly LinkedList<string> _lru = new();
|
|
||||||
private readonly ILogger<DocumentCache> _logger;
|
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
private readonly ILogger<DocumentCache> _logger;
|
||||||
|
private readonly LinkedList<string> _lru = new();
|
||||||
|
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
private long _hits = 0;
|
private long _hits;
|
||||||
private long _misses = 0;
|
private long _misses;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DocumentCache"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="peerNodeConfigurationProvider">The configuration provider used for cache size limits.</param>
|
|
||||||
/// <param name="logger">The logger instance.</param>
|
|
||||||
public DocumentCache(IPeerNodeConfigurationProvider peerNodeConfigurationProvider, ILogger<DocumentCache>? logger = null)
|
|
||||||
{
|
|
||||||
_peerNodeConfigurationProvider = peerNodeConfigurationProvider;
|
|
||||||
_logger = logger ?? NullLogger<DocumentCache>.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a document from cache.
|
/// Initializes a new instance of the <see cref="DocumentCache" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The document collection name.</param>
|
/// <param name="peerNodeConfigurationProvider">The configuration provider used for cache size limits.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="logger">The logger instance.</param>
|
||||||
/// <returns>A task whose result is the cached document, or <see langword="null"/> if not found.</returns>
|
public DocumentCache(IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
||||||
public async Task<Document?> Get(string collection, string key)
|
ILogger<DocumentCache>? logger = null)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
_peerNodeConfigurationProvider = peerNodeConfigurationProvider;
|
||||||
{
|
_logger = logger ?? NullLogger<DocumentCache>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a document from cache.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The document collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <returns>A task whose result is the cached document, or <see langword="null" /> if not found.</returns>
|
||||||
|
public async Task<Document?> Get(string collection, string key)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
var cacheKey = $"{collection}:{key}";
|
var cacheKey = $"{collection}:{key}";
|
||||||
|
|
||||||
if (_cache.TryGetValue(cacheKey, out var entry))
|
if (_cache.TryGetValue(cacheKey, out var entry))
|
||||||
@@ -90,16 +89,16 @@ public class DocumentCache : IDocumentCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets a document in cache.
|
/// Sets a document in cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The document collection name.</param>
|
/// <param name="collection">The document collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="document">The document to cache.</param>
|
/// <param name="document">The document to cache.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
public async Task Set(string collection, string key, Document document)
|
public async Task Set(string collection, string key, Document document)
|
||||||
{
|
{
|
||||||
var peerConfig = await _peerNodeConfigurationProvider.GetConfiguration();
|
var peerConfig = await _peerNodeConfigurationProvider.GetConfiguration();
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -118,7 +117,7 @@ public class DocumentCache : IDocumentCache
|
|||||||
// Evict if full
|
// Evict if full
|
||||||
if (_cache.Count >= peerConfig.MaxDocumentCacheSize)
|
if (_cache.Count >= peerConfig.MaxDocumentCacheSize)
|
||||||
{
|
{
|
||||||
var oldest = _lru.Last!.Value;
|
string oldest = _lru.Last!.Value;
|
||||||
_lru.RemoveLast();
|
_lru.RemoveLast();
|
||||||
_cache.Remove(oldest);
|
_cache.Remove(oldest);
|
||||||
_logger.LogTrace("Evicted oldest cache entry {Key}", oldest);
|
_logger.LogTrace("Evicted oldest cache entry {Key}", oldest);
|
||||||
@@ -130,15 +129,15 @@ public class DocumentCache : IDocumentCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a document from cache.
|
/// Removes a document from cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The document collection name.</param>
|
/// <param name="collection">The document collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
public void Remove(string collection, string key)
|
public void Remove(string collection, string key)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var cacheKey = $"{collection}:{key}";
|
var cacheKey = $"{collection}:{key}";
|
||||||
|
|
||||||
if (_cache.TryGetValue(cacheKey, out var entry))
|
if (_cache.TryGetValue(cacheKey, out var entry))
|
||||||
@@ -151,13 +150,13 @@ public class DocumentCache : IDocumentCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all cached documents.
|
/// Clears all cached documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var count = _cache.Count;
|
int count = _cache.Count;
|
||||||
_cache.Clear();
|
_cache.Clear();
|
||||||
_lru.Clear();
|
_lru.Clear();
|
||||||
_logger.LogInformation("Cleared cache ({Count} entries)", count);
|
_logger.LogInformation("Cleared cache ({Count} entries)", count);
|
||||||
@@ -165,15 +164,15 @@ public class DocumentCache : IDocumentCache
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets cache statistics.
|
/// Gets cache statistics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public (long Hits, long Misses, int Size, double HitRate) GetStatistics()
|
public (long Hits, long Misses, int Size, double HitRate) GetStatistics()
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var total = _hits + _misses;
|
long total = _hits + _misses;
|
||||||
var hitRate = total > 0 ? (double)_hits / total : 0;
|
double hitRate = total > 0 ? (double)_hits / total : 0;
|
||||||
return (_hits, _misses, _cache.Count, hitRate);
|
return (_hits, _misses, _cache.Count, hitRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Cache
|
namespace ZB.MOM.WW.CBDDC.Core.Cache;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines operations for caching documents by collection and key.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDocumentCache
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines operations for caching documents by collection and key.
|
/// Clears all cached documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDocumentCache
|
void Clear();
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Clears all cached documents.
|
|
||||||
/// </summary>
|
|
||||||
void Clear();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a cached document by collection and key.
|
/// Gets a cached document by collection and key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <returns>The cached document, or <see langword="null"/> if not found.</returns>
|
/// <returns>The cached document, or <see langword="null" /> if not found.</returns>
|
||||||
Task<Document?> Get(string collection, string key);
|
Task<Document?> Get(string collection, string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets cache hit/miss statistics.
|
/// Gets cache hit/miss statistics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A tuple containing hits, misses, current size, and hit rate.</returns>
|
/// <returns>A tuple containing hits, misses, current size, and hit rate.</returns>
|
||||||
(long Hits, long Misses, int Size, double HitRate) GetStatistics();
|
(long Hits, long Misses, int Size, double HitRate) GetStatistics();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a cached document by collection and key.
|
/// Removes a cached document by collection and key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
void Remove(string collection, string key);
|
void Remove(string collection, string key);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds or updates a cached document.
|
/// Adds or updates a cached document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="document">The document to cache.</param>
|
/// <param name="document">The document to cache.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Set(string collection, string key, Document document);
|
Task Set(string collection, string key, Document document);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event arguments for when changes are applied to the peer store.
|
/// Event arguments for when changes are applied to the peer store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ChangesAppliedEventArgs : EventArgs
|
public class ChangesAppliedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the changes that were applied.
|
/// Initializes a new instance of the <see cref="ChangesAppliedEventArgs" /> class.
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OplogEntry> Changes { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ChangesAppliedEventArgs"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="changes">The changes that were applied.</param>
|
/// <param name="changes">The changes that were applied.</param>
|
||||||
public ChangesAppliedEventArgs(IEnumerable<OplogEntry> changes)
|
public ChangesAppliedEventArgs(IEnumerable<OplogEntry> changes)
|
||||||
{
|
{
|
||||||
Changes = changes;
|
Changes = changes;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the changes that were applied.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<OplogEntry> Changes { get; }
|
||||||
|
}
|
||||||
@@ -1,45 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using System.Linq;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides health check functionality.
|
/// Provides health check functionality.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCHealthCheck : ICBDDCHealthCheck
|
public class CBDDCHealthCheck : ICBDDCHealthCheck
|
||||||
{
|
{
|
||||||
private readonly IOplogStore _store;
|
private readonly ILogger<CBDDCHealthCheck> _logger;
|
||||||
private readonly ISyncStatusTracker _syncTracker;
|
private readonly IOplogStore _store;
|
||||||
private readonly ILogger<CBDDCHealthCheck> _logger;
|
private readonly ISyncStatusTracker _syncTracker;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CBDDCHealthCheck"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="store">The oplog store used for database health checks.</param>
|
|
||||||
/// <param name="syncTracker">The tracker that provides synchronization status.</param>
|
|
||||||
/// <param name="logger">The logger instance.</param>
|
|
||||||
public CBDDCHealthCheck(
|
|
||||||
IOplogStore store,
|
|
||||||
ISyncStatusTracker syncTracker,
|
|
||||||
ILogger<CBDDCHealthCheck>? logger = null)
|
|
||||||
{
|
|
||||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
|
||||||
_syncTracker = syncTracker ?? throw new ArgumentNullException(nameof(syncTracker));
|
|
||||||
_logger = logger ?? NullLogger<CBDDCHealthCheck>.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a comprehensive health check.
|
/// Initializes a new instance of the <see cref="CBDDCHealthCheck" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the health check.</param>
|
/// <param name="store">The oplog store used for database health checks.</param>
|
||||||
public async Task<HealthStatus> CheckAsync(CancellationToken cancellationToken = default)
|
/// <param name="syncTracker">The tracker that provides synchronization status.</param>
|
||||||
{
|
/// <param name="logger">The logger instance.</param>
|
||||||
|
public CBDDCHealthCheck(
|
||||||
|
IOplogStore store,
|
||||||
|
ISyncStatusTracker syncTracker,
|
||||||
|
ILogger<CBDDCHealthCheck>? logger = null)
|
||||||
|
{
|
||||||
|
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||||
|
_syncTracker = syncTracker ?? throw new ArgumentNullException(nameof(syncTracker));
|
||||||
|
_logger = logger ?? NullLogger<CBDDCHealthCheck>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a comprehensive health check.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A token used to cancel the health check.</param>
|
||||||
|
public async Task<HealthStatus> CheckAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
var status = new HealthStatus();
|
var status = new HealthStatus();
|
||||||
|
|
||||||
// Check database health
|
// Check database health
|
||||||
@@ -65,9 +64,7 @@ public class CBDDCHealthCheck : ICBDDCHealthCheck
|
|||||||
|
|
||||||
// Add error messages from sync tracker
|
// Add error messages from sync tracker
|
||||||
foreach (var error in syncStatus.SyncErrors.Take(5)) // Last 5 errors
|
foreach (var error in syncStatus.SyncErrors.Take(5)) // Last 5 errors
|
||||||
{
|
|
||||||
status.Errors.Add($"{error.Timestamp:yyyy-MM-dd HH:mm:ss} - {error.Message}");
|
status.Errors.Add($"{error.Timestamp:yyyy-MM-dd HH:mm:ss} - {error.Message}");
|
||||||
}
|
|
||||||
|
|
||||||
// Add metadata
|
// Add metadata
|
||||||
status.Metadata["TotalDocumentsSynced"] = syncStatus.TotalDocumentsSynced;
|
status.Metadata["TotalDocumentsSynced"] = syncStatus.TotalDocumentsSynced;
|
||||||
@@ -79,4 +76,4 @@ public class CBDDCHealthCheck : ICBDDCHealthCheck
|
|||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,145 +4,145 @@ using System.Collections.Generic;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the health status of an CBDDC instance.
|
/// Represents the health status of an CBDDC instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class HealthStatus
|
public class HealthStatus
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the database is healthy.
|
/// Indicates if the database is healthy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DatabaseHealthy { get; set; }
|
public bool DatabaseHealthy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if network connectivity is available.
|
/// Indicates if network connectivity is available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool NetworkHealthy { get; set; }
|
public bool NetworkHealthy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of currently connected peers.
|
/// Number of currently connected peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ConnectedPeers { get; set; }
|
public int ConnectedPeers { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp of the last successful sync operation.
|
/// Timestamp of the last successful sync operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? LastSyncTime { get; set; }
|
public DateTime? LastSyncTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of recent errors.
|
/// List of recent errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<string> Errors { get; set; } = new();
|
public List<string> Errors { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Overall health status.
|
/// Overall health status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsHealthy => DatabaseHealthy && NetworkHealthy && Errors.Count == 0;
|
public bool IsHealthy => DatabaseHealthy && NetworkHealthy && Errors.Count == 0;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Additional diagnostic information.
|
/// Additional diagnostic information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the synchronization status.
|
/// Represents the synchronization status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncStatus
|
public class SyncStatus
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the node is currently online.
|
/// Indicates if the node is currently online.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsOnline { get; set; }
|
public bool IsOnline { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp of the last sync operation.
|
/// Timestamp of the last sync operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? LastSyncTime { get; set; }
|
public DateTime? LastSyncTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of pending operations in the offline queue.
|
/// Number of pending operations in the offline queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int PendingOperations { get; set; }
|
public int PendingOperations { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of active peer nodes.
|
/// List of active peer nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<PeerInfo> ActivePeers { get; set; } = new();
|
public List<PeerInfo> ActivePeers { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recent sync errors.
|
/// Recent sync errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SyncError> SyncErrors { get; set; } = new();
|
public List<SyncError> SyncErrors { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total number of documents synced.
|
/// Total number of documents synced.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long TotalDocumentsSynced { get; set; }
|
public long TotalDocumentsSynced { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total bytes transferred.
|
/// Total bytes transferred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long TotalBytesTransferred { get; set; }
|
public long TotalBytesTransferred { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Information about a peer node.
|
/// Information about a peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PeerInfo
|
public class PeerInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unique identifier of the peer.
|
/// Unique identifier of the peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Network address of the peer.
|
/// Network address of the peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Address { get; set; } = "";
|
public string Address { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Last time the peer was seen.
|
/// Last time the peer was seen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime LastSeen { get; set; }
|
public DateTime LastSeen { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates if the peer is currently connected.
|
/// Indicates if the peer is currently connected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsConnected { get; set; }
|
public bool IsConnected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of successful syncs with this peer.
|
/// Number of successful syncs with this peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int SuccessfulSyncs { get; set; }
|
public int SuccessfulSyncs { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of failed syncs with this peer.
|
/// Number of failed syncs with this peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int FailedSyncs { get; set; }
|
public int FailedSyncs { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a synchronization error.
|
/// Represents a synchronization error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncError
|
public class SyncError
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp when the error occurred.
|
/// Timestamp when the error occurred.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error message.
|
/// Error message.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Message { get; set; } = "";
|
public string Message { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Peer node ID if applicable.
|
/// Peer node ID if applicable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PeerNodeId { get; set; }
|
public string? PeerNodeId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Error code.
|
/// Error code.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ErrorCode { get; set; }
|
public string? ErrorCode { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics
|
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
|
|
||||||
|
public interface ICBDDCHealthCheck
|
||||||
{
|
{
|
||||||
public interface ICBDDCHealthCheck
|
/// <summary>
|
||||||
{
|
/// Performs a health check for the implementing component.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Performs a health check for the implementing component.
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// </summary>
|
/// <returns>The resulting health status.</returns>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
Task<HealthStatus> CheckAsync(CancellationToken cancellationToken = default);
|
||||||
/// <returns>The resulting health status.</returns>
|
}
|
||||||
Task<HealthStatus> CheckAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +1,62 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics
|
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks synchronization status and peer health metrics.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISyncStatusTracker
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks synchronization status and peer health metrics.
|
/// Removes peer entries that have been inactive longer than the specified threshold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISyncStatusTracker
|
/// <param name="inactiveThreshold">The inactivity threshold used to prune peers.</param>
|
||||||
{
|
void CleanupInactivePeers(TimeSpan inactiveThreshold);
|
||||||
/// <summary>
|
|
||||||
/// Removes peer entries that have been inactive longer than the specified threshold.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inactiveThreshold">The inactivity threshold used to prune peers.</param>
|
|
||||||
void CleanupInactivePeers(TimeSpan inactiveThreshold);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current synchronization status snapshot.
|
/// Gets the current synchronization status snapshot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The current <see cref="SyncStatus"/>.</returns>
|
/// <returns>The current <see cref="SyncStatus" />.</returns>
|
||||||
SyncStatus GetStatus();
|
SyncStatus GetStatus();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records an error encountered during synchronization.
|
/// Records an error encountered during synchronization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The error message.</param>
|
/// <param name="message">The error message.</param>
|
||||||
/// <param name="peerNodeId">The related peer node identifier, if available.</param>
|
/// <param name="peerNodeId">The related peer node identifier, if available.</param>
|
||||||
/// <param name="errorCode">An optional error code.</param>
|
/// <param name="errorCode">An optional error code.</param>
|
||||||
void RecordError(string message, string? peerNodeId = null, string? errorCode = null);
|
void RecordError(string message, string? peerNodeId = null, string? errorCode = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a failed operation for the specified peer.
|
/// Records a failed operation for the specified peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
void RecordPeerFailure(string nodeId);
|
void RecordPeerFailure(string nodeId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a successful operation for the specified peer.
|
/// Records a successful operation for the specified peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
void RecordPeerSuccess(string nodeId);
|
void RecordPeerSuccess(string nodeId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records synchronization throughput metrics.
|
/// Records synchronization throughput metrics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documentCount">The number of synchronized documents.</param>
|
/// <param name="documentCount">The number of synchronized documents.</param>
|
||||||
/// <param name="bytesTransferred">The number of bytes transferred.</param>
|
/// <param name="bytesTransferred">The number of bytes transferred.</param>
|
||||||
void RecordSync(int documentCount, long bytesTransferred);
|
void RecordSync(int documentCount, long bytesTransferred);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets whether the local node is currently online.
|
/// Sets whether the local node is currently online.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="isOnline">A value indicating whether the node is online.</param>
|
/// <param name="isOnline">A value indicating whether the node is online.</param>
|
||||||
void SetOnlineStatus(bool isOnline);
|
void SetOnlineStatus(bool isOnline);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates peer connectivity details.
|
/// Updates peer connectivity details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
/// <param name="address">The peer network address.</param>
|
/// <param name="address">The peer network address.</param>
|
||||||
/// <param name="isConnected">A value indicating whether the peer is connected.</param>
|
/// <param name="isConnected">A value indicating whether the peer is connected.</param>
|
||||||
void UpdatePeer(string nodeId, string address, bool isConnected);
|
void UpdatePeer(string nodeId, string address, bool isConnected);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
namespace ZB.MOM.WW.CBDDC.Core.Diagnostics;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks synchronization status and provides diagnostics.
|
/// Tracks synchronization status and provides diagnostics.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncStatusTracker : ISyncStatusTracker
|
public class SyncStatusTracker : ISyncStatusTracker
|
||||||
{
|
{
|
||||||
private readonly ILogger<SyncStatusTracker> _logger;
|
private const int MaxErrorHistory = 50;
|
||||||
private readonly object _lock = new();
|
|
||||||
|
|
||||||
private bool _isOnline = false;
|
|
||||||
private DateTime? _lastSyncTime;
|
|
||||||
private readonly List<PeerInfo> _activePeers = new();
|
private readonly List<PeerInfo> _activePeers = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly ILogger<SyncStatusTracker> _logger;
|
||||||
private readonly Queue<SyncError> _recentErrors = new();
|
private readonly Queue<SyncError> _recentErrors = new();
|
||||||
private long _totalDocumentsSynced = 0;
|
|
||||||
private long _totalBytesTransferred = 0;
|
private bool _isOnline;
|
||||||
private const int MaxErrorHistory = 50;
|
private DateTime? _lastSyncTime;
|
||||||
|
private long _totalBytesTransferred;
|
||||||
/// <summary>
|
private long _totalDocumentsSynced;
|
||||||
/// Initializes a new instance of the <see cref="SyncStatusTracker"/> class.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="logger">Optional logger instance.</param>
|
/// Initializes a new instance of the <see cref="SyncStatusTracker" /> class.
|
||||||
public SyncStatusTracker(ILogger<SyncStatusTracker>? logger = null)
|
/// </summary>
|
||||||
{
|
/// <param name="logger">Optional logger instance.</param>
|
||||||
_logger = logger ?? NullLogger<SyncStatusTracker>.Instance;
|
public SyncStatusTracker(ILogger<SyncStatusTracker>? logger = null)
|
||||||
}
|
{
|
||||||
|
_logger = logger ?? NullLogger<SyncStatusTracker>.Instance;
|
||||||
/// <summary>
|
}
|
||||||
/// Updates online status.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="isOnline">Whether the node is currently online.</param>
|
/// Updates online status.
|
||||||
public void SetOnlineStatus(bool isOnline)
|
/// </summary>
|
||||||
{
|
/// <param name="isOnline">Whether the node is currently online.</param>
|
||||||
lock (_lock)
|
public void SetOnlineStatus(bool isOnline)
|
||||||
{
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
if (_isOnline != isOnline)
|
if (_isOnline != isOnline)
|
||||||
{
|
{
|
||||||
_isOnline = isOnline;
|
_isOnline = isOnline;
|
||||||
@@ -47,15 +47,15 @@ public class SyncStatusTracker : ISyncStatusTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a successful sync operation.
|
/// Records a successful sync operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documentCount">The number of documents synchronized.</param>
|
/// <param name="documentCount">The number of documents synchronized.</param>
|
||||||
/// <param name="bytesTransferred">The number of bytes transferred.</param>
|
/// <param name="bytesTransferred">The number of bytes transferred.</param>
|
||||||
public void RecordSync(int documentCount, long bytesTransferred)
|
public void RecordSync(int documentCount, long bytesTransferred)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_lastSyncTime = DateTime.UtcNow;
|
_lastSyncTime = DateTime.UtcNow;
|
||||||
_totalDocumentsSynced += documentCount;
|
_totalDocumentsSynced += documentCount;
|
||||||
_totalBytesTransferred += bytesTransferred;
|
_totalBytesTransferred += bytesTransferred;
|
||||||
@@ -64,16 +64,16 @@ public class SyncStatusTracker : ISyncStatusTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a sync error.
|
/// Records a sync error.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The error message.</param>
|
/// <param name="message">The error message.</param>
|
||||||
/// <param name="peerNodeId">The related peer node identifier, if available.</param>
|
/// <param name="peerNodeId">The related peer node identifier, if available.</param>
|
||||||
/// <param name="errorCode">The error code, if available.</param>
|
/// <param name="errorCode">The error code, if available.</param>
|
||||||
public void RecordError(string message, string? peerNodeId = null, string? errorCode = null)
|
public void RecordError(string message, string? peerNodeId = null, string? errorCode = null)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var error = new SyncError
|
var error = new SyncError
|
||||||
{
|
{
|
||||||
Timestamp = DateTime.UtcNow,
|
Timestamp = DateTime.UtcNow,
|
||||||
@@ -84,25 +84,22 @@ public class SyncStatusTracker : ISyncStatusTracker
|
|||||||
|
|
||||||
_recentErrors.Enqueue(error);
|
_recentErrors.Enqueue(error);
|
||||||
|
|
||||||
while (_recentErrors.Count > MaxErrorHistory)
|
while (_recentErrors.Count > MaxErrorHistory) _recentErrors.Dequeue();
|
||||||
{
|
|
||||||
_recentErrors.Dequeue();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("Sync error recorded: {Message} (Peer: {Peer})", message, peerNodeId ?? "N/A");
|
_logger.LogWarning("Sync error recorded: {Message} (Peer: {Peer})", message, peerNodeId ?? "N/A");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates peer information.
|
/// Updates peer information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
/// <param name="address">The peer address.</param>
|
/// <param name="address">The peer address.</param>
|
||||||
/// <param name="isConnected">Whether the peer is currently connected.</param>
|
/// <param name="isConnected">Whether the peer is currently connected.</param>
|
||||||
public void UpdatePeer(string nodeId, string address, bool isConnected)
|
public void UpdatePeer(string nodeId, string address, bool isConnected)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
||||||
|
|
||||||
if (peer == null)
|
if (peer == null)
|
||||||
@@ -126,40 +123,34 @@ public class SyncStatusTracker : ISyncStatusTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records successful sync with a peer.
|
/// Records successful sync with a peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
public void RecordPeerSuccess(string nodeId)
|
public void RecordPeerSuccess(string nodeId)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
||||||
if (peer != null)
|
if (peer != null) peer.SuccessfulSyncs++;
|
||||||
{
|
|
||||||
peer.SuccessfulSyncs++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Records failed sync with a peer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nodeId">The peer node identifier.</param>
|
|
||||||
public void RecordPeerFailure(string nodeId)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
|
||||||
if (peer != null)
|
|
||||||
{
|
|
||||||
peer.FailedSyncs++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets current sync status.
|
/// Records failed sync with a peer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
|
public void RecordPeerFailure(string nodeId)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var peer = _activePeers.FirstOrDefault(p => p.NodeId == nodeId);
|
||||||
|
if (peer != null) peer.FailedSyncs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets current sync status.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SyncStatus GetStatus()
|
public SyncStatus GetStatus()
|
||||||
{
|
{
|
||||||
@@ -178,21 +169,18 @@ public class SyncStatusTracker : ISyncStatusTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cleans up inactive peers.
|
/// Cleans up inactive peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inactiveThreshold">The inactivity threshold used to remove peers.</param>
|
/// <param name="inactiveThreshold">The inactivity threshold used to remove peers.</param>
|
||||||
public void CleanupInactivePeers(TimeSpan inactiveThreshold)
|
public void CleanupInactivePeers(TimeSpan inactiveThreshold)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var cutoff = DateTime.UtcNow - inactiveThreshold;
|
var cutoff = DateTime.UtcNow - inactiveThreshold;
|
||||||
var removed = _activePeers.RemoveAll(p => p.LastSeen < cutoff);
|
int removed = _activePeers.RemoveAll(p => p.LastSeen < cutoff);
|
||||||
|
|
||||||
if (removed > 0)
|
if (removed > 0) _logger.LogInformation("Removed {Count} inactive peers", removed);
|
||||||
{
|
|
||||||
_logger.LogInformation("Removed {Count} inactive peers", removed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,15 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
using System.Text.Json;
|
||||||
using System;
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a stored document and its synchronization metadata.
|
/// Represents a stored document and its synchronization metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Document
|
public class Document
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection that contains the document.
|
/// Initializes a new instance of the <see cref="Document" /> class.
|
||||||
/// </summary>
|
|
||||||
public string Collection { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the document key.
|
|
||||||
/// </summary>
|
|
||||||
public string Key { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the document content.
|
|
||||||
/// </summary>
|
|
||||||
public JsonElement Content { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the timestamp of the latest applied update.
|
|
||||||
/// </summary>
|
|
||||||
public HlcTimestamp UpdatedAt { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a value indicating whether the document is deleted.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDeleted { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="Document"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection that contains the document.</param>
|
/// <param name="collection">The collection that contains the document.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
@@ -52,32 +26,59 @@ public class Document
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges a remote operation into the current document using last-write-wins or a conflict resolver.
|
/// Gets the collection that contains the document.
|
||||||
|
/// </summary>
|
||||||
|
public string Collection { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document key.
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document content.
|
||||||
|
/// </summary>
|
||||||
|
public JsonElement Content { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timestamp of the latest applied update.
|
||||||
|
/// </summary>
|
||||||
|
public HlcTimestamp UpdatedAt { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the document is deleted.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDeleted { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Merges a remote operation into the current document using last-write-wins or a conflict resolver.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="oplogEntry">The remote operation to merge.</param>
|
/// <param name="oplogEntry">The remote operation to merge.</param>
|
||||||
/// <param name="resolver">An optional conflict resolver for custom merge behavior.</param>
|
/// <param name="resolver">An optional conflict resolver for custom merge behavior.</param>
|
||||||
public void Merge(OplogEntry oplogEntry, IConflictResolver? resolver = null)
|
public void Merge(OplogEntry oplogEntry, IConflictResolver? resolver = null)
|
||||||
{
|
{
|
||||||
if (oplogEntry == null) return;
|
if (oplogEntry == null) return;
|
||||||
if (Collection != oplogEntry.Collection) return;
|
if (Collection != oplogEntry.Collection) return;
|
||||||
if (Key != oplogEntry.Key) return;
|
if (Key != oplogEntry.Key) return;
|
||||||
if (resolver == null)
|
if (resolver == null)
|
||||||
{
|
{
|
||||||
//last wins
|
//last wins
|
||||||
if (UpdatedAt <= oplogEntry.Timestamp)
|
if (UpdatedAt <= oplogEntry.Timestamp)
|
||||||
{
|
{
|
||||||
Content = oplogEntry.Payload ?? default;
|
Content = oplogEntry.Payload ?? default;
|
||||||
UpdatedAt = oplogEntry.Timestamp;
|
UpdatedAt = oplogEntry.Timestamp;
|
||||||
IsDeleted = oplogEntry.Operation == OperationType.Delete;
|
IsDeleted = oplogEntry.Operation == OperationType.Delete;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
return;
|
||||||
var resolutionResult = resolver.Resolve(this, oplogEntry);
|
}
|
||||||
if (resolutionResult.ShouldApply && resolutionResult.MergedDocument != null)
|
|
||||||
{
|
var resolutionResult = resolver.Resolve(this, oplogEntry);
|
||||||
Content = resolutionResult.MergedDocument.Content;
|
if (resolutionResult.ShouldApply && resolutionResult.MergedDocument != null)
|
||||||
UpdatedAt = resolutionResult.MergedDocument.UpdatedAt;
|
{
|
||||||
IsDeleted = resolutionResult.MergedDocument.IsDeleted;
|
Content = resolutionResult.MergedDocument.Content;
|
||||||
}
|
UpdatedAt = resolutionResult.MergedDocument.UpdatedAt;
|
||||||
}
|
IsDeleted = resolutionResult.MergedDocument.IsDeleted;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,14 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Exceptions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base exception for all CBDDC-related errors.
|
|
||||||
/// </summary>
|
|
||||||
public class CBDDCException : Exception
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Error code for programmatic error handling.
|
|
||||||
/// </summary>
|
|
||||||
public string ErrorCode { get; }
|
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Core.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base exception for all CBDDC-related errors.
|
||||||
|
/// </summary>
|
||||||
|
public class CBDDCException : Exception
|
||||||
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CBDDCException"/> class.
|
/// Initializes a new instance of the <see cref="CBDDCException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="errorCode">The application-specific error code.</param>
|
/// <param name="errorCode">The application-specific error code.</param>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
@@ -24,7 +19,7 @@ public class CBDDCException : Exception
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CBDDCException"/> class.
|
/// Initializes a new instance of the <see cref="CBDDCException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="errorCode">The application-specific error code.</param>
|
/// <param name="errorCode">The application-specific error code.</param>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
@@ -34,137 +29,151 @@ public class CBDDCException : Exception
|
|||||||
{
|
{
|
||||||
ErrorCode = errorCode;
|
ErrorCode = errorCode;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
/// <summary>
|
/// Error code for programmatic error handling.
|
||||||
/// Exception thrown when network operations fail.
|
/// </summary>
|
||||||
/// </summary>
|
public string ErrorCode { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when network operations fail.
|
||||||
|
/// </summary>
|
||||||
public class NetworkException : CBDDCException
|
public class NetworkException : CBDDCException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="NetworkException"/> class.
|
/// Initializes a new instance of the <see cref="NetworkException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public NetworkException(string message)
|
public NetworkException(string message)
|
||||||
: base("NETWORK_ERROR", message) { }
|
: base("NETWORK_ERROR", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="NetworkException"/> class.
|
/// Initializes a new instance of the <see cref="NetworkException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
/// <param name="innerException">The exception that caused the current exception.</param>
|
/// <param name="innerException">The exception that caused the current exception.</param>
|
||||||
public NetworkException(string message, Exception innerException)
|
public NetworkException(string message, Exception innerException)
|
||||||
: base("NETWORK_ERROR", message, innerException) { }
|
: base("NETWORK_ERROR", message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when persistence operations fail.
|
/// Exception thrown when persistence operations fail.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PersistenceException : CBDDCException
|
public class PersistenceException : CBDDCException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PersistenceException"/> class.
|
/// Initializes a new instance of the <see cref="PersistenceException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public PersistenceException(string message)
|
public PersistenceException(string message)
|
||||||
: base("PERSISTENCE_ERROR", message) { }
|
: base("PERSISTENCE_ERROR", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PersistenceException"/> class.
|
/// Initializes a new instance of the <see cref="PersistenceException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
/// <param name="innerException">The exception that caused the current exception.</param>
|
/// <param name="innerException">The exception that caused the current exception.</param>
|
||||||
public PersistenceException(string message, Exception innerException)
|
public PersistenceException(string message, Exception innerException)
|
||||||
: base("PERSISTENCE_ERROR", message, innerException) { }
|
: base("PERSISTENCE_ERROR", message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when synchronization operations fail.
|
/// Exception thrown when synchronization operations fail.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncException : CBDDCException
|
public class SyncException : CBDDCException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncException"/> class.
|
/// Initializes a new instance of the <see cref="SyncException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public SyncException(string message)
|
public SyncException(string message)
|
||||||
: base("SYNC_ERROR", message) { }
|
: base("SYNC_ERROR", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncException"/> class.
|
/// Initializes a new instance of the <see cref="SyncException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
/// <param name="innerException">The exception that caused the current exception.</param>
|
/// <param name="innerException">The exception that caused the current exception.</param>
|
||||||
public SyncException(string message, Exception innerException)
|
public SyncException(string message, Exception innerException)
|
||||||
: base("SYNC_ERROR", message, innerException) { }
|
: base("SYNC_ERROR", message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when configuration is invalid.
|
/// Exception thrown when configuration is invalid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ConfigurationException : CBDDCException
|
public class ConfigurationException : CBDDCException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ConfigurationException"/> class.
|
/// Initializes a new instance of the <see cref="ConfigurationException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public ConfigurationException(string message)
|
public ConfigurationException(string message)
|
||||||
: base("CONFIG_ERROR", message) { }
|
: base("CONFIG_ERROR", message)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when database corruption is detected.
|
/// Exception thrown when database corruption is detected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DatabaseCorruptionException : PersistenceException
|
public class DatabaseCorruptionException : PersistenceException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DatabaseCorruptionException"/> class.
|
/// Initializes a new instance of the <see cref="DatabaseCorruptionException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public DatabaseCorruptionException(string message)
|
public DatabaseCorruptionException(string message)
|
||||||
: base(message) { }
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DatabaseCorruptionException"/> class.
|
/// Initializes a new instance of the <see cref="DatabaseCorruptionException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
/// <param name="innerException">The exception that caused the current exception.</param>
|
/// <param name="innerException">The exception that caused the current exception.</param>
|
||||||
public DatabaseCorruptionException(string message, Exception innerException)
|
public DatabaseCorruptionException(string message, Exception innerException)
|
||||||
: base(message, innerException) { }
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when a timeout occurs.
|
/// Exception thrown when a timeout occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TimeoutException : CBDDCException
|
public class TimeoutException : CBDDCException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TimeoutException"/> class.
|
/// Initializes a new instance of the <see cref="TimeoutException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="operation">The operation that timed out.</param>
|
/// <param name="operation">The operation that timed out.</param>
|
||||||
/// <param name="timeoutMs">The timeout in milliseconds.</param>
|
/// <param name="timeoutMs">The timeout in milliseconds.</param>
|
||||||
public TimeoutException(string operation, int timeoutMs)
|
public TimeoutException(string operation, int timeoutMs)
|
||||||
: base("TIMEOUT_ERROR", $"Operation '{operation}' timed out after {timeoutMs}ms") { }
|
: base("TIMEOUT_ERROR", $"Operation '{operation}' timed out after {timeoutMs}ms")
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when a document is not found in a collection.
|
/// Exception thrown when a document is not found in a collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocumentNotFoundException : PersistenceException
|
public class DocumentNotFoundException : PersistenceException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the document key that was not found.
|
/// Initializes a new instance of the <see cref="DocumentNotFoundException" /> class.
|
||||||
/// </summary>
|
|
||||||
public string Key { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the collection where the document was searched.
|
|
||||||
/// </summary>
|
|
||||||
public string Collection { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DocumentNotFoundException"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection where the document was searched.</param>
|
/// <param name="collection">The collection where the document was searched.</param>
|
||||||
/// <param name="key">The document key that was not found.</param>
|
/// <param name="key">The document key that was not found.</param>
|
||||||
@@ -174,16 +183,28 @@ public class DocumentNotFoundException : PersistenceException
|
|||||||
Collection = collection;
|
Collection = collection;
|
||||||
Key = key;
|
Key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document key that was not found.
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection where the document was searched.
|
||||||
|
/// </summary>
|
||||||
|
public string Collection { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown when a concurrency conflict occurs during persistence operations.
|
/// Exception thrown when a concurrency conflict occurs during persistence operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCConcurrencyException : PersistenceException
|
public class CBDDCConcurrencyException : PersistenceException
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CBDDCConcurrencyException"/> class.
|
/// Initializes a new instance of the <see cref="CBDDCConcurrencyException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
public CBDDCConcurrencyException(string message) : base(message) { }
|
public CBDDCConcurrencyException(string message) : base(message)
|
||||||
}
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Hybrid Logical Clock timestamp.
|
/// Represents a Hybrid Logical Clock timestamp.
|
||||||
/// Provides a Total Ordering of events in a distributed system.
|
/// Provides a Total Ordering of events in a distributed system.
|
||||||
/// Implements value semantics and comparable interfaces.
|
/// Implements value semantics and comparable interfaces.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable, IEquatable<HlcTimestamp>
|
public readonly struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable, IEquatable<HlcTimestamp>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the physical time component of the timestamp.
|
/// Gets the physical time component of the timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long PhysicalTime { get; }
|
public long PhysicalTime { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the logical counter component used to order events with equal physical time.
|
/// Gets the logical counter component used to order events with equal physical time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int LogicalCounter { get; }
|
public int LogicalCounter { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the node identifier that produced this timestamp.
|
/// Gets the node identifier that produced this timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; }
|
public string NodeId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HlcTimestamp"/> struct.
|
/// Initializes a new instance of the <see cref="HlcTimestamp" /> struct.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="physicalTime">The physical time component.</param>
|
/// <param name="physicalTime">The physical time component.</param>
|
||||||
/// <param name="logicalCounter">The logical counter component.</param>
|
/// <param name="logicalCounter">The logical counter component.</param>
|
||||||
@@ -35,36 +35,36 @@ public readonly struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable, IE
|
|||||||
PhysicalTime = physicalTime;
|
PhysicalTime = physicalTime;
|
||||||
LogicalCounter = logicalCounter;
|
LogicalCounter = logicalCounter;
|
||||||
NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId));
|
NodeId = nodeId ?? throw new ArgumentNullException(nameof(nodeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compares two timestamps to establish a total order.
|
/// Compares two timestamps to establish a total order.
|
||||||
/// Order: PhysicalTime -> LogicalCounter -> NodeId (lexicographical tie-breaker).
|
/// Order: PhysicalTime -> LogicalCounter -> NodeId (lexicographical tie-breaker).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The other timestamp to compare with this instance.</param>
|
/// <param name="other">The other timestamp to compare with this instance.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A value less than zero if this instance is earlier than <paramref name="other"/>, zero if they are equal,
|
/// A value less than zero if this instance is earlier than <paramref name="other" />, zero if they are equal,
|
||||||
/// or greater than zero if this instance is later than <paramref name="other"/>.
|
/// or greater than zero if this instance is later than <paramref name="other" />.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public int CompareTo(HlcTimestamp other)
|
public int CompareTo(HlcTimestamp other)
|
||||||
{
|
{
|
||||||
int timeComparison = PhysicalTime.CompareTo(other.PhysicalTime);
|
int timeComparison = PhysicalTime.CompareTo(other.PhysicalTime);
|
||||||
if (timeComparison != 0) return timeComparison;
|
if (timeComparison != 0) return timeComparison;
|
||||||
|
|
||||||
int counterComparison = LogicalCounter.CompareTo(other.LogicalCounter);
|
int counterComparison = LogicalCounter.CompareTo(other.LogicalCounter);
|
||||||
if (counterComparison != 0) return counterComparison;
|
if (counterComparison != 0) return counterComparison;
|
||||||
|
|
||||||
// Use Ordinal comparison for consistent tie-breaking across cultures/platforms
|
// Use Ordinal comparison for consistent tie-breaking across cultures/platforms
|
||||||
return string.Compare(NodeId, other.NodeId, StringComparison.Ordinal);
|
return string.Compare(NodeId, other.NodeId, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compares this instance with another object.
|
/// Compares this instance with another object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="obj">The object to compare with this instance.</param>
|
/// <param name="obj">The object to compare with this instance.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A value less than zero if this instance is earlier than <paramref name="obj"/>, zero if equal, or greater
|
/// A value less than zero if this instance is earlier than <paramref name="obj" />, zero if equal, or greater
|
||||||
/// than zero if later.
|
/// than zero if later.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public int CompareTo(object? obj)
|
public int CompareTo(object? obj)
|
||||||
{
|
{
|
||||||
@@ -74,10 +74,10 @@ public readonly struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable, IE
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether this instance and another timestamp are equal.
|
/// Determines whether this instance and another timestamp are equal.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The other timestamp to compare.</param>
|
/// <param name="other">The other timestamp to compare.</param>
|
||||||
/// <returns><see langword="true"/> if the timestamps are equal; otherwise, <see langword="false"/>.</returns>
|
/// <returns><see langword="true" /> if the timestamps are equal; otherwise, <see langword="false" />.</returns>
|
||||||
public bool Equals(HlcTimestamp other)
|
public bool Equals(HlcTimestamp other)
|
||||||
{
|
{
|
||||||
return PhysicalTime == other.PhysicalTime &&
|
return PhysicalTime == other.PhysicalTime &&
|
||||||
@@ -96,42 +96,68 @@ public readonly struct HlcTimestamp : IComparable<HlcTimestamp>, IComparable, IE
|
|||||||
{
|
{
|
||||||
unchecked
|
unchecked
|
||||||
{
|
{
|
||||||
var hashCode = PhysicalTime.GetHashCode();
|
int hashCode = PhysicalTime.GetHashCode();
|
||||||
hashCode = (hashCode * 397) ^ LogicalCounter;
|
hashCode = (hashCode * 397) ^ LogicalCounter;
|
||||||
// Ensure HashCode uses the same comparison logic as Equals/CompareTo
|
// Ensure HashCode uses the same comparison logic as Equals/CompareTo
|
||||||
// Handle null NodeId gracefully (possible via default(HlcTimestamp))
|
// Handle null NodeId gracefully (possible via default(HlcTimestamp))
|
||||||
hashCode = (hashCode * 397) ^ (NodeId != null ? StringComparer.Ordinal.GetHashCode(NodeId) : 0);
|
hashCode = (hashCode * 397) ^ (NodeId != null ? StringComparer.Ordinal.GetHashCode(NodeId) : 0);
|
||||||
return hashCode;
|
return hashCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool operator ==(HlcTimestamp left, HlcTimestamp right) => left.Equals(right);
|
public static bool operator ==(HlcTimestamp left, HlcTimestamp right)
|
||||||
public static bool operator !=(HlcTimestamp left, HlcTimestamp right) => !left.Equals(right);
|
{
|
||||||
|
return left.Equals(right);
|
||||||
// Standard comparison operators making usage in SyncOrchestrator cleaner (e.g., remote > local)
|
}
|
||||||
public static bool operator <(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) < 0;
|
|
||||||
public static bool operator <=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) <= 0;
|
public static bool operator !=(HlcTimestamp left, HlcTimestamp right)
|
||||||
public static bool operator >(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) > 0;
|
{
|
||||||
public static bool operator >=(HlcTimestamp left, HlcTimestamp right) => left.CompareTo(right) >= 0;
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard comparison operators making usage in SyncOrchestrator cleaner (e.g., remote > local)
|
||||||
|
public static bool operator <(HlcTimestamp left, HlcTimestamp right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator <=(HlcTimestamp left, HlcTimestamp right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >(HlcTimestamp left, HlcTimestamp right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >=(HlcTimestamp left, HlcTimestamp right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString() => FormattableString.Invariant($"{PhysicalTime}:{LogicalCounter}:{NodeId}");
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return FormattableString.Invariant($"{PhysicalTime}:{LogicalCounter}:{NodeId}");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a timestamp string.
|
/// Parses a timestamp string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="s">The string to parse, in the format "PhysicalTime:LogicalCounter:NodeId".</param>
|
/// <param name="s">The string to parse, in the format "PhysicalTime:LogicalCounter:NodeId".</param>
|
||||||
/// <returns>The parsed <see cref="HlcTimestamp"/>.</returns>
|
/// <returns>The parsed <see cref="HlcTimestamp" />.</returns>
|
||||||
public static HlcTimestamp Parse(string s)
|
public static HlcTimestamp Parse(string s)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(s)) throw new ArgumentNullException(nameof(s));
|
if (string.IsNullOrEmpty(s)) throw new ArgumentNullException(nameof(s));
|
||||||
var parts = s.Split(':');
|
string[] parts = s.Split(':');
|
||||||
if (parts.Length != 3) throw new FormatException("Invalid HlcTimestamp format. Expected 'PhysicalTime:LogicalCounter:NodeId'.");
|
if (parts.Length != 3)
|
||||||
if (!long.TryParse(parts[0], out var physicalTime))
|
throw new FormatException("Invalid HlcTimestamp format. Expected 'PhysicalTime:LogicalCounter:NodeId'.");
|
||||||
throw new FormatException("Invalid PhysicalTime component in HlcTimestamp.");
|
if (!long.TryParse(parts[0], out long physicalTime))
|
||||||
if (!int.TryParse(parts[1], out var logicalCounter))
|
throw new FormatException("Invalid PhysicalTime component in HlcTimestamp.");
|
||||||
throw new FormatException("Invalid LogicalCounter component in HlcTimestamp.");
|
if (!int.TryParse(parts[1], out int logicalCounter))
|
||||||
var nodeId = parts[2];
|
throw new FormatException("Invalid LogicalCounter component in HlcTimestamp.");
|
||||||
return new HlcTimestamp(physicalTime, logicalCounter, nodeId);
|
string nodeId = parts[2];
|
||||||
}
|
return new HlcTimestamp(physicalTime, logicalCounter, nodeId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ using ZB.MOM.WW.CBDDC.Core.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Management;
|
namespace ZB.MOM.WW.CBDDC.Core.Management;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for managing remote peer configurations.
|
/// Service for managing remote peer configurations.
|
||||||
/// Provides CRUD operations for adding, removing, enabling/disabling remote cloud nodes.
|
/// Provides CRUD operations for adding, removing, enabling/disabling remote cloud nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPeerManagementService
|
public interface IPeerManagementService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a static remote peer with simple authentication.
|
/// Adds a static remote peer with simple authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">Unique identifier for the remote peer.</param>
|
/// <param name="nodeId">Unique identifier for the remote peer.</param>
|
||||||
/// <param name="address">Network address (hostname:port) of the remote peer.</param>
|
/// <param name="address">Network address (hostname:port) of the remote peer.</param>
|
||||||
@@ -20,14 +20,14 @@ public interface IPeerManagementService
|
|||||||
Task AddStaticPeerAsync(string nodeId, string address, CancellationToken cancellationToken = default);
|
Task AddStaticPeerAsync(string nodeId, string address, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a remote peer configuration.
|
/// Removes a remote peer configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">Unique identifier of the peer to remove.</param>
|
/// <param name="nodeId">Unique identifier of the peer to remove.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes confirmation tracking for a peer and optionally removes static remote configuration.
|
/// Removes confirmation tracking for a peer and optionally removes static remote configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">Unique identifier of the peer to untrack.</param>
|
/// <param name="nodeId">Unique identifier of the peer to untrack.</param>
|
||||||
/// <param name="removeRemoteConfig">When true, also removes static remote peer configuration.</param>
|
/// <param name="removeRemoteConfig">When true, also removes static remote peer configuration.</param>
|
||||||
@@ -38,23 +38,23 @@ public interface IPeerManagementService
|
|||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all configured remote peers.
|
/// Retrieves all configured remote peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Collection of remote peer configurations.</returns>
|
/// <returns>Collection of remote peer configurations.</returns>
|
||||||
Task<IEnumerable<RemotePeerConfiguration>> GetAllRemotePeersAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<RemotePeerConfiguration>> GetAllRemotePeersAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables synchronization with a remote peer.
|
/// Enables synchronization with a remote peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">Unique identifier of the peer to enable.</param>
|
/// <param name="nodeId">Unique identifier of the peer to enable.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task EnablePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task EnablePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disables synchronization with a remote peer (keeps configuration).
|
/// Disables synchronization with a remote peer (keeps configuration).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">Unique identifier of the peer to disable.</param>
|
/// <param name="nodeId">Unique identifier of the peer to disable.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task DisablePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task DisablePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -2,29 +2,28 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Management;
|
namespace ZB.MOM.WW.CBDDC.Core.Management;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implementation of peer management service.
|
/// Implementation of peer management service.
|
||||||
/// Provides CRUD operations for managing remote peer configurations.
|
/// Provides CRUD operations for managing remote peer configurations.
|
||||||
///
|
/// Remote peer configurations are stored in a synchronized collection and automatically
|
||||||
/// Remote peer configurations are stored in a synchronized collection and automatically
|
/// replicated across all nodes in the cluster. Any change made on one node will be
|
||||||
/// replicated across all nodes in the cluster. Any change made on one node will be
|
/// synchronized to all other nodes through the normal CBDDC sync process.
|
||||||
/// synchronized to all other nodes through the normal CBDDC sync process.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PeerManagementService : IPeerManagementService
|
public class PeerManagementService : IPeerManagementService
|
||||||
{
|
{
|
||||||
private readonly IPeerConfigurationStore _store;
|
|
||||||
private readonly IPeerOplogConfirmationStore _peerOplogConfirmationStore;
|
|
||||||
private readonly ILogger<PeerManagementService> _logger;
|
private readonly ILogger<PeerManagementService> _logger;
|
||||||
|
private readonly IPeerOplogConfirmationStore _peerOplogConfirmationStore;
|
||||||
|
private readonly IPeerConfigurationStore _store;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the PeerManagementService class.
|
/// Initializes a new instance of the PeerManagementService class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="store">Database instance for accessing the synchronized collection.</param>
|
/// <param name="store">Database instance for accessing the synchronized collection.</param>
|
||||||
/// <param name="peerOplogConfirmationStore">Peer confirmation tracking store.</param>
|
/// <param name="peerOplogConfirmationStore">Peer confirmation tracking store.</param>
|
||||||
@@ -35,12 +34,13 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
ILogger<PeerManagementService>? logger = null)
|
ILogger<PeerManagementService>? logger = null)
|
||||||
{
|
{
|
||||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||||
_peerOplogConfirmationStore = peerOplogConfirmationStore ?? throw new ArgumentNullException(nameof(peerOplogConfirmationStore));
|
_peerOplogConfirmationStore = peerOplogConfirmationStore ??
|
||||||
|
throw new ArgumentNullException(nameof(peerOplogConfirmationStore));
|
||||||
_logger = logger ?? NullLogger<PeerManagementService>.Instance;
|
_logger = logger ?? NullLogger<PeerManagementService>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds or updates a static remote peer configuration.
|
/// Adds or updates a static remote peer configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique node identifier of the peer.</param>
|
/// <param name="nodeId">The unique node identifier of the peer.</param>
|
||||||
/// <param name="address">The peer network address in host:port format.</param>
|
/// <param name="address">The peer network address in host:port format.</param>
|
||||||
@@ -60,22 +60,23 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
};
|
};
|
||||||
|
|
||||||
await _store.SaveRemotePeerAsync(config, cancellationToken);
|
await _store.SaveRemotePeerAsync(config, cancellationToken);
|
||||||
_logger.LogInformation("Added static remote peer: {NodeId} at {Address} (will sync to all cluster nodes)", nodeId, address);
|
_logger.LogInformation("Added static remote peer: {NodeId} at {Address} (will sync to all cluster nodes)",
|
||||||
|
nodeId, address);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a remote peer configuration.
|
/// Removes a remote peer configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique node identifier of the peer to remove.</param>
|
/// <param name="nodeId">The unique node identifier of the peer to remove.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
public async Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default)
|
public async Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await RemovePeerTrackingAsync(nodeId, removeRemoteConfig: true, cancellationToken);
|
await RemovePeerTrackingAsync(nodeId, true, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes peer tracking and optionally removes remote peer configuration.
|
/// Removes peer tracking and optionally removes remote peer configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique node identifier of the peer to untrack.</param>
|
/// <param name="nodeId">The unique node identifier of the peer to untrack.</param>
|
||||||
/// <param name="removeRemoteConfig">When true, also removes static remote peer configuration.</param>
|
/// <param name="removeRemoteConfig">When true, also removes static remote peer configuration.</param>
|
||||||
@@ -93,7 +94,8 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
if (removeRemoteConfig)
|
if (removeRemoteConfig)
|
||||||
{
|
{
|
||||||
await _store.RemoveRemotePeerAsync(nodeId, cancellationToken);
|
await _store.RemoveRemotePeerAsync(nodeId, cancellationToken);
|
||||||
_logger.LogInformation("Removed remote peer and tracking: {NodeId} (will sync to all cluster nodes)", nodeId);
|
_logger.LogInformation("Removed remote peer and tracking: {NodeId} (will sync to all cluster nodes)",
|
||||||
|
nodeId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,17 +103,18 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all configured remote peers.
|
/// Gets all configured remote peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result contains remote peer configurations.</returns>
|
/// <returns>A task that represents the asynchronous operation. The task result contains remote peer configurations.</returns>
|
||||||
public async Task<IEnumerable<RemotePeerConfiguration>> GetAllRemotePeersAsync(CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<RemotePeerConfiguration>> GetAllRemotePeersAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await _store.GetRemotePeersAsync(cancellationToken);
|
return await _store.GetRemotePeersAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enables a configured remote peer.
|
/// Enables a configured remote peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique node identifier of the peer to enable.</param>
|
/// <param name="nodeId">The unique node identifier of the peer to enable.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
@@ -122,10 +125,7 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
|
|
||||||
var peer = await _store.GetRemotePeerAsync(nodeId, cancellationToken);
|
var peer = await _store.GetRemotePeerAsync(nodeId, cancellationToken);
|
||||||
|
|
||||||
if (peer == null)
|
if (peer == null) return; // Peer not found, nothing to enable
|
||||||
{
|
|
||||||
return; // Peer not found, nothing to enable
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!peer.IsEnabled)
|
if (!peer.IsEnabled)
|
||||||
{
|
{
|
||||||
@@ -136,7 +136,7 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Disables a configured remote peer.
|
/// Disables a configured remote peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique node identifier of the peer to disable.</param>
|
/// <param name="nodeId">The unique node identifier of the peer to disable.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
@@ -147,10 +147,7 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
|
|
||||||
var peer = await _store.GetRemotePeerAsync(nodeId, cancellationToken);
|
var peer = await _store.GetRemotePeerAsync(nodeId, cancellationToken);
|
||||||
|
|
||||||
if (peer == null)
|
if (peer == null) return; // Peer not found, nothing to disable
|
||||||
{
|
|
||||||
return; // Peer not found, nothing to disable
|
|
||||||
}
|
|
||||||
|
|
||||||
if (peer.IsEnabled)
|
if (peer.IsEnabled)
|
||||||
{
|
{
|
||||||
@@ -163,23 +160,16 @@ public class PeerManagementService : IPeerManagementService
|
|||||||
private static void ValidateNodeId(string nodeId)
|
private static void ValidateNodeId(string nodeId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(nodeId))
|
if (string.IsNullOrWhiteSpace(nodeId))
|
||||||
{
|
|
||||||
throw new ArgumentException("NodeId cannot be null or empty", nameof(nodeId));
|
throw new ArgumentException("NodeId cannot be null or empty", nameof(nodeId));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateAddress(string address)
|
private static void ValidateAddress(string address)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(address))
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
{
|
|
||||||
throw new ArgumentException("Address cannot be null or empty", nameof(address));
|
throw new ArgumentException("Address cannot be null or empty", nameof(address));
|
||||||
}
|
|
||||||
|
|
||||||
// Basic format validation (should contain host:port)
|
// Basic format validation (should contain host:port)
|
||||||
if (!address.Contains(':'))
|
if (!address.Contains(':'))
|
||||||
{
|
|
||||||
throw new ArgumentException("Address must be in format 'host:port'", nameof(address));
|
throw new ArgumentException("Address must be in format 'host:port'", nameof(address));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -1,38 +1,40 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a method that handles peer node configuration change notifications.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sender">The source of the event.</param>
|
|
||||||
/// <param name="newConfig">The updated peer node configuration.</param>
|
|
||||||
public delegate void PeerNodeConfigurationChangedEventHandler(object? sender, PeerNodeConfiguration newConfig);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines a contract for retrieving and monitoring configuration settings for a peer node.
|
/// Represents a method that handles peer node configuration change notifications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Implementations of this interface provide access to the current configuration and notify subscribers
|
/// <param name="sender">The source of the event.</param>
|
||||||
/// when configuration changes occur. This interface is typically used by components that require up-to-date
|
/// <param name="newConfig">The updated peer node configuration.</param>
|
||||||
/// configuration information for peer-to-peer networking scenarios.</remarks>
|
public delegate void PeerNodeConfigurationChangedEventHandler(object? sender, PeerNodeConfiguration newConfig);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a contract for retrieving and monitoring configuration settings for a peer node.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Implementations of this interface provide access to the current configuration and notify subscribers
|
||||||
|
/// when configuration changes occur. This interface is typically used by components that require up-to-date
|
||||||
|
/// configuration information for peer-to-peer networking scenarios.
|
||||||
|
/// </remarks>
|
||||||
public interface IPeerNodeConfigurationProvider
|
public interface IPeerNodeConfigurationProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the current configuration settings for the peer node.
|
/// Asynchronously retrieves the current configuration settings for the peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A task that represents the asynchronous operation. The task result contains the current
|
/// A task that represents the asynchronous operation. The task result contains the current
|
||||||
/// <see cref="PeerNodeConfiguration"/>.
|
/// <see cref="PeerNodeConfiguration" />.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public Task<PeerNodeConfiguration> GetConfiguration();
|
public Task<PeerNodeConfiguration> GetConfiguration();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the configuration of the peer node changes.
|
/// Occurs when the configuration of the peer node changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Subscribe to this event to be notified when any configuration settings for the peer node are
|
/// <remarks>
|
||||||
/// modified. Event handlers can use this notification to update dependent components or respond to configuration
|
/// Subscribe to this event to be notified when any configuration settings for the peer node are
|
||||||
/// changes as needed.</remarks>
|
/// modified. Event handlers can use this notification to update dependent components or respond to configuration
|
||||||
|
/// changes as needed.
|
||||||
|
/// </remarks>
|
||||||
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the role of a node in the distributed network cluster.
|
/// Defines the role of a node in the distributed network cluster.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum NodeRole
|
public enum NodeRole
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Standard member node that synchronizes only within the local area network.
|
/// Standard member node that synchronizes only within the local area network.
|
||||||
/// Does not connect to cloud remote nodes.
|
/// Does not connect to cloud remote nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Member = 0,
|
Member = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Leader node that acts as a gateway to cloud remote nodes.
|
/// Leader node that acts as a gateway to cloud remote nodes.
|
||||||
/// Elected via the Bully algorithm (lexicographically smallest NodeId).
|
/// Elected via the Bully algorithm (lexicographically smallest NodeId).
|
||||||
/// Responsible for synchronizing local cluster changes with cloud nodes.
|
/// Responsible for synchronizing local cluster changes with cloud nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CloudGateway = 1
|
CloudGateway = 1
|
||||||
}
|
}
|
||||||
@@ -1,53 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a peer node in a distributed network, including its unique identifier, network address, and last seen
|
/// Represents a peer node in a distributed network, including its unique identifier, network address, and last seen
|
||||||
/// timestamp.
|
/// timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PeerNode
|
public class PeerNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the unique identifier for the node.
|
/// Initializes a new instance of the PeerNode class with the specified node identifier, network address, and last
|
||||||
/// </summary>
|
/// seen timestamp.
|
||||||
public string NodeId { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the address associated with the current instance.
|
|
||||||
/// </summary>
|
|
||||||
public string Address { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the date and time when the entity was last observed or updated.
|
|
||||||
/// </summary>
|
|
||||||
public DateTimeOffset LastSeen { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the configuration settings for the peer node.
|
|
||||||
/// </summary>
|
|
||||||
public PeerNodeConfiguration? Configuration { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the type of the peer node (LanDiscovered, StaticRemote, or CloudRemote).
|
|
||||||
/// </summary>
|
|
||||||
public PeerType Type { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the role assigned to this node within the cluster.
|
|
||||||
/// </summary>
|
|
||||||
public NodeRole Role { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of collections this peer is interested in.
|
|
||||||
/// </summary>
|
|
||||||
public System.Collections.Generic.IReadOnlyList<string> InterestingCollections { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the PeerNode class with the specified node identifier, network address, and last
|
|
||||||
/// seen timestamp.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier for the peer node. Cannot be null or empty.</param>
|
/// <param name="nodeId">The unique identifier for the peer node. Cannot be null or empty.</param>
|
||||||
/// <param name="address">The network address of the peer node. Cannot be null or empty.</param>
|
/// <param name="address">The network address of the peer node. Cannot be null or empty.</param>
|
||||||
@@ -57,10 +21,10 @@ public class PeerNode
|
|||||||
/// <param name="configuration">The peer node configuration</param>
|
/// <param name="configuration">The peer node configuration</param>
|
||||||
/// <param name="interestingCollections">The list of collections this peer is interested in.</param>
|
/// <param name="interestingCollections">The list of collections this peer is interested in.</param>
|
||||||
public PeerNode(
|
public PeerNode(
|
||||||
string nodeId,
|
string nodeId,
|
||||||
string address,
|
string address,
|
||||||
DateTimeOffset lastSeen,
|
DateTimeOffset lastSeen,
|
||||||
PeerType type = PeerType.LanDiscovered,
|
PeerType type = PeerType.LanDiscovered,
|
||||||
NodeRole role = NodeRole.Member,
|
NodeRole role = NodeRole.Member,
|
||||||
PeerNodeConfiguration? configuration = null,
|
PeerNodeConfiguration? configuration = null,
|
||||||
IEnumerable<string>? interestingCollections = null)
|
IEnumerable<string>? interestingCollections = null)
|
||||||
@@ -73,4 +37,39 @@ public class PeerNode
|
|||||||
Configuration = configuration;
|
Configuration = configuration;
|
||||||
InterestingCollections = new List<string>(interestingCollections ?? []).AsReadOnly();
|
InterestingCollections = new List<string>(interestingCollections ?? []).AsReadOnly();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the unique identifier for the node.
|
||||||
|
/// </summary>
|
||||||
|
public string NodeId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address associated with the current instance.
|
||||||
|
/// </summary>
|
||||||
|
public string Address { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the date and time when the entity was last observed or updated.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset LastSeen { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configuration settings for the peer node.
|
||||||
|
/// </summary>
|
||||||
|
public PeerNodeConfiguration? Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the type of the peer node (LanDiscovered, StaticRemote, or CloudRemote).
|
||||||
|
/// </summary>
|
||||||
|
public PeerType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the role assigned to this node within the cluster.
|
||||||
|
/// </summary>
|
||||||
|
public NodeRole Role { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of collections this peer is interested in.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> InterestingCollections { get; }
|
||||||
|
}
|
||||||
@@ -1,96 +1,101 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the configuration settings for a peer node in a distributed network.
|
/// Represents the configuration settings for a peer node in a distributed network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Use this class to specify identification, network port, and authentication details required for a
|
/// <remarks>
|
||||||
/// peer node to participate in a cluster or peer-to-peer environment. The <see cref="Default"/> property provides a
|
/// Use this class to specify identification, network port, and authentication details required for a
|
||||||
/// basic configuration suitable for development or testing scenarios.</remarks>
|
/// peer node to participate in a cluster or peer-to-peer environment. The <see cref="Default" /> property provides a
|
||||||
|
/// basic configuration suitable for development or testing scenarios.
|
||||||
|
/// </remarks>
|
||||||
public class PeerNodeConfiguration
|
public class PeerNodeConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier for the node.
|
/// Gets or sets the unique identifier for the node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; set; } = string.Empty;
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the TCP port number used for network communication.
|
/// Gets or sets the TCP port number used for network communication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TcpPort { get; set; }
|
public int TcpPort { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the authentication token used to authorize API requests.
|
/// Gets or sets the authentication token used to authorize API requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AuthToken { get; set; } = string.Empty;
|
public string AuthToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum size of the document cache items. Default: 10.
|
/// Maximum size of the document cache items. Default: 10.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxDocumentCacheSize { get; set; } = 100;
|
public int MaxDocumentCacheSize { get; set; } = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum size of offline queue. Default: 1000.
|
/// Maximum size of offline queue. Default: 1000.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaxQueueSize { get; set; } = 1000;
|
public int MaxQueueSize { get; set; } = 1000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Number of retry attempts for failed network operations. Default: 3.
|
/// Number of retry attempts for failed network operations. Default: 3.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int RetryAttempts { get; set; } = 3;
|
public int RetryAttempts { get; set; } = 3;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delay between retry attempts in milliseconds. Default: 1000ms.
|
/// Delay between retry attempts in milliseconds. Default: 1000ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int RetryDelayMs { get; set; } = 1000;
|
public int RetryDelayMs { get; set; } = 1000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interval between periodic maintenance operations (Oplog pruning) in minutes. Default: 60 minutes.
|
/// Interval between periodic maintenance operations (Oplog pruning) in minutes. Default: 60 minutes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int MaintenanceIntervalMinutes { get; set; } = 60;
|
public int MaintenanceIntervalMinutes { get; set; } = 60;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Oplog retention period in hours. Entries older than this will be pruned. Default: 24 hours.
|
/// Oplog retention period in hours. Entries older than this will be pruned. Default: 24 hours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int OplogRetentionHours { get; set; } = 24;
|
public int OplogRetentionHours { get; set; } = 24;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a list of known peers to connect to directly, bypassing discovery.
|
/// Gets or sets a list of known peers to connect to directly, bypassing discovery.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public System.Collections.Generic.List<KnownPeerConfiguration> KnownPeers { get; set; } = new();
|
public List<KnownPeerConfiguration> KnownPeers { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the default configuration settings for a peer node.
|
/// Gets the default configuration settings for a peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Each access returns a new instance of the configuration with a unique node identifier. The
|
/// <remarks>
|
||||||
/// default settings use TCP port 9000 and a generated authentication token. Modify the returned instance as needed
|
/// Each access returns a new instance of the configuration with a unique node identifier. The
|
||||||
/// before use.</remarks>
|
/// default settings use TCP port 9000 and a generated authentication token. Modify the returned instance as needed
|
||||||
public static PeerNodeConfiguration Default => new PeerNodeConfiguration
|
/// before use.
|
||||||
{
|
/// </remarks>
|
||||||
NodeId = Guid.NewGuid().ToString(),
|
public static PeerNodeConfiguration Default => new()
|
||||||
TcpPort = 9000,
|
{
|
||||||
AuthToken = Guid.NewGuid().ToString("N")
|
NodeId = Guid.NewGuid().ToString(),
|
||||||
};
|
TcpPort = 9000,
|
||||||
|
AuthToken = Guid.NewGuid().ToString("N")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration for a known peer node.
|
/// Configuration for a known peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class KnownPeerConfiguration
|
public class KnownPeerConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The unique identifier of the peer node.
|
/// The unique identifier of the peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; set; } = string.Empty;
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The hostname or IP address of the peer.
|
/// The hostname or IP address of the peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Host { get; set; } = string.Empty;
|
public string Host { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The TCP port of the peer.
|
/// The TCP port of the peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the type of peer node in the distributed network.
|
/// Defines the type of peer node in the distributed network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum PeerType
|
public enum PeerType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Peer discovered via UDP broadcast on the local area network.
|
/// Peer discovered via UDP broadcast on the local area network.
|
||||||
/// These peers are ephemeral and removed after timeout when no longer broadcasting.
|
/// These peers are ephemeral and removed after timeout when no longer broadcasting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
LanDiscovered = 0,
|
LanDiscovered = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Peer manually configured with a static address.
|
/// Peer manually configured with a static address.
|
||||||
/// These peers are persistent across restarts and stored in the database.
|
/// These peers are persistent across restarts and stored in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
StaticRemote = 1,
|
StaticRemote = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cloud remote node.
|
/// Cloud remote node.
|
||||||
/// Always active if internet connectivity is available.
|
/// Always active if internet connectivity is available.
|
||||||
/// Synchronized only by the elected leader node to reduce overhead.
|
/// Synchronized only by the elected leader node to reduce overhead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
CloudRemote = 2
|
CloudRemote = 2
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration for a remote peer node that is persistent across restarts.
|
/// Configuration for a remote peer node that is persistent across restarts.
|
||||||
/// This collection is automatically synchronized across all nodes in the cluster.
|
/// This collection is automatically synchronized across all nodes in the cluster.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RemotePeerConfiguration
|
public class RemotePeerConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the unique identifier for the remote peer node.
|
/// Gets or sets the unique identifier for the remote peer node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the network address of the remote peer (hostname:port).
|
/// Gets or sets the network address of the remote peer (hostname:port).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Address { get; set; } = "";
|
public string Address { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type of the peer (StaticRemote or CloudRemote).
|
/// Gets or sets the type of the peer (StaticRemote or CloudRemote).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public PeerType Type { get; set; }
|
public PeerType Type { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether this peer is enabled for synchronization.
|
/// Gets or sets whether this peer is enabled for synchronization.
|
||||||
/// Disabled peers are stored but not used for sync.
|
/// Disabled peers are stored but not used for sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of collections this peer is interested in.
|
/// Gets or sets the list of collections this peer is interested in.
|
||||||
/// If empty, the peer is interested in all collections.
|
/// If empty, the peer is interested in all collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public System.Collections.Generic.List<string> InterestingCollections { get; set; } = new();
|
public List<string> InterestingCollections { get; set; } = new();
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,16 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
namespace ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides peer node configuration from an in-memory static source.
|
/// Provides peer node configuration from an in-memory static source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
|
public class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvider
|
||||||
{
|
{
|
||||||
private PeerNodeConfiguration _configuration = new();
|
private PeerNodeConfiguration _configuration = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current peer node configuration.
|
/// Initializes a new instance of the <see cref="StaticPeerNodeConfigurationProvider" /> class.
|
||||||
/// </summary>
|
|
||||||
public PeerNodeConfiguration Configuration
|
|
||||||
{
|
|
||||||
get => _configuration;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_configuration != value)
|
|
||||||
{
|
|
||||||
_configuration = value;
|
|
||||||
OnConfigurationChanged(_configuration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="StaticPeerNodeConfigurationProvider"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The initial peer node configuration.</param>
|
/// <param name="configuration">The initial peer node configuration.</param>
|
||||||
public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration)
|
public StaticPeerNodeConfigurationProvider(PeerNodeConfiguration configuration)
|
||||||
@@ -35,12 +19,28 @@ public class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the peer node configuration changes.
|
/// Gets or sets the current peer node configuration.
|
||||||
|
/// </summary>
|
||||||
|
public PeerNodeConfiguration Configuration
|
||||||
|
{
|
||||||
|
get => _configuration;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_configuration != value)
|
||||||
|
{
|
||||||
|
_configuration = value;
|
||||||
|
OnConfigurationChanged(_configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when the peer node configuration changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
public event PeerNodeConfigurationChangedEventHandler? ConfigurationChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current peer node configuration.
|
/// Gets the current peer node configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task whose result is the current configuration.</returns>
|
/// <returns>A task whose result is the current configuration.</returns>
|
||||||
public Task<PeerNodeConfiguration> GetConfiguration()
|
public Task<PeerNodeConfiguration> GetConfiguration()
|
||||||
@@ -49,11 +49,11 @@ public class StaticPeerNodeConfigurationProvider : IPeerNodeConfigurationProvide
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Raises the <see cref="ConfigurationChanged"/> event.
|
/// Raises the <see cref="ConfigurationChanged" /> event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="newConfig">The new peer node configuration.</param>
|
/// <param name="newConfig">The new peer node configuration.</param>
|
||||||
protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig)
|
protected virtual void OnConfigurationChanged(PeerNodeConfiguration newConfig)
|
||||||
{
|
{
|
||||||
ConfigurationChanged?.Invoke(this, newConfig);
|
ConfigurationChanged?.Invoke(this, newConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.Globalization;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
@@ -10,84 +12,56 @@ public enum OperationType
|
|||||||
Delete
|
Delete
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class OplogEntryExtensions
|
public static class OplogEntryExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes a deterministic hash for the specified oplog entry.
|
/// Computes a deterministic hash for the specified oplog entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entry">The oplog entry to hash.</param>
|
/// <param name="entry">The oplog entry to hash.</param>
|
||||||
/// <returns>The lowercase hexadecimal SHA-256 hash of the entry.</returns>
|
/// <returns>The lowercase hexadecimal SHA-256 hash of the entry.</returns>
|
||||||
public static string ComputeHash(this OplogEntry entry)
|
public static string ComputeHash(this OplogEntry entry)
|
||||||
{
|
{
|
||||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
using var sha256 = SHA256.Create();
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
sb.Append(entry.Collection);
|
sb.Append(entry.Collection);
|
||||||
sb.Append('|');
|
sb.Append('|');
|
||||||
sb.Append(entry.Key);
|
sb.Append(entry.Key);
|
||||||
sb.Append('|');
|
sb.Append('|');
|
||||||
// Ensure stable string representation for Enum (integer value)
|
// Ensure stable string representation for Enum (integer value)
|
||||||
sb.Append(((int)entry.Operation).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
sb.Append(((int)entry.Operation).ToString(CultureInfo.InvariantCulture));
|
||||||
sb.Append('|');
|
sb.Append('|');
|
||||||
// Payload excluded from hash to avoid serialization non-determinism
|
// Payload excluded from hash to avoid serialization non-determinism
|
||||||
// sb.Append(entry.Payload...);
|
// sb.Append(entry.Payload...);
|
||||||
sb.Append('|');
|
sb.Append('|');
|
||||||
// Timestamp.ToString() is now Invariant
|
// Timestamp.ToString() is now Invariant
|
||||||
sb.Append(entry.Timestamp.ToString());
|
sb.Append(entry.Timestamp.ToString());
|
||||||
sb.Append('|');
|
sb.Append('|');
|
||||||
sb.Append(entry.PreviousHash);
|
sb.Append(entry.PreviousHash);
|
||||||
|
|
||||||
var bytes = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
|
byte[] bytes = Encoding.UTF8.GetBytes(sb.ToString());
|
||||||
var hashBytes = sha256.ComputeHash(bytes);
|
byte[] hashBytes = sha256.ComputeHash(bytes);
|
||||||
|
|
||||||
// Convert to hex string
|
// Convert to hex string
|
||||||
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OplogEntry
|
public class OplogEntry
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection name associated with this entry.
|
/// Initializes a new instance of the <see cref="OplogEntry" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Collection { get; }
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <summary>
|
/// <param name="key">The document key.</param>
|
||||||
/// Gets the document key associated with this entry.
|
/// <param name="operation">The operation type.</param>
|
||||||
/// </summary>
|
/// <param name="payload">The serialized payload.</param>
|
||||||
public string Key { get; }
|
/// <param name="timestamp">The logical timestamp.</param>
|
||||||
/// <summary>
|
/// <param name="previousHash">The previous entry hash.</param>
|
||||||
/// Gets the operation represented by this entry.
|
/// <param name="hash">The current entry hash. If null, it is computed.</param>
|
||||||
/// </summary>
|
public OplogEntry(string collection, string key, OperationType operation, JsonElement? payload,
|
||||||
public OperationType Operation { get; }
|
HlcTimestamp timestamp, string previousHash, string? hash = null)
|
||||||
/// <summary>
|
{
|
||||||
/// Gets the serialized payload for the operation.
|
|
||||||
/// </summary>
|
|
||||||
public JsonElement? Payload { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the logical timestamp for this entry.
|
|
||||||
/// </summary>
|
|
||||||
public HlcTimestamp Timestamp { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the hash of this entry.
|
|
||||||
/// </summary>
|
|
||||||
public string Hash { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the hash of the previous entry in the chain.
|
|
||||||
/// </summary>
|
|
||||||
public string PreviousHash { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="OplogEntry"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="collection">The collection name.</param>
|
|
||||||
/// <param name="key">The document key.</param>
|
|
||||||
/// <param name="operation">The operation type.</param>
|
|
||||||
/// <param name="payload">The serialized payload.</param>
|
|
||||||
/// <param name="timestamp">The logical timestamp.</param>
|
|
||||||
/// <param name="previousHash">The previous entry hash.</param>
|
|
||||||
/// <param name="hash">The current entry hash. If null, it is computed.</param>
|
|
||||||
public OplogEntry(string collection, string key, OperationType operation, JsonElement? payload, HlcTimestamp timestamp, string previousHash, string? hash = null)
|
|
||||||
{
|
|
||||||
Collection = collection;
|
Collection = collection;
|
||||||
Key = key;
|
Key = key;
|
||||||
Operation = operation;
|
Operation = operation;
|
||||||
@@ -98,10 +72,45 @@ public class OplogEntry
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies if the stored Hash matches the content.
|
/// Gets the collection name associated with this entry.
|
||||||
|
/// </summary>
|
||||||
|
public string Collection { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document key associated with this entry.
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the operation represented by this entry.
|
||||||
|
/// </summary>
|
||||||
|
public OperationType Operation { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the serialized payload for the operation.
|
||||||
|
/// </summary>
|
||||||
|
public JsonElement? Payload { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logical timestamp for this entry.
|
||||||
|
/// </summary>
|
||||||
|
public HlcTimestamp Timestamp { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the hash of this entry.
|
||||||
|
/// </summary>
|
||||||
|
public string Hash { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the hash of the previous entry in the chain.
|
||||||
|
/// </summary>
|
||||||
|
public string PreviousHash { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies if the stored Hash matches the content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsValid()
|
public bool IsValid()
|
||||||
{
|
{
|
||||||
return Hash == this.ComputeHash();
|
return Hash == this.ComputeHash();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,42 +3,42 @@ using System;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a persisted confirmation watermark for a tracked peer and source node.
|
/// Represents a persisted confirmation watermark for a tracked peer and source node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PeerOplogConfirmation
|
public class PeerOplogConfirmation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the tracked peer node identifier.
|
/// Gets or sets the tracked peer node identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PeerNodeId { get; set; } = "";
|
public string PeerNodeId { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the source node identifier this confirmation applies to.
|
/// Gets or sets the source node identifier this confirmation applies to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string SourceNodeId { get; set; } = "";
|
public string SourceNodeId { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the physical wall-clock component of the confirmed HLC timestamp.
|
/// Gets or sets the physical wall-clock component of the confirmed HLC timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ConfirmedWall { get; set; }
|
public long ConfirmedWall { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the logical counter component of the confirmed HLC timestamp.
|
/// Gets or sets the logical counter component of the confirmed HLC timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int ConfirmedLogic { get; set; }
|
public int ConfirmedLogic { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the confirmed hash at the watermark.
|
/// Gets or sets the confirmed hash at the watermark.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ConfirmedHash { get; set; } = "";
|
public string ConfirmedHash { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets when this confirmation record was last updated in UTC.
|
/// Gets or sets when this confirmation record was last updated in UTC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset LastConfirmedUtc { get; set; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset LastConfirmedUtc { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether this tracked peer is active for pruning/sync gating.
|
/// Gets or sets whether this tracked peer is active for pruning/sync gating.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -1,225 +1,269 @@
|
|||||||
using System.Text.Json;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
public abstract class QueryNode
|
||||||
|
{
|
||||||
public abstract class QueryNode { }
|
}
|
||||||
|
|
||||||
public class Eq : QueryNode
|
public class Eq : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new equality query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The value to compare against.</param>
|
||||||
|
public Eq(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value to compare against.
|
/// Gets the value to compare against.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new equality query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The value to compare against.</param>
|
|
||||||
public Eq(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Gt : QueryNode
|
public class Gt : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new greater-than query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The threshold value.</param>
|
||||||
|
public Gt(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the threshold value.
|
/// Gets the threshold value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new greater-than query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The threshold value.</param>
|
|
||||||
public Gt(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Lt : QueryNode
|
public class Lt : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new less-than query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The threshold value.</param>
|
||||||
|
public Lt(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the threshold value.
|
/// Gets the threshold value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new less-than query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The threshold value.</param>
|
|
||||||
public Lt(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Gte : QueryNode
|
public class Gte : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new greater-than-or-equal query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The threshold value.</param>
|
||||||
|
public Gte(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the threshold value.
|
/// Gets the threshold value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new greater-than-or-equal query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The threshold value.</param>
|
|
||||||
public Gte(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Lte : QueryNode
|
public class Lte : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new less-than-or-equal query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The threshold value.</param>
|
||||||
|
public Lte(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the threshold value.
|
/// Gets the threshold value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new less-than-or-equal query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The threshold value.</param>
|
|
||||||
public Lte(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Neq : QueryNode
|
public class Neq : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new not-equal query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The value to compare against.</param>
|
||||||
|
public Neq(string field, object value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value to compare against.
|
/// Gets the value to compare against.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object Value { get; }
|
public object Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new not-equal query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The value to compare against.</param>
|
|
||||||
public Neq(string field, object value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class In : QueryNode
|
public class In : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new in-list query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="values">The set of values to compare against.</param>
|
||||||
|
public In(string field, object[] values)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the set of values to compare against.
|
/// Gets the set of values to compare against.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object[] Values { get; }
|
public object[] Values { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new in-list query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="values">The set of values to compare against.</param>
|
|
||||||
public In(string field, object[] values) { Field = field; Values = values; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Contains : QueryNode
|
public class Contains : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new contains query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The substring value to search for.</param>
|
||||||
|
public Contains(string field, string value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the substring value to search for.
|
/// Gets the substring value to search for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Value { get; }
|
public string Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new contains query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The substring value to search for.</param>
|
|
||||||
public Contains(string field, string value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotContains : QueryNode
|
public class NotContains : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the field name to compare.
|
/// Initializes a new not-contains query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field name to compare.</param>
|
||||||
|
/// <param name="value">The substring value to exclude.</param>
|
||||||
|
public NotContains(string field, string value)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the field name to compare.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Field { get; }
|
public string Field { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the substring value to exclude.
|
/// Gets the substring value to exclude.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Value { get; }
|
public string Value { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new not-contains query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="field">The field name to compare.</param>
|
|
||||||
/// <param name="value">The substring value to exclude.</param>
|
|
||||||
public NotContains(string field, string value) { Field = field; Value = value; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class And : QueryNode
|
public class And : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the left side of the logical operation.
|
/// Initializes a new logical AND query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">The left query node.</param>
|
||||||
|
/// <param name="right">The right query node.</param>
|
||||||
|
public And(QueryNode left, QueryNode right)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Right = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the left side of the logical operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public QueryNode Left { get; }
|
public QueryNode Left { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the right side of the logical operation.
|
/// Gets the right side of the logical operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public QueryNode Right { get; }
|
public QueryNode Right { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new logical AND query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="left">The left query node.</param>
|
|
||||||
/// <param name="right">The right query node.</param>
|
|
||||||
public And(QueryNode left, QueryNode right) { Left = left; Right = right; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Or : QueryNode
|
public class Or : QueryNode
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the left side of the logical operation.
|
/// Initializes a new logical OR query node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">The left query node.</param>
|
||||||
|
/// <param name="right">The right query node.</param>
|
||||||
|
public Or(QueryNode left, QueryNode right)
|
||||||
|
{
|
||||||
|
Left = left;
|
||||||
|
Right = right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the left side of the logical operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public QueryNode Left { get; }
|
public QueryNode Left { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the right side of the logical operation.
|
/// Gets the right side of the logical operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public QueryNode Right { get; }
|
public QueryNode Right { get; }
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new logical OR query node.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="left">The left query node.</param>
|
|
||||||
/// <param name="right">The right query node.</param>
|
|
||||||
public Or(QueryNode left, QueryNode right) { Left = left; Right = right; }
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,9 @@ Core abstractions and logic for **CBDDC**, a peer-to-peer data synchronization m
|
|||||||
|
|
||||||
## What Is CBDDC?
|
## What Is CBDDC?
|
||||||
|
|
||||||
CBDDC is **not** a database — it's a sync layer that plugs into your existing data store (BLite) and enables automatic P2P replication across nodes in a mesh network. Your application reads and writes to its database as usual; CBDDC handles synchronization in the background.
|
CBDDC is **not** a database <EFBFBD> it's a sync layer that plugs into your existing data store (BLite) and enables automatic
|
||||||
|
P2P replication across nodes in a mesh network. Your application reads and writes to its database as usual; CBDDC
|
||||||
|
handles synchronization in the background.
|
||||||
|
|
||||||
## What's In This Package
|
## What's In This Package
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ CBDDC is **not** a database
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Pick a persistence provider
|
# Pick a persistence provider
|
||||||
dotnet add package ZB.MOM.WW.CBDDC.Persistence # Embedded document DB
|
dotnet add package ZB.MOM.WW.CBDDC.Persistence # Embedded document DB
|
||||||
|
|
||||||
# Add networking
|
# Add networking
|
||||||
dotnet add package ZB.MOM.WW.CBDDC.Network
|
dotnet add package ZB.MOM.WW.CBDDC.Network
|
||||||
@@ -65,12 +67,12 @@ builder.Services.AddCBDDCCore()
|
|||||||
|
|
||||||
## Key Concepts
|
## Key Concepts
|
||||||
|
|
||||||
| Concept | Description |
|
| Concept | Description |
|
||||||
|---------|-------------|
|
|-------------------|------------------------------------------------------------------------------|
|
||||||
| **CDC** | Change Data Capture — watches collections registered via `WatchCollection()` |
|
| **CDC** | Change Data Capture <EFBFBD> watches collections registered via `WatchCollection()` |
|
||||||
| **Oplog** | Append-only hash-chained journal of changes per node |
|
| **Oplog** | Append-only hash-chained journal of changes per node |
|
||||||
| **VectorClock** | Tracks causal ordering across the mesh |
|
| **VectorClock** | Tracks causal ordering across the mesh |
|
||||||
| **DocumentStore** | Your bridge between entities and the sync engine |
|
| **DocumentStore** | Your bridge between entities and the sync engine |
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -91,15 +93,16 @@ Your App ? DbContext.SaveChangesAsync()
|
|||||||
|
|
||||||
## Related Packages
|
## Related Packages
|
||||||
|
|
||||||
- **ZB.MOM.WW.CBDDC.Persistence** � BLite embedded provider (.NET 10+)
|
- **ZB.MOM.WW.CBDDC.Persistence** <EFBFBD> BLite embedded provider (.NET 10+)
|
||||||
- **ZB.MOM.WW.CBDDC.Network** — P2P networking (UDP discovery, TCP sync, Gossip)
|
- **ZB.MOM.WW.CBDDC.Network** <EFBFBD> P2P networking (UDP discovery, TCP sync, Gossip)
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **[Complete Documentation](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net)**
|
- **[Complete Documentation](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net)**
|
||||||
- **[Sample Application](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net/tree/main/samples/ZB.MOM.WW.CBDDC.Sample.Console)**
|
- **[Sample Application](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net/tree/main/samples/ZB.MOM.WW.CBDDC.Sample.Console)
|
||||||
|
**
|
||||||
- **[Integration Guide](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net#integrating-with-your-database)**
|
- **[Integration Guide](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net#integrating-with-your-database)**
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net/blob/main/LICENSE)
|
MIT <EFBFBD> see [LICENSE](https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net/blob/main/LICENSE)
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Resilience
|
namespace ZB.MOM.WW.CBDDC.Core.Resilience;
|
||||||
|
|
||||||
|
public interface IRetryPolicy
|
||||||
{
|
{
|
||||||
public interface IRetryPolicy
|
/// <summary>
|
||||||
{
|
/// Executes an asynchronous operation with retry handling.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Executes an asynchronous operation with retry handling.
|
/// <param name="operation">The operation to execute.</param>
|
||||||
/// </summary>
|
/// <param name="operationName">The operation name used for diagnostics.</param>
|
||||||
/// <param name="operation">The operation to execute.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
/// <param name="operationName">The operation name used for diagnostics.</param>
|
/// <returns>A task that represents the asynchronous execution.</returns>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
Task ExecuteAsync(Func<Task> operation, string operationName, CancellationToken cancellationToken = default);
|
||||||
/// <returns>A task that represents the asynchronous execution.</returns>
|
|
||||||
Task ExecuteAsync(Func<Task> operation, string operationName, CancellationToken cancellationToken = default);
|
/// <summary>
|
||||||
/// <summary>
|
/// Executes an asynchronous operation with retry handling and returns a result.
|
||||||
/// Executes an asynchronous operation with retry handling and returns a result.
|
/// </summary>
|
||||||
/// </summary>
|
/// <typeparam name="T">The result type.</typeparam>
|
||||||
/// <typeparam name="T">The result type.</typeparam>
|
/// <param name="operation">The operation to execute.</param>
|
||||||
/// <param name="operation">The operation to execute.</param>
|
/// <param name="operationName">The operation name used for diagnostics.</param>
|
||||||
/// <param name="operationName">The operation name used for diagnostics.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <returns>A task that represents the asynchronous execution and yields the operation result.</returns>
|
||||||
/// <returns>A task that represents the asynchronous execution and yields the operation result.</returns>
|
Task<T> ExecuteAsync<T>(Func<Task<T>> operation, string operationName,
|
||||||
Task<T> ExecuteAsync<T>(Func<Task<T>> operation, string operationName, CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,50 +1,53 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Net.Sockets;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Exceptions;
|
using System.Threading;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Exceptions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using TimeoutException = ZB.MOM.WW.CBDDC.Core.Exceptions.TimeoutException;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Resilience;
|
namespace ZB.MOM.WW.CBDDC.Core.Resilience;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides retry logic for transient failures.
|
/// Provides retry logic for transient failures.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RetryPolicy : IRetryPolicy
|
public class RetryPolicy : IRetryPolicy
|
||||||
{
|
{
|
||||||
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
private readonly ILogger<RetryPolicy> _logger;
|
||||||
private readonly ILogger<RetryPolicy> _logger;
|
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RetryPolicy"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="peerNodeConfigurationProvider">The provider for retry configuration values.</param>
|
|
||||||
/// <param name="logger">The logger instance.</param>
|
|
||||||
public RetryPolicy(IPeerNodeConfigurationProvider peerNodeConfigurationProvider, ILogger<RetryPolicy>? logger = null)
|
|
||||||
{
|
|
||||||
_logger = logger ?? NullLogger<RetryPolicy>.Instance;
|
|
||||||
_peerNodeConfigurationProvider = peerNodeConfigurationProvider
|
|
||||||
?? throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes an operation with retry logic.
|
/// Initializes a new instance of the <see cref="RetryPolicy" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The result type returned by the operation.</typeparam>
|
/// <param name="peerNodeConfigurationProvider">The provider for retry configuration values.</param>
|
||||||
/// <param name="operation">The asynchronous operation to execute.</param>
|
/// <param name="logger">The logger instance.</param>
|
||||||
/// <param name="operationName">The operation name used for logging.</param>
|
public RetryPolicy(IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
||||||
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
|
ILogger<RetryPolicy>? logger = null)
|
||||||
public async Task<T> ExecuteAsync<T>(
|
{
|
||||||
Func<Task<T>> operation,
|
_logger = logger ?? NullLogger<RetryPolicy>.Instance;
|
||||||
string operationName,
|
_peerNodeConfigurationProvider = peerNodeConfigurationProvider
|
||||||
CancellationToken cancellationToken = default)
|
?? throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes an operation with retry logic.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The result type returned by the operation.</typeparam>
|
||||||
|
/// <param name="operation">The asynchronous operation to execute.</param>
|
||||||
|
/// <param name="operationName">The operation name used for logging.</param>
|
||||||
|
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
|
||||||
|
public async Task<T> ExecuteAsync<T>(
|
||||||
|
Func<Task<T>> operation,
|
||||||
|
string operationName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
||||||
Exception? lastException = null;
|
Exception? lastException = null;
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= config.RetryAttempts; attempt++)
|
for (var attempt = 1; attempt <= config.RetryAttempts; attempt++)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Executing {Operation} (attempt {Attempt}/{Max})",
|
_logger.LogDebug("Executing {Operation} (attempt {Attempt}/{Max})",
|
||||||
@@ -55,7 +58,7 @@ public class RetryPolicy : IRetryPolicy
|
|||||||
catch (Exception ex) when (attempt < config.RetryAttempts && IsTransient(ex))
|
catch (Exception ex) when (attempt < config.RetryAttempts && IsTransient(ex))
|
||||||
{
|
{
|
||||||
lastException = ex;
|
lastException = ex;
|
||||||
var delay = config.RetryDelayMs * attempt; // Exponential backoff
|
int delay = config.RetryDelayMs * attempt; // Exponential backoff
|
||||||
|
|
||||||
_logger.LogWarning(ex,
|
_logger.LogWarning(ex,
|
||||||
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
|
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
|
||||||
@@ -63,36 +66,31 @@ public class RetryPolicy : IRetryPolicy
|
|||||||
|
|
||||||
await Task.Delay(delay, cancellationToken);
|
await Task.Delay(delay, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (lastException != null)
|
if (lastException != null)
|
||||||
{
|
_logger.LogError(lastException,
|
||||||
_logger.LogError(lastException,
|
"Operation {Operation} failed after {Attempts} attempts",
|
||||||
"Operation {Operation} failed after {Attempts} attempts",
|
operationName, config.RetryAttempts);
|
||||||
operationName, config.RetryAttempts);
|
else
|
||||||
}
|
_logger.LogError(
|
||||||
else
|
"Operation {Operation} failed after {Attempts} attempts",
|
||||||
{
|
operationName, config.RetryAttempts);
|
||||||
_logger.LogError(
|
|
||||||
"Operation {Operation} failed after {Attempts} attempts",
|
|
||||||
operationName, config.RetryAttempts);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new CBDDCException("RETRY_EXHAUSTED",
|
throw new CBDDCException("RETRY_EXHAUSTED",
|
||||||
$"Operation '{operationName}' failed after {config.RetryAttempts} attempts",
|
$"Operation '{operationName}' failed after {config.RetryAttempts} attempts",
|
||||||
lastException!);
|
lastException!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes an operation with retry logic (void return).
|
/// Executes an operation with retry logic (void return).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="operation">The asynchronous operation to execute.</param>
|
/// <param name="operation">The asynchronous operation to execute.</param>
|
||||||
/// <param name="operationName">The operation name used for logging.</param>
|
/// <param name="operationName">The operation name used for logging.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
|
/// <param name="cancellationToken">A token used to cancel retry delays.</param>
|
||||||
public async Task ExecuteAsync(
|
public async Task ExecuteAsync(
|
||||||
Func<Task> operation,
|
Func<Task> operation,
|
||||||
string operationName,
|
string operationName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ExecuteAsync(async () =>
|
await ExecuteAsync(async () =>
|
||||||
{
|
{
|
||||||
@@ -104,13 +102,13 @@ public class RetryPolicy : IRetryPolicy
|
|||||||
private static bool IsTransient(Exception ex)
|
private static bool IsTransient(Exception ex)
|
||||||
{
|
{
|
||||||
// Network errors are typically transient
|
// Network errors are typically transient
|
||||||
if (ex is NetworkException or System.Net.Sockets.SocketException or System.IO.IOException)
|
if (ex is NetworkException or SocketException or IOException)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Timeout errors are transient
|
// Timeout errors are transient
|
||||||
if (ex is Exceptions.TimeoutException or OperationCanceledException)
|
if (ex is TimeoutException or OperationCanceledException)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,24 @@
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
public class SnapshotMetadata
|
public class SnapshotMetadata
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the node identifier associated with the snapshot.
|
/// Gets or sets the node identifier associated with the snapshot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the physical time component of the snapshot timestamp.
|
/// Gets or sets the physical time component of the snapshot timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long TimestampPhysicalTime { get; set; }
|
public long TimestampPhysicalTime { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the logical counter component of the snapshot timestamp.
|
/// Gets or sets the logical counter component of the snapshot timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TimestampLogicalCounter { get; set; }
|
public int TimestampLogicalCounter { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the snapshot hash.
|
/// Gets or sets the snapshot hash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Hash { get; set; } = "";
|
public string Hash { get; set; } = "";
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents an error that occurs when a database is found to be corrupt.
|
/// Represents an error that occurs when a database is found to be corrupt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CorruptDatabaseException : Exception
|
public class CorruptDatabaseException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CorruptDatabaseException"/> class.
|
/// Initializes a new instance of the <see cref="CorruptDatabaseException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The exception message.</param>
|
/// <param name="message">The exception message.</param>
|
||||||
/// <param name="innerException">The underlying exception that caused this error.</param>
|
/// <param name="innerException">The underlying exception that caused this error.</param>
|
||||||
public CorruptDatabaseException(string message, Exception innerException) : base(message, innerException) { }
|
public CorruptDatabaseException(string message, Exception innerException) : base(message, innerException)
|
||||||
}
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,108 +1,115 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines the contract for storing and retrieving document metadata for sync tracking.
|
/// Defines the contract for storing and retrieving document metadata for sync tracking.
|
||||||
/// Document metadata stores HLC timestamps and deleted state without modifying application entities.
|
/// Document metadata stores HLC timestamps and deleted state without modifying application entities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDocumentMetadataStore : ISnapshotable<DocumentMetadata>
|
public interface IDocumentMetadataStore : ISnapshotable<DocumentMetadata>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the metadata for a specific document.
|
/// Gets the metadata for a specific document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>The document metadata if found; otherwise null.</returns>
|
/// <returns>The document metadata if found; otherwise null.</returns>
|
||||||
Task<DocumentMetadata?> GetMetadataAsync(string collection, string key, CancellationToken cancellationToken = default);
|
Task<DocumentMetadata?> GetMetadataAsync(string collection, string key,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets metadata for all documents in a collection.
|
/// Gets metadata for all documents in a collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>Enumerable of document metadata for the collection.</returns>
|
/// <returns>Enumerable of document metadata for the collection.</returns>
|
||||||
Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection, CancellationToken cancellationToken = default);
|
Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upserts (inserts or updates) metadata for a document.
|
/// Upserts (inserts or updates) metadata for a document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="metadata">The metadata to upsert.</param>
|
/// <param name="metadata">The metadata to upsert.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
Task UpsertMetadataAsync(DocumentMetadata metadata, CancellationToken cancellationToken = default);
|
Task UpsertMetadataAsync(DocumentMetadata metadata, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upserts metadata for multiple documents in batch.
|
/// Upserts metadata for multiple documents in batch.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="metadatas">The metadata items to upsert.</param>
|
/// <param name="metadatas">The metadata items to upsert.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas, CancellationToken cancellationToken = default);
|
Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks a document as deleted by setting IsDeleted=true and updating the timestamp.
|
/// Marks a document as deleted by setting IsDeleted=true and updating the timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The collection name.</param>
|
/// <param name="collection">The collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="timestamp">The HLC timestamp of the deletion.</param>
|
/// <param name="timestamp">The HLC timestamp of the deletion.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp, CancellationToken cancellationToken = default);
|
Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all document metadata with timestamps after the specified timestamp.
|
/// Gets all document metadata with timestamps after the specified timestamp.
|
||||||
/// Used for incremental sync to find documents modified since last sync.
|
/// Used for incremental sync to find documents modified since last sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="since">The timestamp to compare against.</param>
|
/// <param name="since">The timestamp to compare against.</param>
|
||||||
/// <param name="collections">Optional collection filter.</param>
|
/// <param name="collections">Optional collection filter.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>Documents modified after the specified timestamp.</returns>
|
/// <returns>Documents modified after the specified timestamp.</returns>
|
||||||
Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since, IEnumerable<string>? collections = null, CancellationToken cancellationToken = default);
|
Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since,
|
||||||
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents metadata for a document used in sync tracking.
|
/// Represents metadata for a document used in sync tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DocumentMetadata
|
public class DocumentMetadata
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the collection name.
|
/// Initializes a new instance of the <see cref="DocumentMetadata" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Collection { get; set; } = "";
|
public DocumentMetadata()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the document key.
|
/// Initializes a new instance of the <see cref="DocumentMetadata" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; set; } = "";
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
/// <summary>
|
/// <param name="updatedAt">The last update timestamp.</param>
|
||||||
/// Gets or sets the HLC timestamp of the last modification.
|
/// <param name="isDeleted">Whether the document is marked as deleted.</param>
|
||||||
/// </summary>
|
public DocumentMetadata(string collection, string key, HlcTimestamp updatedAt, bool isDeleted = false)
|
||||||
public HlcTimestamp UpdatedAt { get; set; }
|
{
|
||||||
|
Collection = collection;
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether this document is marked as deleted (tombstone).
|
|
||||||
/// </summary>
|
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DocumentMetadata"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public DocumentMetadata() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DocumentMetadata"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="collection">The collection name.</param>
|
|
||||||
/// <param name="key">The document key.</param>
|
|
||||||
/// <param name="updatedAt">The last update timestamp.</param>
|
|
||||||
/// <param name="isDeleted">Whether the document is marked as deleted.</param>
|
|
||||||
public DocumentMetadata(string collection, string key, HlcTimestamp updatedAt, bool isDeleted = false)
|
|
||||||
{
|
|
||||||
Collection = collection;
|
|
||||||
Key = key;
|
Key = key;
|
||||||
UpdatedAt = updatedAt;
|
UpdatedAt = updatedAt;
|
||||||
IsDeleted = isDeleted;
|
IsDeleted = isDeleted;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection name.
|
||||||
|
/// </summary>
|
||||||
|
public string Collection { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the document key.
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HLC timestamp of the last modification.
|
||||||
|
/// </summary>
|
||||||
|
public HlcTimestamp UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets whether this document is marked as deleted (tombstone).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
}
|
||||||
@@ -1,91 +1,112 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
using System.Threading;
|
||||||
using System.Threading;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles basic CRUD operations for documents.
|
/// Handles basic CRUD operations for documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDocumentStore : ISnapshotable<Document>
|
public interface IDocumentStore : ISnapshotable<Document>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collections this store is interested in.
|
/// Gets the collections this store is interested in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<string> InterestedCollection { get; }
|
IEnumerable<string> InterestedCollection { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves a incoming from the specified collection by its key.
|
/// Asynchronously retrieves a incoming from the specified collection by its key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The name of the collection containing the incoming to retrieve. Cannot be null or empty.</param>
|
/// <param name="collection">The name of the collection containing the incoming to retrieve. Cannot be null or empty.</param>
|
||||||
/// <param name="key">The unique key identifying the incoming within the collection. Cannot be null or empty.</param>
|
/// <param name="key">The unique key identifying the incoming within the collection. Cannot be null or empty.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result contains the incoming if found; otherwise, null.</returns>
|
/// <returns>
|
||||||
|
/// A task that represents the asynchronous operation. The task result contains the incoming if found; otherwise,
|
||||||
|
/// null.
|
||||||
|
/// </returns>
|
||||||
Task<Document?> GetDocumentAsync(string collection, string key, CancellationToken cancellationToken = default);
|
Task<Document?> GetDocumentAsync(string collection, string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves all documents belonging to the specified collection.
|
/// Asynchronously retrieves all documents belonging to the specified collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The name of the collection from which to retrieve documents. Cannot be null or empty.</param>
|
/// <param name="collection">The name of the collection from which to retrieve documents. Cannot be null or empty.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result contains an enumerable collection of
|
/// <returns>
|
||||||
/// documents in the specified collection. The collection is empty if no documents are found.</returns>
|
/// A task that represents the asynchronous operation. The task result contains an enumerable collection of
|
||||||
Task<IEnumerable<Document>> GetDocumentsByCollectionAsync(string collection, CancellationToken cancellationToken = default);
|
/// documents in the specified collection. The collection is empty if no documents are found.
|
||||||
|
/// </returns>
|
||||||
|
Task<IEnumerable<Document>> GetDocumentsByCollectionAsync(string collection,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously inserts a batch of documents into the data store.
|
/// Asynchronously inserts a batch of documents into the data store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The collection of documents to insert. Cannot be null or contain null elements.</param>
|
/// <param name="documents">The collection of documents to insert. Cannot be null or contain null elements.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result is <see langword="true"/> if all documents
|
/// <returns>
|
||||||
/// were inserted successfully; otherwise, <see langword="false"/>.</returns>
|
/// A task that represents the asynchronous operation. The task result is <see langword="true" /> if all documents
|
||||||
Task<bool> InsertBatchDocumentsAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default);
|
/// were inserted successfully; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
Task<bool> InsertBatchDocumentsAsync(IEnumerable<Document> documents,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously updates the specified incoming in the data store.
|
/// Asynchronously updates the specified incoming in the data store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="document">The incoming to update. Cannot be null.</param>
|
/// <param name="document">The incoming to update. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the update operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result is <see langword="true"/> if the incoming was
|
/// <returns>
|
||||||
/// successfully updated; otherwise, <see langword="false"/>.</returns>
|
/// A task that represents the asynchronous operation. The task result is <see langword="true" /> if the incoming was
|
||||||
|
/// successfully updated; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
Task<bool> PutDocumentAsync(Document document, CancellationToken cancellationToken = default);
|
Task<bool> PutDocumentAsync(Document document, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously updates a batch of documents in the data store.
|
/// Asynchronously updates a batch of documents in the data store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The collection of documents to update. Cannot be null or contain null elements.</param>
|
/// <param name="documents">The collection of documents to update. Cannot be null or contain null elements.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result is <see langword="true"/> if all documents
|
/// <returns>
|
||||||
/// were updated successfully; otherwise, <see langword="false"/>.</returns>
|
/// A task that represents the asynchronous operation. The task result is <see langword="true" /> if all documents
|
||||||
Task<bool> UpdateBatchDocumentsAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default);
|
/// were updated successfully; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
Task<bool> UpdateBatchDocumentsAsync(IEnumerable<Document> documents,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously deletes a incoming identified by the specified key from the given collection.
|
/// Asynchronously deletes a incoming identified by the specified key from the given collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The name of the collection containing the incoming to delete. Cannot be null or empty.</param>
|
/// <param name="collection">The name of the collection containing the incoming to delete. Cannot be null or empty.</param>
|
||||||
/// <param name="key">The unique key identifying the incoming to delete. Cannot be null or empty.</param>
|
/// <param name="key">The unique key identifying the incoming to delete. Cannot be null or empty.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous delete operation. The task result is <see langword="true"/> if the
|
/// <returns>
|
||||||
/// incoming was successfully deleted; otherwise, <see langword="false"/>.</returns>
|
/// A task that represents the asynchronous delete operation. The task result is <see langword="true" /> if the
|
||||||
|
/// incoming was successfully deleted; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
Task<bool> DeleteDocumentAsync(string collection, string key, CancellationToken cancellationToken = default);
|
Task<bool> DeleteDocumentAsync(string collection, string key, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously deletes a batch of documents identified by their keys.
|
/// Asynchronously deletes a batch of documents identified by their keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If any of the specified documents cannot be deleted, the method returns <see langword="false"/> but does not
|
/// If any of the specified documents cannot be deleted, the method returns <see langword="false" /> but does not
|
||||||
/// throw an exception. The operation is performed asynchronously and may complete partially if cancellation is requested.
|
/// throw an exception. The operation is performed asynchronously and may complete partially if cancellation is
|
||||||
/// </remarks>
|
/// requested.
|
||||||
/// <param name="documentKeys">A collection of incoming keys that specify the documents to delete. Cannot be null or contain null or empty
|
/// </remarks>
|
||||||
/// values.</param>
|
/// <param name="documentKeys">
|
||||||
|
/// A collection of incoming keys that specify the documents to delete. Cannot be null or contain null or empty
|
||||||
|
/// values.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the delete operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous delete operation. The task result is <see langword="true"/> if all
|
/// <returns>
|
||||||
/// specified documents were successfully deleted; otherwise, <see langword="false"/>.</returns>
|
/// A task that represents the asynchronous delete operation. The task result is <see langword="true" /> if all
|
||||||
Task<bool> DeleteBatchDocumentsAsync(IEnumerable<string> documentKeys, CancellationToken cancellationToken = default);
|
/// specified documents were successfully deleted; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
Task<bool> DeleteBatchDocumentsAsync(IEnumerable<string> documentKeys,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously merges the specified incoming with existing data and returns the updated incoming.
|
/// Asynchronously merges the specified incoming with existing data and returns the updated incoming.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="incoming">The incoming to merge. Cannot be null.</param>
|
/// <param name="incoming">The incoming to merge. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
||||||
@@ -93,11 +114,14 @@ public interface IDocumentStore : ISnapshotable<Document>
|
|||||||
Task<Document> MergeAsync(Document incoming, CancellationToken cancellationToken = default);
|
Task<Document> MergeAsync(Document incoming, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves documents identified by the specified collection and key pairs.
|
/// Asynchronously retrieves documents identified by the specified collection and key pairs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documentKeys">A list of tuples, each containing the collection name and the document key that uniquely identify the documents
|
/// <param name="documentKeys">
|
||||||
/// to retrieve. Cannot be null or empty.</param>
|
/// A list of tuples, each containing the collection name and the document key that uniquely identify the documents
|
||||||
|
/// to retrieve. Cannot be null or empty.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous retrieval operation.</returns>
|
/// <returns>A task that represents the asynchronous retrieval operation.</returns>
|
||||||
Task<IEnumerable<Document>> GetDocumentsAsync(List<(string Collection, string Key)> documentKeys, CancellationToken cancellationToken);
|
Task<IEnumerable<Document>> GetDocumentsAsync(List<(string Collection, string Key)> documentKeys,
|
||||||
}
|
CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -7,17 +6,17 @@ using System.Threading.Tasks;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles operations related to the Operation Log (Oplog), synchronization, and logical clocks.
|
/// Handles operations related to the Operation Log (Oplog), synchronization, and logical clocks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IOplogStore : ISnapshotable<OplogEntry>
|
public interface IOplogStore : ISnapshotable<OplogEntry>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when changes are applied to the store from external sources (sync).
|
/// Occurs when changes are applied to the store from external sources (sync).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<ChangesAppliedEventArgs> ChangesApplied;
|
event EventHandler<ChangesAppliedEventArgs> ChangesApplied;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Appends a new entry to the operation log asynchronously.
|
/// Appends a new entry to the operation log asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entry">The operation log entry to append. Cannot be null.</param>
|
/// <param name="entry">The operation log entry to append. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the append operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the append operation.</param>
|
||||||
@@ -25,57 +24,64 @@ public interface IOplogStore : ISnapshotable<OplogEntry>
|
|||||||
Task AppendOplogEntryAsync(OplogEntry entry, CancellationToken cancellationToken = default);
|
Task AppendOplogEntryAsync(OplogEntry entry, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves all oplog entries that occurred after the specified timestamp.
|
/// Asynchronously retrieves all oplog entries that occurred after the specified timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timestamp">The timestamp after which oplog entries should be returned.</param>
|
/// <param name="timestamp">The timestamp after which oplog entries should be returned.</param>
|
||||||
/// <param name="collections">An optional collection of collection names to filter the results.</param>
|
/// <param name="collections">An optional collection of collection names to filter the results.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing matching oplog entries.</returns>
|
/// <returns>A task that represents the asynchronous operation containing matching oplog entries.</returns>
|
||||||
Task<IEnumerable<OplogEntry>> GetOplogAfterAsync(HlcTimestamp timestamp, IEnumerable<string>? collections = null, CancellationToken cancellationToken = default);
|
Task<IEnumerable<OplogEntry>> GetOplogAfterAsync(HlcTimestamp timestamp, IEnumerable<string>? collections = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the latest observed hybrid logical clock (HLC) timestamp.
|
/// Asynchronously retrieves the latest observed hybrid logical clock (HLC) timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing the latest HLC timestamp.</returns>
|
/// <returns>A task that represents the asynchronous operation containing the latest HLC timestamp.</returns>
|
||||||
Task<HlcTimestamp> GetLatestTimestampAsync(CancellationToken cancellationToken = default);
|
Task<HlcTimestamp> GetLatestTimestampAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the current vector clock representing the state of distributed events.
|
/// Asynchronously retrieves the current vector clock representing the state of distributed events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing the current vector clock.</returns>
|
/// <returns>A task that represents the asynchronous operation containing the current vector clock.</returns>
|
||||||
Task<VectorClock> GetVectorClockAsync(CancellationToken cancellationToken = default);
|
Task<VectorClock> GetVectorClockAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a collection of oplog entries for the specified node that occurred after the given timestamp.
|
/// Retrieves a collection of oplog entries for the specified node that occurred after the given timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the node for which to retrieve oplog entries. Cannot be null or empty.</param>
|
/// <param name="nodeId">The unique identifier of the node for which to retrieve oplog entries. Cannot be null or empty.</param>
|
||||||
/// <param name="since">The timestamp after which oplog entries should be returned.</param>
|
/// <param name="since">The timestamp after which oplog entries should be returned.</param>
|
||||||
/// <param name="collections">An optional collection of collection names to filter the oplog entries.</param>
|
/// <param name="collections">An optional collection of collection names to filter the oplog entries.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing oplog entries for the specified node.</returns>
|
/// <returns>A task that represents the asynchronous operation containing oplog entries for the specified node.</returns>
|
||||||
Task<IEnumerable<OplogEntry>> GetOplogForNodeAfterAsync(string nodeId, HlcTimestamp since, IEnumerable<string>? collections = null, CancellationToken cancellationToken = default);
|
Task<IEnumerable<OplogEntry>> GetOplogForNodeAfterAsync(string nodeId, HlcTimestamp since,
|
||||||
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the hash of the most recent entry for the specified node.
|
/// Asynchronously retrieves the hash of the most recent entry for the specified node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the node for which to retrieve the last entry hash. Cannot be null or empty.</param>
|
/// <param name="nodeId">
|
||||||
|
/// The unique identifier of the node for which to retrieve the last entry hash. Cannot be null or
|
||||||
|
/// empty.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing the hash string of the last entry or null.</returns>
|
/// <returns>A task that represents the asynchronous operation containing the hash string of the last entry or null.</returns>
|
||||||
Task<string?> GetLastEntryHashAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task<string?> GetLastEntryHashAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves a sequence of oplog entries representing the chain between the specified start and end hashes.
|
/// Asynchronously retrieves a sequence of oplog entries representing the chain between the specified start and end
|
||||||
|
/// hashes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startHash">The hash of the first entry in the chain range. Cannot be null or empty.</param>
|
/// <param name="startHash">The hash of the first entry in the chain range. Cannot be null or empty.</param>
|
||||||
/// <param name="endHash">The hash of the last entry in the chain range. Cannot be null or empty.</param>
|
/// <param name="endHash">The hash of the last entry in the chain range. Cannot be null or empty.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation containing OplogEntry objects in chain order.</returns>
|
/// <returns>A task that represents the asynchronous operation containing OplogEntry objects in chain order.</returns>
|
||||||
Task<IEnumerable<OplogEntry>> GetChainRangeAsync(string startHash, string endHash, CancellationToken cancellationToken = default);
|
Task<IEnumerable<OplogEntry>> GetChainRangeAsync(string startHash, string endHash,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the oplog entry associated with the specified hash value.
|
/// Asynchronously retrieves the oplog entry associated with the specified hash value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hash">The hash string identifying the oplog entry to retrieve. Cannot be null or empty.</param>
|
/// <param name="hash">The hash string identifying the oplog entry to retrieve. Cannot be null or empty.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
@@ -83,7 +89,7 @@ public interface IOplogStore : ISnapshotable<OplogEntry>
|
|||||||
Task<OplogEntry?> GetEntryByHashAsync(string hash, CancellationToken cancellationToken = default);
|
Task<OplogEntry?> GetEntryByHashAsync(string hash, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies a batch of oplog entries asynchronously to the target data store.
|
/// Applies a batch of oplog entries asynchronously to the target data store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="oplogEntries">A collection of OplogEntry objects representing the operations to apply. Cannot be null.</param>
|
/// <param name="oplogEntries">A collection of OplogEntry objects representing the operations to apply. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the batch operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the batch operation.</param>
|
||||||
@@ -91,11 +97,10 @@ public interface IOplogStore : ISnapshotable<OplogEntry>
|
|||||||
Task ApplyBatchAsync(IEnumerable<OplogEntry> oplogEntries, CancellationToken cancellationToken = default);
|
Task ApplyBatchAsync(IEnumerable<OplogEntry> oplogEntries, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously removes entries from the oplog that are older than the specified cutoff timestamp.
|
/// Asynchronously removes entries from the oplog that are older than the specified cutoff timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cutoff">The timestamp that defines the upper bound for entries to be pruned.</param>
|
/// <param name="cutoff">The timestamp that defines the upper bound for entries to be pruned.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the prune operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the prune operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous prune operation.</returns>
|
/// <returns>A task that represents the asynchronous prune operation.</returns>
|
||||||
Task PruneOplogAsync(HlcTimestamp cutoff, CancellationToken cancellationToken = default);
|
Task PruneOplogAsync(HlcTimestamp cutoff, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -6,26 +6,26 @@ using ZB.MOM.WW.CBDDC.Core.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles storage and retrieval of remote peer configurations.
|
/// Handles storage and retrieval of remote peer configurations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPeerConfigurationStore : ISnapshotable<RemotePeerConfiguration>
|
public interface IPeerConfigurationStore : ISnapshotable<RemotePeerConfiguration>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Saves or updates a remote peer configuration in the persistent store.
|
/// Saves or updates a remote peer configuration in the persistent store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peer">The remote peer configuration to save.</param>
|
/// <param name="peer">The remote peer configuration to save.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task SaveRemotePeerAsync(RemotePeerConfiguration peer, CancellationToken cancellationToken = default);
|
Task SaveRemotePeerAsync(RemotePeerConfiguration peer, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves all remote peer configurations from the persistent store.
|
/// Retrieves all remote peer configurations from the persistent store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Collection of remote peer configurations.</returns>
|
/// <returns>Collection of remote peer configurations.</returns>
|
||||||
Task<IEnumerable<RemotePeerConfiguration>> GetRemotePeersAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<RemotePeerConfiguration>> GetRemotePeersAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the configuration for a remote peer identified by the specified node ID.
|
/// Asynchronously retrieves the configuration for a remote peer identified by the specified node ID.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the remote peer whose configuration is to be retrieved.</param>
|
/// <param name="nodeId">The unique identifier of the remote peer whose configuration is to be retrieved.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
@@ -33,9 +33,9 @@ public interface IPeerConfigurationStore : ISnapshotable<RemotePeerConfiguration
|
|||||||
Task<RemotePeerConfiguration?> GetRemotePeerAsync(string nodeId, CancellationToken cancellationToken);
|
Task<RemotePeerConfiguration?> GetRemotePeerAsync(string nodeId, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a remote peer configuration from the persistent store.
|
/// Removes a remote peer configuration from the persistent store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the peer to remove.</param>
|
/// <param name="nodeId">The unique identifier of the peer to remove.</param>
|
||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,12 @@ using ZB.MOM.WW.CBDDC.Core.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines persistence operations for peer oplog confirmation tracking.
|
/// Defines persistence operations for peer oplog confirmation tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPeerOplogConfirmationStore : ISnapshotable<PeerOplogConfirmation>
|
public interface IPeerOplogConfirmationStore : ISnapshotable<PeerOplogConfirmation>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures the specified peer is tracked for confirmation-based pruning.
|
/// Ensures the specified peer is tracked for confirmation-based pruning.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeId">The peer node identifier.</param>
|
/// <param name="peerNodeId">The peer node identifier.</param>
|
||||||
/// <param name="address">The peer network address.</param>
|
/// <param name="address">The peer network address.</param>
|
||||||
@@ -24,7 +24,7 @@ public interface IPeerOplogConfirmationStore : ISnapshotable<PeerOplogConfirmati
|
|||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the confirmation watermark for a tracked peer and source node.
|
/// Updates the confirmation watermark for a tracked peer and source node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeId">The tracked peer node identifier.</param>
|
/// <param name="peerNodeId">The tracked peer node identifier.</param>
|
||||||
/// <param name="sourceNodeId">The source node identifier of the confirmed oplog stream.</param>
|
/// <param name="sourceNodeId">The source node identifier of the confirmed oplog stream.</param>
|
||||||
@@ -39,14 +39,14 @@ public interface IPeerOplogConfirmationStore : ISnapshotable<PeerOplogConfirmati
|
|||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all persisted peer confirmations.
|
/// Gets all persisted peer confirmations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>All peer confirmations.</returns>
|
/// <returns>All peer confirmations.</returns>
|
||||||
Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets persisted confirmations for a specific tracked peer.
|
/// Gets persisted confirmations for a specific tracked peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeId">The peer node identifier.</param>
|
/// <param name="peerNodeId">The peer node identifier.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
@@ -56,16 +56,16 @@ public interface IPeerOplogConfirmationStore : ISnapshotable<PeerOplogConfirmati
|
|||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deactivates tracking for the specified peer.
|
/// Deactivates tracking for the specified peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeId">The peer node identifier.</param>
|
/// <param name="peerNodeId">The peer node identifier.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
Task RemovePeerTrackingAsync(string peerNodeId, CancellationToken cancellationToken = default);
|
Task RemovePeerTrackingAsync(string peerNodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all active tracked peer identifiers.
|
/// Gets all active tracked peer identifiers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>Distinct active tracked peer identifiers.</returns>
|
/// <returns>Distinct active tracked peer identifiers.</returns>
|
||||||
Task<IEnumerable<string>> GetActiveTrackedPeersAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<string>> GetActiveTrackedPeersAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -7,16 +7,21 @@ namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
|||||||
public interface ISnapshotMetadataStore : ISnapshotable<SnapshotMetadata>
|
public interface ISnapshotMetadataStore : ISnapshotable<SnapshotMetadata>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the snapshot metadata associated with the specified node identifier.
|
/// Asynchronously retrieves the snapshot metadata associated with the specified node identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the node for which to retrieve snapshot metadata. Cannot be null or empty.</param>
|
/// <param name="nodeId">
|
||||||
|
/// The unique identifier of the node for which to retrieve snapshot metadata. Cannot be null or
|
||||||
|
/// empty.
|
||||||
|
/// </param>
|
||||||
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation. The task result contains the <see cref="SnapshotMetadata"/>
|
/// <returns>
|
||||||
/// for the specified node if found; otherwise, <see langword="null"/>.</returns>
|
/// A task that represents the asynchronous operation. The task result contains the <see cref="SnapshotMetadata" />
|
||||||
|
/// for the specified node if found; otherwise, <see langword="null" />.
|
||||||
|
/// </returns>
|
||||||
Task<SnapshotMetadata?> GetSnapshotMetadataAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task<SnapshotMetadata?> GetSnapshotMetadataAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously inserts the specified snapshot metadata into the data store.
|
/// Asynchronously inserts the specified snapshot metadata into the data store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="metadata">The snapshot metadata to insert. Cannot be null.</param>
|
/// <param name="metadata">The snapshot metadata to insert. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
@@ -24,7 +29,7 @@ public interface ISnapshotMetadataStore : ISnapshotable<SnapshotMetadata>
|
|||||||
Task InsertSnapshotMetadataAsync(SnapshotMetadata metadata, CancellationToken cancellationToken = default);
|
Task InsertSnapshotMetadataAsync(SnapshotMetadata metadata, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously updates the metadata for an existing snapshot.
|
/// Asynchronously updates the metadata for an existing snapshot.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="existingMeta">The metadata object representing the snapshot to update. Cannot be null.</param>
|
/// <param name="existingMeta">The metadata object representing the snapshot to update. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
|
||||||
@@ -32,7 +37,7 @@ public interface ISnapshotMetadataStore : ISnapshotable<SnapshotMetadata>
|
|||||||
Task UpdateSnapshotMetadataAsync(SnapshotMetadata existingMeta, CancellationToken cancellationToken = default);
|
Task UpdateSnapshotMetadataAsync(SnapshotMetadata existingMeta, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously retrieves the hash of the current snapshot for the specified node.
|
/// Asynchronously retrieves the hash of the current snapshot for the specified node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The unique identifier of the node for which to obtain the snapshot hash.</param>
|
/// <param name="nodeId">The unique identifier of the node for which to obtain the snapshot hash.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
@@ -40,9 +45,9 @@ public interface ISnapshotMetadataStore : ISnapshotable<SnapshotMetadata>
|
|||||||
Task<string?> GetSnapshotHashAsync(string nodeId, CancellationToken cancellationToken = default);
|
Task<string?> GetSnapshotHashAsync(string nodeId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all snapshot metadata entries. Used for initializing VectorClock cache.
|
/// Gets all snapshot metadata entries. Used for initializing VectorClock cache.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token.</param>
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
/// <returns>All snapshot metadata entries.</returns>
|
/// <returns>All snapshot metadata entries.</returns>
|
||||||
Task<IEnumerable<SnapshotMetadata>> GetAllSnapshotMetadataAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<SnapshotMetadata>> GetAllSnapshotMetadataAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -5,12 +5,12 @@ using System.Threading.Tasks;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles full database lifecycle operations such as snapshots, replacement, and clearing data.
|
/// Handles full database lifecycle operations such as snapshots, replacement, and clearing data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISnapshotService
|
public interface ISnapshotService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously creates a snapshot of the current state and writes it to the specified destination stream.
|
/// Asynchronously creates a snapshot of the current state and writes it to the specified destination stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination">The stream to which the snapshot data will be written.</param>
|
/// <param name="destination">The stream to which the snapshot data will be written.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the snapshot creation operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the snapshot creation operation.</param>
|
||||||
@@ -18,7 +18,7 @@ public interface ISnapshotService
|
|||||||
Task CreateSnapshotAsync(Stream destination, CancellationToken cancellationToken = default);
|
Task CreateSnapshotAsync(Stream destination, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replaces the existing database with the contents provided in the specified stream asynchronously.
|
/// Replaces the existing database with the contents provided in the specified stream asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="databaseStream">A stream containing the new database data to be used for replacement.</param>
|
/// <param name="databaseStream">A stream containing the new database data to be used for replacement.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||||
@@ -26,10 +26,10 @@ public interface ISnapshotService
|
|||||||
Task ReplaceDatabaseAsync(Stream databaseStream, CancellationToken cancellationToken = default);
|
Task ReplaceDatabaseAsync(Stream databaseStream, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges the provided snapshot stream into the current data store asynchronously.
|
/// Merges the provided snapshot stream into the current data store asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="snapshotStream">A stream containing the snapshot data to be merged.</param>
|
/// <param name="snapshotStream">A stream containing the snapshot data to be merged.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous merge operation.</returns>
|
/// <returns>A task that represents the asynchronous merge operation.</returns>
|
||||||
Task MergeSnapshotAsync(Stream snapshotStream, CancellationToken cancellationToken = default);
|
Task MergeSnapshotAsync(Stream snapshotStream, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -7,24 +7,28 @@ namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
|||||||
public interface ISnapshotable<T>
|
public interface ISnapshotable<T>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously deletes the underlying data store and all of its contents.
|
/// Asynchronously deletes the underlying data store and all of its contents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the drop operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the drop operation.</param>
|
||||||
/// <remarks>After calling this method, the data store and all stored data will be permanently removed.
|
/// <remarks>
|
||||||
/// This operation cannot be undone. Any further operations on the data store may result in errors.</remarks>
|
/// After calling this method, the data store and all stored data will be permanently removed.
|
||||||
|
/// This operation cannot be undone. Any further operations on the data store may result in errors.
|
||||||
|
/// </remarks>
|
||||||
/// <returns>A task that represents the asynchronous drop operation.</returns>
|
/// <returns>A task that represents the asynchronous drop operation.</returns>
|
||||||
Task DropAsync(CancellationToken cancellationToken = default);
|
Task DropAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Asynchronously exports a collection of items of type T.
|
/// Asynchronously exports a collection of items of type T.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the export operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the export operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous export operation. The task result contains an enumerable collection of
|
/// <returns>
|
||||||
/// exported items of type T.</returns>
|
/// A task that represents the asynchronous export operation. The task result contains an enumerable collection of
|
||||||
|
/// exported items of type T.
|
||||||
|
/// </returns>
|
||||||
Task<IEnumerable<T>> ExportAsync(CancellationToken cancellationToken = default);
|
Task<IEnumerable<T>> ExportAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Imports the specified collection of items asynchronously.
|
/// Imports the specified collection of items asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">The collection of items to import. Cannot be null. Each item will be processed in sequence.</param>
|
/// <param name="items">The collection of items to import. Cannot be null. Each item will be processed in sequence.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the import operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the import operation.</param>
|
||||||
@@ -32,13 +36,15 @@ public interface ISnapshotable<T>
|
|||||||
Task ImportAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
|
Task ImportAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges the specified collection of items into the target data store asynchronously.
|
/// Merges the specified collection of items into the target data store asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>If the operation is canceled via the provided cancellation token, the returned task will be
|
/// <remarks>
|
||||||
/// in a canceled state. The merge operation may update existing items or add new items, depending on the
|
/// If the operation is canceled via the provided cancellation token, the returned task will be
|
||||||
/// implementation.</remarks>
|
/// in a canceled state. The merge operation may update existing items or add new items, depending on the
|
||||||
|
/// implementation.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="items">The collection of items to merge into the data store. Cannot be null.</param>
|
/// <param name="items">The collection of items to merge into the data store. Cannot be null.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
/// <param name="cancellationToken">A cancellation token that can be used to cancel the merge operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous merge operation.</returns>
|
/// <returns>A task that represents the asynchronous merge operation.</returns>
|
||||||
Task MergeAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
|
Task MergeAsync(IEnumerable<T> items, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,49 +1,49 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
namespace ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages Vector Clock state for the local node.
|
/// Manages Vector Clock state for the local node.
|
||||||
/// Tracks the latest timestamp and hash per node for sync coordination.
|
/// Tracks the latest timestamp and hash per node for sync coordination.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IVectorClockService
|
public interface IVectorClockService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether the cache has been populated with initial data.
|
/// Indicates whether the cache has been populated with initial data.
|
||||||
/// Reset to false by <see cref="Invalidate"/>.
|
/// Reset to false by <see cref="Invalidate" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsInitialized { get; set; }
|
bool IsInitialized { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the cache with a new OplogEntry's timestamp and hash.
|
/// Updates the cache with a new OplogEntry's timestamp and hash.
|
||||||
/// Called by both DocumentStore (local CDC) and OplogStore (remote sync).
|
/// Called by both DocumentStore (local CDC) and OplogStore (remote sync).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entry">The oplog entry containing timestamp and hash data.</param>
|
/// <param name="entry">The oplog entry containing timestamp and hash data.</param>
|
||||||
void Update(OplogEntry entry);
|
void Update(OplogEntry entry);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current Vector Clock built from cached node timestamps.
|
/// Returns the current Vector Clock built from cached node timestamps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
Task<VectorClock> GetVectorClockAsync(CancellationToken cancellationToken = default);
|
Task<VectorClock> GetVectorClockAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the latest known timestamp across all nodes.
|
/// Returns the latest known timestamp across all nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
Task<HlcTimestamp> GetLatestTimestampAsync(CancellationToken cancellationToken = default);
|
Task<HlcTimestamp> GetLatestTimestampAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the last known hash for the specified node.
|
/// Returns the last known hash for the specified node.
|
||||||
/// Returns null if the node is unknown.
|
/// Returns null if the node is unknown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier.</param>
|
/// <param name="nodeId">The node identifier.</param>
|
||||||
string? GetLastHash(string nodeId);
|
string? GetLastHash(string nodeId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the cache with a specific node's timestamp and hash.
|
/// Updates the cache with a specific node's timestamp and hash.
|
||||||
/// Used for snapshot metadata fallback.
|
/// Used for snapshot metadata fallback.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier.</param>
|
/// <param name="nodeId">The node identifier.</param>
|
||||||
/// <param name="timestamp">The timestamp to store for the node.</param>
|
/// <param name="timestamp">The timestamp to store for the node.</param>
|
||||||
@@ -51,8 +51,8 @@ public interface IVectorClockService
|
|||||||
void UpdateNode(string nodeId, HlcTimestamp timestamp, string hash);
|
void UpdateNode(string nodeId, HlcTimestamp timestamp, string hash);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears the cache and resets <see cref="IsInitialized"/> to false,
|
/// Clears the cache and resets <see cref="IsInitialized" /> to false,
|
||||||
/// forcing re-initialization on next access.
|
/// forcing re-initialization on next access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
void Invalidate();
|
void Invalidate();
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
using System.Text.Json;
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
|
||||||
|
|
||||||
public class ConflictResolutionResult
|
public class ConflictResolutionResult
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the remote change should be applied.
|
/// Initializes a new instance of the <see cref="ConflictResolutionResult" /> class.
|
||||||
/// </summary>
|
|
||||||
public bool ShouldApply { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the merged document to apply when conflict resolution produced one.
|
|
||||||
/// </summary>
|
|
||||||
public Document? MergedDocument { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ConflictResolutionResult"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="shouldApply">Indicates whether the change should be applied.</param>
|
/// <param name="shouldApply">Indicates whether the change should be applied.</param>
|
||||||
/// <param name="mergedDocument">The merged document produced by resolution, if any.</param>
|
/// <param name="mergedDocument">The merged document produced by resolution, if any.</param>
|
||||||
@@ -26,25 +14,42 @@ public class ConflictResolutionResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a result indicating that the resolved document should be applied.
|
/// Gets a value indicating whether the remote change should be applied.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldApply { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the merged document to apply when conflict resolution produced one.
|
||||||
|
/// </summary>
|
||||||
|
public Document? MergedDocument { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a result indicating that the resolved document should be applied.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="document">The merged document to apply.</param>
|
/// <param name="document">The merged document to apply.</param>
|
||||||
/// <returns>A resolution result that applies the provided document.</returns>
|
/// <returns>A resolution result that applies the provided document.</returns>
|
||||||
public static ConflictResolutionResult Apply(Document document) => new(true, document);
|
public static ConflictResolutionResult Apply(Document document)
|
||||||
|
{
|
||||||
|
return new ConflictResolutionResult(true, document);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a result indicating that the remote change should be ignored.
|
/// Creates a result indicating that the remote change should be ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A resolution result that skips applying the remote change.</returns>
|
/// <returns>A resolution result that skips applying the remote change.</returns>
|
||||||
public static ConflictResolutionResult Ignore() => new(false, null);
|
public static ConflictResolutionResult Ignore()
|
||||||
|
{
|
||||||
|
return new ConflictResolutionResult(false, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IConflictResolver
|
public interface IConflictResolver
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves a conflict between local state and a remote oplog entry.
|
/// Resolves a conflict between local state and a remote oplog entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="local">The local document state, if present.</param>
|
/// <param name="local">The local document state, if present.</param>
|
||||||
/// <param name="remote">The incoming remote oplog entry.</param>
|
/// <param name="remote">The incoming remote oplog entry.</param>
|
||||||
/// <returns>The resolution outcome indicating whether and how to apply changes.</returns>
|
/// <returns>The resolution outcome indicating whether and how to apply changes.</returns>
|
||||||
ConflictResolutionResult Resolve(Document? local, OplogEntry remote);
|
ConflictResolutionResult Resolve(Document? local, OplogEntry remote);
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a queue for operations that should be executed when connectivity is restored.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOfflineQueue
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a queue for operations that should be executed when connectivity is restored.
|
/// Gets the number of pending operations in the queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IOfflineQueue
|
int Count { get; }
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the number of pending operations in the queue.
|
|
||||||
/// </summary>
|
|
||||||
int Count { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all pending operations from the queue.
|
/// Clears all pending operations from the queue.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Clear();
|
Task Clear();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enqueues a pending operation.
|
/// Enqueues a pending operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="operation">The operation to enqueue.</param>
|
/// <param name="operation">The operation to enqueue.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Enqueue(PendingOperation operation);
|
Task Enqueue(PendingOperation operation);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Flushes the queue by executing each pending operation.
|
/// Flushes the queue by executing each pending operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="executor">The delegate used to execute each operation.</param>
|
/// <param name="executor">The delegate used to execute each operation.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the flush operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the flush operation.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A task that returns a tuple containing the number of successful and failed operations.
|
/// A task that returns a tuple containing the number of successful and failed operations.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
Task<(int Successful, int Failed)> FlushAsync(Func<PendingOperation, Task> executor, CancellationToken cancellationToken = default);
|
Task<(int Successful, int Failed)> FlushAsync(Func<PendingOperation, Task> executor,
|
||||||
}
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,22 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
public class LastWriteWinsConflictResolver : IConflictResolver
|
public class LastWriteWinsConflictResolver : IConflictResolver
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves document conflicts by preferring the entry with the latest timestamp.
|
/// Resolves document conflicts by preferring the entry with the latest timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="local">The local document, if available.</param>
|
/// <param name="local">The local document, if available.</param>
|
||||||
/// <param name="remote">The incoming remote oplog entry.</param>
|
/// <param name="remote">The incoming remote oplog entry.</param>
|
||||||
/// <returns>The conflict resolution result indicating whether to apply or ignore the remote change.</returns>
|
/// <returns>The conflict resolution result indicating whether to apply or ignore the remote change.</returns>
|
||||||
public ConflictResolutionResult Resolve(Document? local, OplogEntry remote)
|
public ConflictResolutionResult Resolve(Document? local, OplogEntry remote)
|
||||||
{
|
{
|
||||||
// If no local document exists, always apply remote change
|
// If no local document exists, always apply remote change
|
||||||
if (local == null)
|
if (local == null)
|
||||||
{
|
{
|
||||||
// Construct new document from oplog entry
|
// Construct new document from oplog entry
|
||||||
var content = remote.Payload ?? default;
|
var content = remote.Payload ?? default;
|
||||||
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp, remote.Operation == OperationType.Delete);
|
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp,
|
||||||
|
remote.Operation == OperationType.Delete);
|
||||||
return ConflictResolutionResult.Apply(newDoc);
|
return ConflictResolutionResult.Apply(newDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,11 +25,12 @@ public class LastWriteWinsConflictResolver : IConflictResolver
|
|||||||
{
|
{
|
||||||
// Remote is newer, apply it
|
// Remote is newer, apply it
|
||||||
var content = remote.Payload ?? default;
|
var content = remote.Payload ?? default;
|
||||||
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp, remote.Operation == OperationType.Delete);
|
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp,
|
||||||
|
remote.Operation == OperationType.Delete);
|
||||||
return ConflictResolutionResult.Apply(newDoc);
|
return ConflictResolutionResult.Apply(newDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local is newer or equal, ignore remote
|
// Local is newer or equal, ignore remote
|
||||||
return ConflictResolutionResult.Ignore();
|
return ConflictResolutionResult.Ignore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,38 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using System;
|
||||||
using Microsoft.Extensions.Logging;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using System.Linq;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queue for operations performed while offline.
|
/// Queue for operations performed while offline.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OfflineQueue : IOfflineQueue
|
public class OfflineQueue : IOfflineQueue
|
||||||
{
|
{
|
||||||
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
private readonly object _lock = new();
|
||||||
private readonly Queue<PendingOperation> _queue = new();
|
private readonly ILogger<OfflineQueue> _logger;
|
||||||
private readonly ILogger<OfflineQueue> _logger;
|
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
||||||
private readonly object _lock = new();
|
private readonly Queue<PendingOperation> _queue = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="OfflineQueue"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="peerNodeConfigurationProvider">The configuration provider used for queue limits.</param>
|
|
||||||
/// <param name="logger">The logger instance.</param>
|
|
||||||
public OfflineQueue(IPeerNodeConfigurationProvider peerNodeConfigurationProvider, ILogger<OfflineQueue>? logger = null)
|
|
||||||
{
|
|
||||||
_peerNodeConfigurationProvider = peerNodeConfigurationProvider;
|
|
||||||
_logger = logger ?? NullLogger<OfflineQueue>.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the number of pending operations.
|
/// Initializes a new instance of the <see cref="OfflineQueue" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="peerNodeConfigurationProvider">The configuration provider used for queue limits.</param>
|
||||||
|
/// <param name="logger">The logger instance.</param>
|
||||||
|
public OfflineQueue(IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
||||||
|
ILogger<OfflineQueue>? logger = null)
|
||||||
|
{
|
||||||
|
_peerNodeConfigurationProvider = peerNodeConfigurationProvider;
|
||||||
|
_logger = logger ?? NullLogger<OfflineQueue>.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of pending operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Count
|
public int Count
|
||||||
{
|
{
|
||||||
@@ -44,15 +45,15 @@ public class OfflineQueue : IOfflineQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enqueues an operation for later execution.
|
/// Enqueues an operation for later execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="operation">The pending operation to enqueue.</param>
|
/// <param name="operation">The pending operation to enqueue.</param>
|
||||||
/// <returns>A task that represents the asynchronous enqueue operation.</returns>
|
/// <returns>A task that represents the asynchronous enqueue operation.</returns>
|
||||||
public async Task Enqueue(PendingOperation operation)
|
public async Task Enqueue(PendingOperation operation)
|
||||||
{
|
{
|
||||||
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_queue.Count >= config.MaxQueueSize)
|
if (_queue.Count >= config.MaxQueueSize)
|
||||||
{
|
{
|
||||||
@@ -67,15 +68,16 @@ public class OfflineQueue : IOfflineQueue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Flushes all pending operations.
|
/// Flushes all pending operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="executor">The delegate that executes each pending operation.</param>
|
/// <param name="executor">The delegate that executes each pending operation.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the operation.</param>
|
||||||
/// <returns>A task whose result contains the number of successful and failed operations.</returns>
|
/// <returns>A task whose result contains the number of successful and failed operations.</returns>
|
||||||
public async Task<(int Successful, int Failed)> FlushAsync(Func<PendingOperation, Task> executor, CancellationToken cancellationToken = default)
|
public async Task<(int Successful, int Failed)> FlushAsync(Func<PendingOperation, Task> executor,
|
||||||
{
|
CancellationToken cancellationToken = default)
|
||||||
List<PendingOperation> operations;
|
{
|
||||||
|
List<PendingOperation> operations;
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
@@ -91,11 +93,10 @@ public class OfflineQueue : IOfflineQueue
|
|||||||
|
|
||||||
_logger.LogInformation("Flushing {Count} pending operations", operations.Count);
|
_logger.LogInformation("Flushing {Count} pending operations", operations.Count);
|
||||||
|
|
||||||
int successful = 0;
|
var successful = 0;
|
||||||
int failed = 0;
|
var failed = 0;
|
||||||
|
|
||||||
foreach (var op in operations)
|
foreach (var op in operations)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await executor(op);
|
await executor(op);
|
||||||
@@ -107,7 +108,6 @@ public class OfflineQueue : IOfflineQueue
|
|||||||
_logger.LogError(ex, "Failed to execute pending {Type} operation for {Collection}:{Key}",
|
_logger.LogError(ex, "Failed to execute pending {Type} operation for {Collection}:{Key}",
|
||||||
op.Type, op.Collection, op.Key);
|
op.Type, op.Collection, op.Key);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Flush completed: {Successful} successful, {Failed} failed",
|
_logger.LogInformation("Flush completed: {Successful} successful, {Failed} failed",
|
||||||
successful, failed);
|
successful, failed);
|
||||||
@@ -116,15 +116,15 @@ public class OfflineQueue : IOfflineQueue
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all pending operations.
|
/// Clears all pending operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task Clear()
|
public async Task Clear()
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
var count = _queue.Count;
|
int count = _queue.Count;
|
||||||
_queue.Clear();
|
_queue.Clear();
|
||||||
_logger.LogInformation("Cleared {Count} pending operations", count);
|
_logger.LogInformation("Cleared {Count} pending operations", count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,34 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
/// <summary>
|
||||||
|
/// Represents a pending operation to be executed when connection is restored.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Represents a pending operation to be executed when connection is restored.
|
|
||||||
/// </summary>
|
|
||||||
public class PendingOperation
|
public class PendingOperation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the operation type.
|
/// Gets or sets the operation type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Type { get; set; } = "";
|
public string Type { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the collection targeted by the operation.
|
/// Gets or sets the collection targeted by the operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Collection { get; set; } = "";
|
public string Collection { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the document key targeted by the operation.
|
/// Gets or sets the document key targeted by the operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; set; } = "";
|
public string Key { get; set; } = "";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the payload associated with the operation.
|
/// Gets or sets the payload associated with the operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object? Data { get; set; }
|
public object? Data { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the UTC time when the operation was queued.
|
/// Gets or sets the UTC time when the operation was queued.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime QueuedAt { get; set; }
|
public DateTime QueuedAt { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,28 +1,27 @@
|
|||||||
using System;
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
namespace ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves merge conflicts by recursively merging object and array nodes.
|
/// Resolves merge conflicts by recursively merging object and array nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves a conflict between a local document and a remote operation.
|
/// Resolves a conflict between a local document and a remote operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="local">The local document, or <see langword="null"/> if none exists.</param>
|
/// <param name="local">The local document, or <see langword="null" /> if none exists.</param>
|
||||||
/// <param name="remote">The remote operation to apply.</param>
|
/// <param name="remote">The remote operation to apply.</param>
|
||||||
/// <returns>The conflict resolution result indicating whether and what to apply.</returns>
|
/// <returns>The conflict resolution result indicating whether and what to apply.</returns>
|
||||||
public ConflictResolutionResult Resolve(Document? local, OplogEntry remote)
|
public ConflictResolutionResult Resolve(Document? local, OplogEntry remote)
|
||||||
{
|
{
|
||||||
if (local == null)
|
if (local == null)
|
||||||
{
|
{
|
||||||
var content = remote.Payload ?? default;
|
var content = remote.Payload ?? default;
|
||||||
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp, remote.Operation == OperationType.Delete);
|
var newDoc = new Document(remote.Collection, remote.Key, content, remote.Timestamp,
|
||||||
|
remote.Operation == OperationType.Delete);
|
||||||
return ConflictResolutionResult.Apply(newDoc);
|
return ConflictResolutionResult.Apply(newDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +32,7 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
var newDoc = new Document(remote.Collection, remote.Key, default, remote.Timestamp, true);
|
var newDoc = new Document(remote.Collection, remote.Key, default, remote.Timestamp, true);
|
||||||
return ConflictResolutionResult.Apply(newDoc);
|
return ConflictResolutionResult.Apply(newDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConflictResolutionResult.Ignore();
|
return ConflictResolutionResult.Ignore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +41,14 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
var localTs = local.UpdatedAt;
|
var localTs = local.UpdatedAt;
|
||||||
var remoteTs = remote.Timestamp;
|
var remoteTs = remote.Timestamp;
|
||||||
|
|
||||||
if (localJson.ValueKind == JsonValueKind.Undefined) return ConflictResolutionResult.Apply(new Document(remote.Collection, remote.Key, remoteJson, remoteTs, false));
|
if (localJson.ValueKind == JsonValueKind.Undefined)
|
||||||
if (remoteJson.ValueKind == JsonValueKind.Undefined) return ConflictResolutionResult.Ignore();
|
return ConflictResolutionResult.Apply(new Document(remote.Collection, remote.Key, remoteJson, remoteTs,
|
||||||
|
false));
|
||||||
// Optimization: Use ArrayBufferWriter (Net6.0) or MemoryStream (NS2.0)
|
if (remoteJson.ValueKind == JsonValueKind.Undefined) return ConflictResolutionResult.Ignore();
|
||||||
// Utf8JsonWriter works with both, but ArrayBufferWriter is more efficient for high throughput.
|
|
||||||
|
// Optimization: Use ArrayBufferWriter (Net6.0) or MemoryStream (NS2.0)
|
||||||
|
// Utf8JsonWriter works with both, but ArrayBufferWriter is more efficient for high throughput.
|
||||||
|
|
||||||
JsonElement mergedDocJson;
|
JsonElement mergedDocJson;
|
||||||
|
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
@@ -55,7 +57,8 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
{
|
{
|
||||||
MergeJson(writer, localJson, localTs, remoteJson, remoteTs);
|
MergeJson(writer, localJson, localTs, remoteJson, remoteTs);
|
||||||
}
|
}
|
||||||
mergedDocJson = JsonDocument.Parse(bufferWriter.WrittenMemory).RootElement;
|
|
||||||
|
mergedDocJson = JsonDocument.Parse(bufferWriter.WrittenMemory).RootElement;
|
||||||
#else
|
#else
|
||||||
using (var ms = new MemoryStream())
|
using (var ms = new MemoryStream())
|
||||||
{
|
{
|
||||||
@@ -67,13 +70,14 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
mergedDocJson = JsonDocument.Parse(ms.ToArray()).RootElement;
|
mergedDocJson = JsonDocument.Parse(ms.ToArray()).RootElement;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var maxTimestamp = remoteTs.CompareTo(localTs) > 0 ? remoteTs : localTs;
|
var maxTimestamp = remoteTs.CompareTo(localTs) > 0 ? remoteTs : localTs;
|
||||||
var mergedDoc = new Document(remote.Collection, remote.Key, mergedDocJson, maxTimestamp, false);
|
var mergedDoc = new Document(remote.Collection, remote.Key, mergedDocJson, maxTimestamp, false);
|
||||||
return ConflictResolutionResult.Apply(mergedDoc);
|
return ConflictResolutionResult.Apply(mergedDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MergeJson(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote, HlcTimestamp remoteTs)
|
private void MergeJson(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote,
|
||||||
|
HlcTimestamp remoteTs)
|
||||||
{
|
{
|
||||||
if (local.ValueKind != remote.ValueKind)
|
if (local.ValueKind != remote.ValueKind)
|
||||||
{
|
{
|
||||||
@@ -93,7 +97,7 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Primitives
|
// Primitives
|
||||||
if (local.GetRawText() == remote.GetRawText())
|
if (local.GetRawText() == remote.GetRawText())
|
||||||
{
|
{
|
||||||
local.WriteTo(writer);
|
local.WriteTo(writer);
|
||||||
}
|
}
|
||||||
@@ -102,54 +106,51 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
if (remoteTs.CompareTo(localTs) > 0) remote.WriteTo(writer);
|
if (remoteTs.CompareTo(localTs) > 0) remote.WriteTo(writer);
|
||||||
else local.WriteTo(writer);
|
else local.WriteTo(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MergeObjects(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote, HlcTimestamp remoteTs)
|
private void MergeObjects(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote,
|
||||||
|
HlcTimestamp remoteTs)
|
||||||
{
|
{
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
||||||
// We need to iterate keys. To avoid double iteration efficiently, we can use a dictionary for the UNION of keys.
|
// We need to iterate keys. To avoid double iteration efficiently, we can use a dictionary for the UNION of keys.
|
||||||
// But populating a dictionary is effectively what we did before.
|
// But populating a dictionary is effectively what we did before.
|
||||||
// Can we do better?
|
// Can we do better?
|
||||||
// Yes: Iterate Local, write merged/local. Track handled keys. Then iterate Remote, write remaining.
|
// Yes: Iterate Local, write merged/local. Track handled keys. Then iterate Remote, write remaining.
|
||||||
|
|
||||||
var processedKeys = new HashSet<string>();
|
var processedKeys = new HashSet<string>();
|
||||||
|
|
||||||
foreach (var prop in local.EnumerateObject())
|
foreach (var prop in local.EnumerateObject())
|
||||||
{
|
{
|
||||||
var key = prop.Name;
|
string key = prop.Name;
|
||||||
processedKeys.Add(key); // Mark as processed
|
processedKeys.Add(key); // Mark as processed
|
||||||
|
|
||||||
writer.WritePropertyName(key);
|
writer.WritePropertyName(key);
|
||||||
|
|
||||||
if (remote.TryGetProperty(key, out var remoteVal))
|
if (remote.TryGetProperty(key, out var remoteVal))
|
||||||
{
|
|
||||||
// Collision -> Merge
|
// Collision -> Merge
|
||||||
MergeJson(writer, prop.Value, localTs, remoteVal, remoteTs);
|
MergeJson(writer, prop.Value, localTs, remoteVal, remoteTs);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
// Only local
|
// Only local
|
||||||
prop.Value.WriteTo(writer);
|
prop.Value.WriteTo(writer);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var prop in remote.EnumerateObject())
|
foreach (var prop in remote.EnumerateObject())
|
||||||
{
|
|
||||||
if (!processedKeys.Contains(prop.Name))
|
if (!processedKeys.Contains(prop.Name))
|
||||||
{
|
{
|
||||||
// New from remote
|
// New from remote
|
||||||
writer.WritePropertyName(prop.Name);
|
writer.WritePropertyName(prop.Name);
|
||||||
prop.Value.WriteTo(writer);
|
prop.Value.WriteTo(writer);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MergeArrays(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote, HlcTimestamp remoteTs)
|
private void MergeArrays(Utf8JsonWriter writer, JsonElement local, HlcTimestamp localTs, JsonElement remote,
|
||||||
|
HlcTimestamp remoteTs)
|
||||||
{
|
{
|
||||||
// Heuristic check
|
// Heuristic check
|
||||||
bool localIsObj = HasObjects(local);
|
bool localIsObj = HasObjects(local);
|
||||||
@@ -164,10 +165,10 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (localIsObj != remoteIsObj)
|
if (localIsObj != remoteIsObj)
|
||||||
{
|
{
|
||||||
// Mixed mistmatch LWW
|
// Mixed mistmatch LWW
|
||||||
if (remoteTs.CompareTo(localTs) > 0) remote.WriteTo(writer);
|
if (remoteTs.CompareTo(localTs) > 0) remote.WriteTo(writer);
|
||||||
else local.WriteTo(writer);
|
else local.WriteTo(writer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,44 +185,36 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
|
|
||||||
// We want to write Union of items by ID.
|
// We want to write Union of items by ID.
|
||||||
// To preserve some semblance of order (or just determinism), we can iterate local IDs first, then remote new IDs.
|
// To preserve some semblance of order (or just determinism), we can iterate local IDs first, then remote new IDs.
|
||||||
// Or just use the dictionary values.
|
// Or just use the dictionary values.
|
||||||
|
|
||||||
// NOTE: We cannot simply write to writer inside the map loop if we are creating a merged map.
|
// NOTE: We cannot simply write to writer inside the map loop if we are creating a merged map.
|
||||||
// Let's iterate the union of keys similar to Objects.
|
// Let's iterate the union of keys similar to Objects.
|
||||||
|
|
||||||
var processedIds = new HashSet<string>();
|
var processedIds = new HashSet<string>();
|
||||||
|
|
||||||
// 1. Process Local Items (Merge or Write)
|
// 1. Process Local Items (Merge or Write)
|
||||||
foreach (var kvp in localMap)
|
foreach (var kvp in localMap)
|
||||||
{
|
{
|
||||||
var id = kvp.Key;
|
string id = kvp.Key;
|
||||||
var localItem = kvp.Value;
|
var localItem = kvp.Value;
|
||||||
processedIds.Add(id);
|
processedIds.Add(id);
|
||||||
|
|
||||||
if (remoteMap.TryGetValue(id, out var remoteItem))
|
if (remoteMap.TryGetValue(id, out var remoteItem))
|
||||||
{
|
|
||||||
// Merge recursively
|
// Merge recursively
|
||||||
MergeJson(writer, localItem, localTs, remoteItem, remoteTs);
|
MergeJson(writer, localItem, localTs, remoteItem, remoteTs);
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
// Keep local item
|
// Keep local item
|
||||||
localItem.WriteTo(writer);
|
localItem.WriteTo(writer);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Process New Remote Items
|
// 2. Process New Remote Items
|
||||||
foreach (var kvp in remoteMap)
|
foreach (var kvp in remoteMap)
|
||||||
{
|
|
||||||
if (!processedIds.Contains(kvp.Key))
|
if (!processedIds.Contains(kvp.Key))
|
||||||
{
|
|
||||||
kvp.Value.WriteTo(writer);
|
kvp.Value.WriteTo(writer);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
writer.WriteEndArray();
|
||||||
}
|
}
|
||||||
@@ -249,6 +242,7 @@ public class RecursiveNodeMergeConflictResolver : IConflictResolver
|
|||||||
|
|
||||||
map[id] = item;
|
map[id] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,86 +5,84 @@ using System.Linq;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Core;
|
namespace ZB.MOM.WW.CBDDC.Core;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a Vector Clock for tracking causality in a distributed system.
|
/// Represents a Vector Clock for tracking causality in a distributed system.
|
||||||
/// Maps NodeId -> HlcTimestamp to track the latest known state of each node.
|
/// Maps NodeId -> HlcTimestamp to track the latest known state of each node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VectorClock
|
public class VectorClock
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, HlcTimestamp> _clock;
|
private readonly Dictionary<string, HlcTimestamp> _clock;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new empty vector clock.
|
|
||||||
/// </summary>
|
|
||||||
public VectorClock()
|
|
||||||
{
|
|
||||||
_clock = new Dictionary<string, HlcTimestamp>(StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new vector clock from an existing clock state.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="clock">The clock state to copy.</param>
|
|
||||||
public VectorClock(Dictionary<string, HlcTimestamp> clock)
|
|
||||||
{
|
|
||||||
_clock = new Dictionary<string, HlcTimestamp>(clock, StringComparer.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all node IDs in this vector clock.
|
/// Initializes a new empty vector clock.
|
||||||
|
/// </summary>
|
||||||
|
public VectorClock()
|
||||||
|
{
|
||||||
|
_clock = new Dictionary<string, HlcTimestamp>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new vector clock from an existing clock state.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clock">The clock state to copy.</param>
|
||||||
|
public VectorClock(Dictionary<string, HlcTimestamp> clock)
|
||||||
|
{
|
||||||
|
_clock = new Dictionary<string, HlcTimestamp>(clock, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all node IDs in this vector clock.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> NodeIds => _clock.Keys;
|
public IEnumerable<string> NodeIds => _clock.Keys;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the timestamp for a specific node, or default if not present.
|
/// Gets the timestamp for a specific node, or default if not present.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier.</param>
|
/// <param name="nodeId">The node identifier.</param>
|
||||||
public HlcTimestamp GetTimestamp(string nodeId)
|
public HlcTimestamp GetTimestamp(string nodeId)
|
||||||
{
|
{
|
||||||
return _clock.TryGetValue(nodeId, out var ts) ? ts : default;
|
return _clock.TryGetValue(nodeId, out var ts) ? ts : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets or updates the timestamp for a specific node.
|
/// Sets or updates the timestamp for a specific node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier.</param>
|
/// <param name="nodeId">The node identifier.</param>
|
||||||
/// <param name="timestamp">The timestamp to set.</param>
|
/// <param name="timestamp">The timestamp to set.</param>
|
||||||
public void SetTimestamp(string nodeId, HlcTimestamp timestamp)
|
public void SetTimestamp(string nodeId, HlcTimestamp timestamp)
|
||||||
{
|
{
|
||||||
_clock[nodeId] = timestamp;
|
_clock[nodeId] = timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges another vector clock into this one, taking the maximum timestamp for each node.
|
/// Merges another vector clock into this one, taking the maximum timestamp for each node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The vector clock to merge from.</param>
|
/// <param name="other">The vector clock to merge from.</param>
|
||||||
public void Merge(VectorClock other)
|
public void Merge(VectorClock other)
|
||||||
{
|
{
|
||||||
foreach (var nodeId in other.NodeIds)
|
foreach (string nodeId in other.NodeIds)
|
||||||
{
|
{
|
||||||
var otherTs = other.GetTimestamp(nodeId);
|
var otherTs = other.GetTimestamp(nodeId);
|
||||||
if (!_clock.TryGetValue(nodeId, out var currentTs) || otherTs.CompareTo(currentTs) > 0)
|
if (!_clock.TryGetValue(nodeId, out var currentTs) || otherTs.CompareTo(currentTs) > 0)
|
||||||
{
|
|
||||||
_clock[nodeId] = otherTs;
|
_clock[nodeId] = otherTs;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compares this vector clock with another to determine causality.
|
/// Compares this vector clock with another to determine causality.
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// - Positive: This is strictly ahead (dominates other)
|
/// - Positive: This is strictly ahead (dominates other)
|
||||||
/// - Negative: Other is strictly ahead (other dominates this)
|
/// - Negative: Other is strictly ahead (other dominates this)
|
||||||
/// - Zero: Concurrent (neither dominates)
|
/// - Zero: Concurrent (neither dominates)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The vector clock to compare with.</param>
|
/// <param name="other">The vector clock to compare with.</param>
|
||||||
public CausalityRelation CompareTo(VectorClock other)
|
public CausalityRelation CompareTo(VectorClock other)
|
||||||
{
|
{
|
||||||
bool thisAhead = false;
|
var thisAhead = false;
|
||||||
bool otherAhead = false;
|
var otherAhead = false;
|
||||||
|
|
||||||
var allNodes = new HashSet<string>(_clock.Keys.Union(other._clock.Keys), StringComparer.Ordinal);
|
var allNodes = new HashSet<string>(_clock.Keys.Union(other._clock.Keys), StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var nodeId in allNodes)
|
foreach (string nodeId in allNodes)
|
||||||
{
|
{
|
||||||
var thisTs = GetTimestamp(nodeId);
|
var thisTs = GetTimestamp(nodeId);
|
||||||
var otherTs = other.GetTimestamp(nodeId);
|
var otherTs = other.GetTimestamp(nodeId);
|
||||||
@@ -92,19 +90,11 @@ public class VectorClock
|
|||||||
int cmp = thisTs.CompareTo(otherTs);
|
int cmp = thisTs.CompareTo(otherTs);
|
||||||
|
|
||||||
if (cmp > 0)
|
if (cmp > 0)
|
||||||
{
|
|
||||||
thisAhead = true;
|
thisAhead = true;
|
||||||
}
|
else if (cmp < 0) otherAhead = true;
|
||||||
else if (cmp < 0)
|
|
||||||
{
|
|
||||||
otherAhead = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early exit if concurrent
|
// Early exit if concurrent
|
||||||
if (thisAhead && otherAhead)
|
if (thisAhead && otherAhead) return CausalityRelation.Concurrent;
|
||||||
{
|
|
||||||
return CausalityRelation.Concurrent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thisAhead && !otherAhead)
|
if (thisAhead && !otherAhead)
|
||||||
@@ -115,65 +105,56 @@ public class VectorClock
|
|||||||
return CausalityRelation.Equal;
|
return CausalityRelation.Equal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines which nodes have updates that this vector clock doesn't have.
|
/// Determines which nodes have updates that this vector clock doesn't have.
|
||||||
/// Returns node IDs where the other vector clock is ahead.
|
/// Returns node IDs where the other vector clock is ahead.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other">The vector clock to compare against.</param>
|
/// <param name="other">The vector clock to compare against.</param>
|
||||||
public IEnumerable<string> GetNodesWithUpdates(VectorClock other)
|
public IEnumerable<string> GetNodesWithUpdates(VectorClock other)
|
||||||
{
|
{
|
||||||
var allNodes = new HashSet<string>(_clock.Keys, StringComparer.Ordinal);
|
var allNodes = new HashSet<string>(_clock.Keys, StringComparer.Ordinal);
|
||||||
foreach (var nodeId in other._clock.Keys)
|
foreach (string nodeId in other._clock.Keys) allNodes.Add(nodeId);
|
||||||
{
|
|
||||||
allNodes.Add(nodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var nodeId in allNodes)
|
foreach (string nodeId in allNodes)
|
||||||
{
|
{
|
||||||
var thisTs = GetTimestamp(nodeId);
|
var thisTs = GetTimestamp(nodeId);
|
||||||
var otherTs = other.GetTimestamp(nodeId);
|
var otherTs = other.GetTimestamp(nodeId);
|
||||||
|
|
||||||
if (otherTs.CompareTo(thisTs) > 0)
|
if (otherTs.CompareTo(thisTs) > 0) yield return nodeId;
|
||||||
{
|
|
||||||
yield return nodeId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines which nodes have updates that the other vector clock doesn't have.
|
|
||||||
/// Returns node IDs where this vector clock is ahead.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The vector clock to compare against.</param>
|
|
||||||
public IEnumerable<string> GetNodesToPush(VectorClock other)
|
|
||||||
{
|
|
||||||
var allNodes = new HashSet<string>(_clock.Keys.Union(other._clock.Keys), StringComparer.Ordinal);
|
|
||||||
|
|
||||||
foreach (var nodeId in allNodes)
|
|
||||||
{
|
|
||||||
var thisTs = GetTimestamp(nodeId);
|
|
||||||
var otherTs = other.GetTimestamp(nodeId);
|
|
||||||
|
|
||||||
if (thisTs.CompareTo(otherTs) > 0)
|
|
||||||
{
|
|
||||||
yield return nodeId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a copy of this vector clock.
|
/// Determines which nodes have updates that the other vector clock doesn't have.
|
||||||
|
/// Returns node IDs where this vector clock is ahead.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">The vector clock to compare against.</param>
|
||||||
|
public IEnumerable<string> GetNodesToPush(VectorClock other)
|
||||||
|
{
|
||||||
|
var allNodes = new HashSet<string>(_clock.Keys.Union(other._clock.Keys), StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (string nodeId in allNodes)
|
||||||
|
{
|
||||||
|
var thisTs = GetTimestamp(nodeId);
|
||||||
|
var otherTs = other.GetTimestamp(nodeId);
|
||||||
|
|
||||||
|
if (thisTs.CompareTo(otherTs) > 0) yield return nodeId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a copy of this vector clock.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public VectorClock Clone()
|
public VectorClock Clone()
|
||||||
{
|
{
|
||||||
return new VectorClock(new Dictionary<string, HlcTimestamp>(_clock, StringComparer.Ordinal));
|
return new VectorClock(new Dictionary<string, HlcTimestamp>(_clock, StringComparer.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (_clock.Count == 0)
|
if (_clock.Count == 0)
|
||||||
return "{}";
|
return "{}";
|
||||||
|
|
||||||
var entries = _clock.Select(kvp => $"{kvp.Key}:{kvp.Value}");
|
var entries = _clock.Select(kvp => $"{kvp.Key}:{kvp.Value}");
|
||||||
return "{" + string.Join(", ", entries) + "}";
|
return "{" + string.Join(", ", entries) + "}";
|
||||||
@@ -181,16 +162,19 @@ public class VectorClock
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the causality relationship between two vector clocks.
|
/// Represents the causality relationship between two vector clocks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum CausalityRelation
|
public enum CausalityRelation
|
||||||
{
|
{
|
||||||
/// <summary>Both vector clocks are equal.</summary>
|
/// <summary>Both vector clocks are equal.</summary>
|
||||||
Equal,
|
Equal,
|
||||||
|
|
||||||
/// <summary>This vector clock is strictly ahead (dominates).</summary>
|
/// <summary>This vector clock is strictly ahead (dominates).</summary>
|
||||||
StrictlyAhead,
|
StrictlyAhead,
|
||||||
|
|
||||||
/// <summary>This vector clock is strictly behind (dominated).</summary>
|
/// <summary>This vector clock is strictly behind (dominated).</summary>
|
||||||
StrictlyBehind,
|
StrictlyBehind,
|
||||||
|
|
||||||
/// <summary>Vector clocks are concurrent (neither dominates).</summary>
|
/// <summary>Vector clocks are concurrent (neither dominates).</summary>
|
||||||
Concurrent
|
Concurrent
|
||||||
}
|
}
|
||||||
@@ -1,33 +1,33 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>ZB.MOM.WW.CBDDC.Core</AssemblyName>
|
<AssemblyName>ZB.MOM.WW.CBDDC.Core</AssemblyName>
|
||||||
<RootNamespace>ZB.MOM.WW.CBDDC.Core</RootNamespace>
|
<RootNamespace>ZB.MOM.WW.CBDDC.Core</RootNamespace>
|
||||||
<PackageId>ZB.MOM.WW.CBDDC.Core</PackageId>
|
<PackageId>ZB.MOM.WW.CBDDC.Core</PackageId>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>1.0.3</Version>
|
<Version>1.0.3</Version>
|
||||||
<Authors>MrDevRobot</Authors>
|
<Authors>MrDevRobot</Authors>
|
||||||
<Description>Core abstractions and logic for CBDDC, a lightweight P2P mesh database.</Description>
|
<Description>Core abstractions and logic for CBDDC, a lightweight P2P mesh database.</Description>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageTags>p2p;mesh;database;gossip;cbddc;lan;offline-first;distributed</PackageTags>
|
<PackageTags>p2p;mesh;database;gossip;cbddc;lan;offline-first;distributed</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Storage\Events\" />
|
<Folder Include="Storage\Events\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||||
using ZB.MOM.WW.CBDDC.Hosting.HealthChecks;
|
using ZB.MOM.WW.CBDDC.Hosting.HealthChecks;
|
||||||
using ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
using ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
||||||
using ZB.MOM.WW.CBDDC.Hosting.Services;
|
using ZB.MOM.WW.CBDDC.Hosting.Services;
|
||||||
using ZB.MOM.WW.CBDDC.Network;
|
using ZB.MOM.WW.CBDDC.Network;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Hosting;
|
namespace ZB.MOM.WW.CBDDC.Hosting;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods for configuring CBDDC in ASP.NET Core applications.
|
/// Extension methods for configuring CBDDC in ASP.NET Core applications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CBDDCHostingExtensions
|
public static class CBDDCHostingExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds CBDDC ASP.NET integration with the specified configuration.
|
/// Adds CBDDC ASP.NET integration with the specified configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service collection.</param>
|
/// <param name="services">The service collection.</param>
|
||||||
/// <param name="configure">Action to configure CBDDC options.</param>
|
/// <param name="configure">Action to configure CBDDC options.</param>
|
||||||
@@ -43,7 +43,7 @@ public static class CBDDCHostingExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds CBDDC ASP.NET integration for single-cluster mode.
|
/// Adds CBDDC ASP.NET integration for single-cluster mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service collection.</param>
|
/// <param name="services">The service collection.</param>
|
||||||
/// <param name="configure">Action to configure single-cluster options.</param>
|
/// <param name="configure">Action to configure single-cluster options.</param>
|
||||||
@@ -51,10 +51,7 @@ public static class CBDDCHostingExtensions
|
|||||||
this IServiceCollection services,
|
this IServiceCollection services,
|
||||||
Action<ClusterOptions>? configure = null)
|
Action<ClusterOptions>? configure = null)
|
||||||
{
|
{
|
||||||
return services.AddCBDDCHosting(options =>
|
return services.AddCBDDCHosting(options => { configure?.Invoke(options.Cluster); });
|
||||||
{
|
|
||||||
configure?.Invoke(options.Cluster);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterSingleClusterServices(
|
private static void RegisterSingleClusterServices(
|
||||||
@@ -81,12 +78,10 @@ public static class CBDDCHostingExtensions
|
|||||||
{
|
{
|
||||||
// Health checks
|
// Health checks
|
||||||
if (options.EnableHealthChecks)
|
if (options.EnableHealthChecks)
|
||||||
{
|
|
||||||
services.AddHealthChecks()
|
services.AddHealthChecks()
|
||||||
.AddCheck<CBDDCHealthCheck>(
|
.AddCheck<CBDDCHealthCheck>(
|
||||||
"cbddc",
|
"cbddc",
|
||||||
failureStatus: HealthStatus.Unhealthy,
|
HealthStatus.Unhealthy,
|
||||||
tags: new[] { "db", "ready" });
|
new[] { "db", "ready" });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
namespace ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration options for CBDDC ASP.NET integration.
|
/// Configuration options for CBDDC ASP.NET integration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCHostingOptions
|
public class CBDDCHostingOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the cluster configuration.
|
/// Gets or sets the cluster configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ClusterOptions Cluster { get; set; } = new();
|
public ClusterOptions Cluster { get; set; } = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether to enable health checks.
|
/// Gets or sets whether to enable health checks.
|
||||||
/// Default: true
|
/// Default: true
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableHealthChecks { get; set; } = true;
|
public bool EnableHealthChecks { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,39 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
namespace ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration options for cluster mode.
|
/// Configuration options for cluster mode.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClusterOptions
|
public class ClusterOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the node identifier for this instance.
|
/// Gets or sets the node identifier for this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string NodeId { get; set; } = Environment.MachineName;
|
public string NodeId { get; set; } = Environment.MachineName;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the TCP port for sync operations.
|
/// Gets or sets the TCP port for sync operations.
|
||||||
/// Default: 5001
|
/// Default: 5001
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int TcpPort { get; set; } = 5001;
|
public int TcpPort { get; set; } = 5001;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets whether to enable UDP discovery.
|
/// Gets or sets whether to enable UDP discovery.
|
||||||
/// Default: false (disabled in server mode)
|
/// Default: false (disabled in server mode)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableUdpDiscovery { get; set; } = false;
|
public bool EnableUdpDiscovery { get; set; } = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the lag threshold (in milliseconds) used to determine when a tracked peer is considered lagging.
|
/// Gets or sets the lag threshold (in milliseconds) used to determine when a tracked peer is considered lagging.
|
||||||
/// Peers above this threshold degrade health status.
|
/// Peers above this threshold degrade health status.
|
||||||
/// Default: 30,000 ms.
|
/// Default: 30,000 ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long PeerConfirmationLagThresholdMs { get; set; } = 30_000;
|
public long PeerConfirmationLagThresholdMs { get; set; } = 30_000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the critical lag threshold (in milliseconds) used to determine when a tracked peer causes unhealthy status.
|
/// Gets or sets the critical lag threshold (in milliseconds) used to determine when a tracked peer causes unhealthy
|
||||||
/// Peers above this threshold mark health as unhealthy.
|
/// status.
|
||||||
/// Default: 120,000 ms.
|
/// Peers above this threshold mark health as unhealthy.
|
||||||
|
/// Default: 120,000 ms.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long PeerConfirmationCriticalLagThresholdMs { get; set; } = 120_000;
|
public long PeerConfirmationCriticalLagThresholdMs { get; set; } = 120_000;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
||||||
@@ -10,17 +5,17 @@ using ZB.MOM.WW.CBDDC.Hosting.Configuration;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.HealthChecks;
|
namespace ZB.MOM.WW.CBDDC.Hosting.HealthChecks;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Health check for CBDDC persistence layer.
|
/// Health check for CBDDC persistence layer.
|
||||||
/// Verifies that the database connection is healthy.
|
/// Verifies that the database connection is healthy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCHealthCheck : IHealthCheck
|
public class CBDDCHealthCheck : IHealthCheck
|
||||||
{
|
{
|
||||||
private readonly IOplogStore _oplogStore;
|
private readonly IOplogStore _oplogStore;
|
||||||
private readonly IPeerOplogConfirmationStore _peerOplogConfirmationStore;
|
|
||||||
private readonly CBDDCHostingOptions _options;
|
private readonly CBDDCHostingOptions _options;
|
||||||
|
private readonly IPeerOplogConfirmationStore _peerOplogConfirmationStore;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CBDDCHealthCheck"/> class.
|
/// Initializes a new instance of the <see cref="CBDDCHealthCheck" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="oplogStore">The oplog store used to verify persistence health.</param>
|
/// <param name="oplogStore">The oplog store used to verify persistence health.</param>
|
||||||
/// <param name="peerOplogConfirmationStore">The peer confirmation store used for confirmation lag health checks.</param>
|
/// <param name="peerOplogConfirmationStore">The peer confirmation store used for confirmation lag health checks.</param>
|
||||||
@@ -31,16 +26,17 @@ public class CBDDCHealthCheck : IHealthCheck
|
|||||||
CBDDCHostingOptions options)
|
CBDDCHostingOptions options)
|
||||||
{
|
{
|
||||||
_oplogStore = oplogStore ?? throw new ArgumentNullException(nameof(oplogStore));
|
_oplogStore = oplogStore ?? throw new ArgumentNullException(nameof(oplogStore));
|
||||||
_peerOplogConfirmationStore = peerOplogConfirmationStore ?? throw new ArgumentNullException(nameof(peerOplogConfirmationStore));
|
_peerOplogConfirmationStore = peerOplogConfirmationStore ??
|
||||||
|
throw new ArgumentNullException(nameof(peerOplogConfirmationStore));
|
||||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a health check against the CBDDC persistence layer.
|
/// Performs a health check against the CBDDC persistence layer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The health check execution context.</param>
|
/// <param name="context">The health check execution context.</param>
|
||||||
/// <param name="cancellationToken">A token used to cancel the health check.</param>
|
/// <param name="cancellationToken">A token used to cancel the health check.</param>
|
||||||
/// <returns>A <see cref="HealthCheckResult"/> describing the health status.</returns>
|
/// <returns>A <see cref="HealthCheckResult" /> describing the health status.</returns>
|
||||||
public async Task<HealthCheckResult> CheckHealthAsync(
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
HealthCheckContext context,
|
HealthCheckContext context,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -58,15 +54,18 @@ public class CBDDCHealthCheck : IHealthCheck
|
|||||||
var peersWithNoConfirmation = new List<string>();
|
var peersWithNoConfirmation = new List<string>();
|
||||||
var laggingPeers = new List<string>();
|
var laggingPeers = new List<string>();
|
||||||
var criticalLaggingPeers = new List<string>();
|
var criticalLaggingPeers = new List<string>();
|
||||||
var lastSuccessfulConfirmationUpdateByPeer = new Dictionary<string, DateTimeOffset?>(StringComparer.Ordinal);
|
var lastSuccessfulConfirmationUpdateByPeer =
|
||||||
|
new Dictionary<string, DateTimeOffset?>(StringComparer.Ordinal);
|
||||||
var maxLagMs = 0L;
|
var maxLagMs = 0L;
|
||||||
|
|
||||||
var lagThresholdMs = Math.Max(0, _options.Cluster.PeerConfirmationLagThresholdMs);
|
long lagThresholdMs = Math.Max(0, _options.Cluster.PeerConfirmationLagThresholdMs);
|
||||||
var criticalLagThresholdMs = Math.Max(lagThresholdMs, _options.Cluster.PeerConfirmationCriticalLagThresholdMs);
|
long criticalLagThresholdMs =
|
||||||
|
Math.Max(lagThresholdMs, _options.Cluster.PeerConfirmationCriticalLagThresholdMs);
|
||||||
|
|
||||||
foreach (var peerNodeId in trackedPeers)
|
foreach (string peerNodeId in trackedPeers)
|
||||||
{
|
{
|
||||||
var confirmations = (await _peerOplogConfirmationStore.GetConfirmationsForPeerAsync(peerNodeId, cancellationToken))
|
var confirmations =
|
||||||
|
(await _peerOplogConfirmationStore.GetConfirmationsForPeerAsync(peerNodeId, cancellationToken))
|
||||||
.Where(confirmation => confirmation.IsActive)
|
.Where(confirmation => confirmation.IsActive)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -83,19 +82,14 @@ public class CBDDCHealthCheck : IHealthCheck
|
|||||||
.ThenBy(confirmation => confirmation.ConfirmedLogic)
|
.ThenBy(confirmation => confirmation.ConfirmedLogic)
|
||||||
.First();
|
.First();
|
||||||
|
|
||||||
var lagMs = Math.Max(0, localHead.PhysicalTime - oldestConfirmation.ConfirmedWall);
|
long lagMs = Math.Max(0, localHead.PhysicalTime - oldestConfirmation.ConfirmedWall);
|
||||||
maxLagMs = Math.Max(maxLagMs, lagMs);
|
maxLagMs = Math.Max(maxLagMs, lagMs);
|
||||||
lastSuccessfulConfirmationUpdateByPeer[peerNodeId] = confirmations.Max(confirmation => confirmation.LastConfirmedUtc);
|
lastSuccessfulConfirmationUpdateByPeer[peerNodeId] =
|
||||||
|
confirmations.Max(confirmation => confirmation.LastConfirmedUtc);
|
||||||
|
|
||||||
if (lagMs > lagThresholdMs)
|
if (lagMs > lagThresholdMs) laggingPeers.Add(peerNodeId);
|
||||||
{
|
|
||||||
laggingPeers.Add(peerNodeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lagMs > criticalLagThresholdMs)
|
if (lagMs > criticalLagThresholdMs) criticalLaggingPeers.Add(peerNodeId);
|
||||||
{
|
|
||||||
criticalLaggingPeers.Add(peerNodeId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>
|
var payload = new Dictionary<string, object>
|
||||||
@@ -108,18 +102,14 @@ public class CBDDCHealthCheck : IHealthCheck
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (criticalLaggingPeers.Count > 0)
|
if (criticalLaggingPeers.Count > 0)
|
||||||
{
|
|
||||||
return HealthCheckResult.Unhealthy(
|
return HealthCheckResult.Unhealthy(
|
||||||
$"CBDDC is unhealthy. Critical lag detected for {criticalLaggingPeers.Count} tracked peer(s).",
|
$"CBDDC is unhealthy. Critical lag detected for {criticalLaggingPeers.Count} tracked peer(s).",
|
||||||
data: payload);
|
data: payload);
|
||||||
}
|
|
||||||
|
|
||||||
if (peersWithNoConfirmation.Count > 0 || laggingPeers.Count > 0)
|
if (peersWithNoConfirmation.Count > 0 || laggingPeers.Count > 0)
|
||||||
{
|
|
||||||
return HealthCheckResult.Degraded(
|
return HealthCheckResult.Degraded(
|
||||||
$"CBDDC is degraded. Lagging peers: {laggingPeers.Count}, unconfirmed peers: {peersWithNoConfirmation.Count}.",
|
$"CBDDC is degraded. Lagging peers: {laggingPeers.Count}, unconfirmed peers: {peersWithNoConfirmation.Count}.",
|
||||||
data: payload);
|
data: payload);
|
||||||
}
|
|
||||||
|
|
||||||
return HealthCheckResult.Healthy(
|
return HealthCheckResult.Healthy(
|
||||||
$"CBDDC is healthy. Latest timestamp: {localHead.PhysicalTime}.",
|
$"CBDDC is healthy. Latest timestamp: {localHead.PhysicalTime}.",
|
||||||
@@ -129,7 +119,7 @@ public class CBDDCHealthCheck : IHealthCheck
|
|||||||
{
|
{
|
||||||
return HealthCheckResult.Unhealthy(
|
return HealthCheckResult.Unhealthy(
|
||||||
"CBDDC persistence layer is unavailable",
|
"CBDDC persistence layer is unavailable",
|
||||||
exception: ex);
|
ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
@@ -8,7 +6,7 @@ using ZB.MOM.WW.CBDDC.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
namespace ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hosted service that manages the lifecycle of the discovery service.
|
/// Hosted service that manages the lifecycle of the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DiscoveryServiceHostedService : IHostedService
|
public class DiscoveryServiceHostedService : IHostedService
|
||||||
{
|
{
|
||||||
@@ -16,7 +14,7 @@ public class DiscoveryServiceHostedService : IHostedService
|
|||||||
private readonly ILogger<DiscoveryServiceHostedService> _logger;
|
private readonly ILogger<DiscoveryServiceHostedService> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DiscoveryServiceHostedService"/> class.
|
/// Initializes a new instance of the <see cref="DiscoveryServiceHostedService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="discoveryService">The discovery service to manage.</param>
|
/// <param name="discoveryService">The discovery service to manage.</param>
|
||||||
/// <param name="logger">The logger used for service lifecycle events.</param>
|
/// <param name="logger">The logger used for service lifecycle events.</param>
|
||||||
@@ -29,7 +27,7 @@ public class DiscoveryServiceHostedService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the discovery service.
|
/// Starts the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the startup operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the startup operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
@@ -45,7 +43,7 @@ public class DiscoveryServiceHostedService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the discovery service.
|
/// Stops the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel the shutdown operation.</param>
|
/// <param name="cancellationToken">A token used to cancel the shutdown operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
@@ -59,4 +57,4 @@ public class DiscoveryServiceHostedService : IHostedService
|
|||||||
await _discoveryService.Stop();
|
await _discoveryService.Stop();
|
||||||
_logger.LogInformation("Discovery Service stopped");
|
_logger.LogInformation("Discovery Service stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
@@ -8,15 +6,15 @@ using ZB.MOM.WW.CBDDC.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
namespace ZB.MOM.WW.CBDDC.Hosting.HostedServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hosted service that manages the lifecycle of the TCP sync server.
|
/// Hosted service that manages the lifecycle of the TCP sync server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TcpSyncServerHostedService : IHostedService
|
public class TcpSyncServerHostedService : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ISyncServer _syncServer;
|
|
||||||
private readonly ILogger<TcpSyncServerHostedService> _logger;
|
private readonly ILogger<TcpSyncServerHostedService> _logger;
|
||||||
|
private readonly ISyncServer _syncServer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TcpSyncServerHostedService"/> class.
|
/// Initializes a new instance of the <see cref="TcpSyncServerHostedService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="syncServer">The sync server to start and stop.</param>
|
/// <param name="syncServer">The sync server to start and stop.</param>
|
||||||
/// <param name="logger">The logger instance.</param>
|
/// <param name="logger">The logger instance.</param>
|
||||||
@@ -29,7 +27,7 @@ public class TcpSyncServerHostedService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the TCP sync server.
|
/// Starts the TCP sync server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel startup.</param>
|
/// <param name="cancellationToken">A token used to cancel startup.</param>
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -44,7 +42,7 @@ public class TcpSyncServerHostedService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the TCP sync server.
|
/// Stops the TCP sync server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel shutdown.</param>
|
/// <param name="cancellationToken">A token used to cancel shutdown.</param>
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
@@ -57,4 +55,4 @@ public class TcpSyncServerHostedService : IHostedService
|
|||||||
await _syncServer.Stop();
|
await _syncServer.Stop();
|
||||||
_logger.LogInformation("TCP Sync Server stopped");
|
_logger.LogInformation("TCP Sync Server stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,7 @@ app.Run();
|
|||||||
## Health Checks
|
## Health Checks
|
||||||
|
|
||||||
CBDDC registers health checks that verify:
|
CBDDC registers health checks that verify:
|
||||||
|
|
||||||
- Database connectivity
|
- Database connectivity
|
||||||
- Latest timestamp retrieval
|
- Latest timestamp retrieval
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ curl http://localhost:5000/health
|
|||||||
### Cluster
|
### Cluster
|
||||||
|
|
||||||
Best for:
|
Best for:
|
||||||
|
|
||||||
- Dedicated database servers
|
- Dedicated database servers
|
||||||
- Simple deployments
|
- Simple deployments
|
||||||
- Development/testing environments
|
- Development/testing environments
|
||||||
@@ -60,6 +62,7 @@ Best for:
|
|||||||
## Server Behavior
|
## Server Behavior
|
||||||
|
|
||||||
CBDDC servers operate in respond-only mode:
|
CBDDC servers operate in respond-only mode:
|
||||||
|
|
||||||
- Accept incoming sync connections
|
- Accept incoming sync connections
|
||||||
- Respond to sync requests
|
- Respond to sync requests
|
||||||
- Do not initiate outbound sync
|
- Do not initiate outbound sync
|
||||||
@@ -69,11 +72,11 @@ CBDDC servers operate in respond-only mode:
|
|||||||
|
|
||||||
### ClusterOptions
|
### ClusterOptions
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
| Property | Type | Default | Description |
|
||||||
|----------|------|---------|-------------|
|
|--------------------|--------|-------------|------------------------|
|
||||||
| NodeId | string | MachineName | Unique node identifier |
|
| NodeId | string | MachineName | Unique node identifier |
|
||||||
| TcpPort | int | 5001 | TCP port for sync |
|
| TcpPort | int | 5001 | TCP port for sync |
|
||||||
| EnableUdpDiscovery | bool | false | Enable UDP discovery |
|
| EnableUdpDiscovery | bool | false | Enable UDP discovery |
|
||||||
|
|
||||||
## Production Checklist
|
## Production Checklist
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
@@ -9,24 +7,24 @@ using ZB.MOM.WW.CBDDC.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.Services;
|
namespace ZB.MOM.WW.CBDDC.Hosting.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// No-op implementation of IDiscoveryService for server scenarios.
|
/// No-op implementation of IDiscoveryService for server scenarios.
|
||||||
/// Does not perform UDP broadcast discovery - relies on explicit peer configuration.
|
/// Does not perform UDP broadcast discovery - relies on explicit peer configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NoOpDiscoveryService : IDiscoveryService
|
public class NoOpDiscoveryService : IDiscoveryService
|
||||||
{
|
{
|
||||||
private readonly ILogger<NoOpDiscoveryService> _logger;
|
private readonly ILogger<NoOpDiscoveryService> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="NoOpDiscoveryService"/> class.
|
/// Initializes a new instance of the <see cref="NoOpDiscoveryService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger instance to use, or <see langword="null"/> to use a no-op logger.</param>
|
/// <param name="logger">The logger instance to use, or <see langword="null" /> to use a no-op logger.</param>
|
||||||
public NoOpDiscoveryService(ILogger<NoOpDiscoveryService>? logger = null)
|
public NoOpDiscoveryService(ILogger<NoOpDiscoveryService>? logger = null)
|
||||||
{
|
{
|
||||||
_logger = logger ?? NullLogger<NoOpDiscoveryService>.Instance;
|
_logger = logger ?? NullLogger<NoOpDiscoveryService>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the currently active peers.
|
/// Gets the currently active peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>An empty sequence because discovery is disabled.</returns>
|
/// <returns>An empty sequence because discovery is disabled.</returns>
|
||||||
public IEnumerable<PeerNode> GetActivePeers()
|
public IEnumerable<PeerNode> GetActivePeers()
|
||||||
@@ -35,7 +33,7 @@ public class NoOpDiscoveryService : IDiscoveryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the discovery service.
|
/// Starts the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A completed task.</returns>
|
/// <returns>A completed task.</returns>
|
||||||
public Task Start()
|
public Task Start()
|
||||||
@@ -49,7 +47,7 @@ public class NoOpDiscoveryService : IDiscoveryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the discovery service.
|
/// Stops the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A completed task.</returns>
|
/// <returns>A completed task.</returns>
|
||||||
public Task Stop()
|
public Task Stop()
|
||||||
@@ -63,10 +61,10 @@ public class NoOpDiscoveryService : IDiscoveryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases resources used by this instance.
|
/// Releases resources used by this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_logger.LogDebug("NoOpDiscoveryService disposed");
|
_logger.LogDebug("NoOpDiscoveryService disposed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
@@ -7,24 +6,24 @@ using ZB.MOM.WW.CBDDC.Network;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Hosting.Services;
|
namespace ZB.MOM.WW.CBDDC.Hosting.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// No-op implementation of ISyncOrchestrator for server scenarios.
|
/// No-op implementation of ISyncOrchestrator for server scenarios.
|
||||||
/// Does not initiate outbound sync - only responds to incoming sync requests.
|
/// Does not initiate outbound sync - only responds to incoming sync requests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NoOpSyncOrchestrator : ISyncOrchestrator
|
public class NoOpSyncOrchestrator : ISyncOrchestrator
|
||||||
{
|
{
|
||||||
private readonly ILogger<NoOpSyncOrchestrator> _logger;
|
private readonly ILogger<NoOpSyncOrchestrator> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="NoOpSyncOrchestrator"/> class.
|
/// Initializes a new instance of the <see cref="NoOpSyncOrchestrator" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger instance to use, or <see langword="null"/> for a no-op logger.</param>
|
/// <param name="logger">The logger instance to use, or <see langword="null" /> for a no-op logger.</param>
|
||||||
public NoOpSyncOrchestrator(ILogger<NoOpSyncOrchestrator>? logger = null)
|
public NoOpSyncOrchestrator(ILogger<NoOpSyncOrchestrator>? logger = null)
|
||||||
{
|
{
|
||||||
_logger = logger ?? NullLogger<NoOpSyncOrchestrator>.Instance;
|
_logger = logger ?? NullLogger<NoOpSyncOrchestrator>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the orchestrator lifecycle.
|
/// Starts the orchestrator lifecycle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A completed task.</returns>
|
/// <returns>A completed task.</returns>
|
||||||
public Task Start()
|
public Task Start()
|
||||||
@@ -38,7 +37,7 @@ public class NoOpSyncOrchestrator : ISyncOrchestrator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the orchestrator lifecycle.
|
/// Stops the orchestrator lifecycle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A completed task.</returns>
|
/// <returns>A completed task.</returns>
|
||||||
public Task Stop()
|
public Task Stop()
|
||||||
@@ -52,10 +51,10 @@ public class NoOpSyncOrchestrator : ISyncOrchestrator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases resources used by the orchestrator.
|
/// Releases resources used by the orchestrator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_logger.LogDebug("NoOpSyncOrchestrator disposed");
|
_logger.LogDebug("NoOpSyncOrchestrator disposed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,37 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.CBDDC.Network\ZB.MOM.WW.CBDDC.Network.csproj" />
|
<ProjectReference Include="..\ZB.MOM.WW.CBDDC.Network\ZB.MOM.WW.CBDDC.Network.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>ZB.MOM.WW.CBDDC.Hosting</AssemblyName>
|
<AssemblyName>ZB.MOM.WW.CBDDC.Hosting</AssemblyName>
|
||||||
<RootNamespace>ZB.MOM.WW.CBDDC.Hosting</RootNamespace>
|
<RootNamespace>ZB.MOM.WW.CBDDC.Hosting</RootNamespace>
|
||||||
<PackageId>ZB.MOM.WW.CBDDC.Hosting</PackageId>
|
<PackageId>ZB.MOM.WW.CBDDC.Hosting</PackageId>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>1.0.3</Version>
|
<Version>1.0.3</Version>
|
||||||
<Authors>MrDevRobot</Authors>
|
<Authors>MrDevRobot</Authors>
|
||||||
<Description>ASP.NET Core integration for CBDDC with health checks and hosted services.</Description>
|
<Description>ASP.NET Core integration for CBDDC with health checks and hosted services.</Description>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageTags>p2p;database;aspnetcore;healthcheck;hosting;cluster</PackageTags>
|
<PackageTags>p2p;database;aspnetcore;healthcheck;hosting;cluster</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,37 +1,21 @@
|
|||||||
using System;
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a single CBDDC Peer Node.
|
/// Represents a single CBDDC Peer Node.
|
||||||
/// Acts as a facade to orchestrate the lifecycle of Networking, Discovery, and Synchronization components.
|
/// Acts as a facade to orchestrate the lifecycle of Networking, Discovery, and Synchronization components.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCNode : ICBDDCNode
|
public class CBDDCNode : ICBDDCNode
|
||||||
{
|
{
|
||||||
private readonly ILogger<CBDDCNode> _logger;
|
private readonly ILogger<CBDDCNode> _logger;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the Sync Server instance.
|
|
||||||
/// </summary>
|
|
||||||
public ISyncServer Server { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the Discovery Service instance.
|
/// Initializes a new instance of the <see cref="CBDDCNode" /> class.
|
||||||
/// </summary>
|
|
||||||
public IDiscoveryService Discovery { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the Synchronization Orchestrator instance.
|
|
||||||
/// </summary>
|
|
||||||
public ISyncOrchestrator Orchestrator { get; }
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="CBDDCNode"/> class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="server">The TCP server for handling incoming sync requests.</param>
|
/// <param name="server">The TCP server for handling incoming sync requests.</param>
|
||||||
/// <param name="discovery">The UDP service for peer discovery.</param>
|
/// <param name="discovery">The UDP service for peer discovery.</param>
|
||||||
@@ -50,7 +34,22 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts all node components (Server, Discovery, Orchestrator).
|
/// Gets the Sync Server instance.
|
||||||
|
/// </summary>
|
||||||
|
public ISyncServer Server { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Discovery Service instance.
|
||||||
|
/// </summary>
|
||||||
|
public IDiscoveryService Discovery { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Synchronization Orchestrator instance.
|
||||||
|
/// </summary>
|
||||||
|
public ISyncOrchestrator Orchestrator { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts all node components (Server, Discovery, Orchestrator).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
@@ -66,7 +65,7 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops all node components.
|
/// Stops all node components.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task Stop()
|
public async Task Stop()
|
||||||
{
|
{
|
||||||
@@ -82,7 +81,7 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the address information of this node.
|
/// Gets the address information of this node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NodeAddress Address
|
public NodeAddress Address
|
||||||
{
|
{
|
||||||
@@ -93,12 +92,11 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
{
|
{
|
||||||
// If the server is listening on "Any" (0.0.0.0), we cannot advertise that as a connectable address.
|
// If the server is listening on "Any" (0.0.0.0), we cannot advertise that as a connectable address.
|
||||||
// We must resolve the actual machine IP address that peers can reach.
|
// We must resolve the actual machine IP address that peers can reach.
|
||||||
if (Equals(ep.Address, System.Net.IPAddress.Any) || Equals(ep.Address, System.Net.IPAddress.IPv6Any))
|
if (Equals(ep.Address, IPAddress.Any) || Equals(ep.Address, IPAddress.IPv6Any))
|
||||||
{
|
|
||||||
return new NodeAddress(GetLocalIpAddress(), ep.Port);
|
return new NodeAddress(GetLocalIpAddress(), ep.Port);
|
||||||
}
|
|
||||||
return new NodeAddress(ep.Address.ToString(), ep.Port);
|
return new NodeAddress(ep.Address.ToString(), ep.Port);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new NodeAddress("Unknown", 0);
|
return new NodeAddress("Unknown", 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,20 +105,17 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var interfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()
|
var interfaces = NetworkInterface.GetAllNetworkInterfaces()
|
||||||
.Where(i => i.OperationalStatus == System.Net.NetworkInformation.OperationalStatus.Up
|
.Where(i => i.OperationalStatus == OperationalStatus.Up
|
||||||
&& i.NetworkInterfaceType != System.Net.NetworkInformation.NetworkInterfaceType.Loopback);
|
&& i.NetworkInterfaceType != NetworkInterfaceType.Loopback);
|
||||||
|
|
||||||
foreach (var i in interfaces)
|
foreach (var i in interfaces)
|
||||||
{
|
{
|
||||||
var props = i.GetIPProperties();
|
var props = i.GetIPProperties();
|
||||||
var ipInfo = props.UnicastAddresses
|
var ipInfo = props.UnicastAddresses
|
||||||
.FirstOrDefault(u => u.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork); // Prefer IPv4
|
.FirstOrDefault(u => u.Address.AddressFamily == AddressFamily.InterNetwork); // Prefer IPv4
|
||||||
|
|
||||||
if (ipInfo != null)
|
if (ipInfo != null) return ipInfo.Address.ToString();
|
||||||
{
|
|
||||||
return ipInfo.Address.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "127.0.0.1";
|
return "127.0.0.1";
|
||||||
@@ -133,28 +128,32 @@ public class CBDDCNode : ICBDDCNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NodeAddress
|
public class NodeAddress
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the host portion of the node address.
|
/// Initializes a new instance of the <see cref="NodeAddress" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Host { get; }
|
/// <param name="host">The host name or IP address.</param>
|
||||||
/// <summary>
|
/// <param name="port">The port number.</param>
|
||||||
/// Gets the port portion of the node address.
|
public NodeAddress(string host, int port)
|
||||||
/// </summary>
|
{
|
||||||
public int Port { get; }
|
Host = host;
|
||||||
|
Port = port;
|
||||||
/// <summary>
|
}
|
||||||
/// Initializes a new instance of the <see cref="NodeAddress"/> class.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="host">The host name or IP address.</param>
|
/// Gets the host portion of the node address.
|
||||||
/// <param name="port">The port number.</param>
|
/// </summary>
|
||||||
public NodeAddress(string host, int port)
|
public string Host { get; }
|
||||||
{
|
|
||||||
Host = host;
|
/// <summary>
|
||||||
Port = port;
|
/// Gets the port portion of the node address.
|
||||||
}
|
/// </summary>
|
||||||
|
public int Port { get; }
|
||||||
/// <inheritdoc />
|
|
||||||
public override string ToString() => $"{Host}:{Port}";
|
/// <inheritdoc />
|
||||||
}
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{Host}:{Port}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog.Context;
|
using Serilog.Context;
|
||||||
using System;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hosted service that automatically starts and stops the CBDDC node.
|
/// Hosted service that automatically starts and stops the CBDDC node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CBDDCNodeService : IHostedService
|
public class CBDDCNodeService : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ICBDDCNode _node;
|
|
||||||
private readonly ILogger<CBDDCNodeService> _logger;
|
private readonly ILogger<CBDDCNodeService> _logger;
|
||||||
|
private readonly ICBDDCNode _node;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CBDDCNodeService"/> class.
|
/// Initializes a new instance of the <see cref="CBDDCNodeService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="node">The CBDDC node to manage.</param>
|
/// <param name="node">The CBDDC node to manage.</param>
|
||||||
/// <param name="logger">The logger instance.</param>
|
/// <param name="logger">The logger instance.</param>
|
||||||
@@ -27,7 +24,7 @@ public class CBDDCNodeService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the managed CBDDC node.
|
/// Starts the managed CBDDC node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel startup.</param>
|
/// <param name="cancellationToken">A token used to cancel startup.</param>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
@@ -60,7 +57,7 @@ public class CBDDCNodeService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the managed CBDDC node.
|
/// Stops the managed CBDDC node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">A token used to cancel shutdown.</param>
|
/// <param name="cancellationToken">A token used to cancel shutdown.</param>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
@@ -82,4 +79,4 @@ public class CBDDCNodeService : IHostedService
|
|||||||
// Don't rethrow during shutdown to avoid breaking the shutdown process
|
// Don't rethrow during shutdown to avoid breaking the shutdown process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,15 +8,15 @@ using ZB.MOM.WW.CBDDC.Core.Sync;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides extension methods for registering core CBDDC services.
|
/// Provides extension methods for registering core CBDDC services.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CBDDCServiceCollectionExtensions
|
public static class CBDDCServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers core CBDDC service dependencies.
|
/// Registers core CBDDC service dependencies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services">The service collection to update.</param>
|
/// <param name="services">The service collection to update.</param>
|
||||||
/// <returns>The same <see cref="IServiceCollection"/> instance for chaining.</returns>
|
/// <returns>The same <see cref="IServiceCollection" /> instance for chaining.</returns>
|
||||||
public static IServiceCollection AddCBDDCCore(this IServiceCollection services)
|
public static IServiceCollection AddCBDDCCore(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(services);
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
@@ -29,4 +29,4 @@ public static class CBDDCServiceCollectionExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,33 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Composite discovery service that combines UDP LAN discovery with persistent remote peers from the peerConfigurationStore.
|
/// Composite discovery service that combines UDP LAN discovery with persistent remote peers from the
|
||||||
/// Periodically refreshes the remote peer list and merges with actively discovered LAN peers.
|
/// peerConfigurationStore.
|
||||||
///
|
/// Periodically refreshes the remote peer list and merges with actively discovered LAN peers.
|
||||||
/// Remote peer configurations are stored in a synchronized collection that is automatically
|
/// Remote peer configurations are stored in a synchronized collection that is automatically
|
||||||
/// replicated across all nodes in the cluster. Any node that adds a remote peer will have
|
/// replicated across all nodes in the cluster. Any node that adds a remote peer will have
|
||||||
/// it synchronized to all other nodes automatically.
|
/// it synchronized to all other nodes automatically.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CompositeDiscoveryService : IDiscoveryService
|
public class CompositeDiscoveryService : IDiscoveryService
|
||||||
{
|
{
|
||||||
private readonly IDiscoveryService _udpDiscovery;
|
|
||||||
private readonly IPeerConfigurationStore _peerConfigurationStore;
|
|
||||||
private readonly ILogger<CompositeDiscoveryService> _logger;
|
|
||||||
private readonly TimeSpan _refreshInterval;
|
|
||||||
private const string RemotePeersCollectionName = "_system_remote_peers";
|
private const string RemotePeersCollectionName = "_system_remote_peers";
|
||||||
|
private readonly ILogger<CompositeDiscoveryService> _logger;
|
||||||
|
private readonly IPeerConfigurationStore _peerConfigurationStore;
|
||||||
|
private readonly TimeSpan _refreshInterval;
|
||||||
|
private readonly ConcurrentDictionary<string, PeerNode> _remotePeers = new();
|
||||||
|
private readonly object _startStopLock = new();
|
||||||
|
private readonly IDiscoveryService _udpDiscovery;
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private readonly ConcurrentDictionary<string, PeerNode> _remotePeers = new();
|
|
||||||
private readonly object _startStopLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the CompositeDiscoveryService class.
|
/// Initializes a new instance of the CompositeDiscoveryService class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="udpDiscovery">UDP-based LAN discovery service.</param>
|
/// <param name="udpDiscovery">UDP-based LAN discovery service.</param>
|
||||||
/// <param name="peerConfigurationStore">Database instance for accessing the synchronized remote peers collection.</param>
|
/// <param name="peerConfigurationStore">Database instance for accessing the synchronized remote peers collection.</param>
|
||||||
@@ -45,13 +40,14 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
TimeSpan? refreshInterval = null)
|
TimeSpan? refreshInterval = null)
|
||||||
{
|
{
|
||||||
_udpDiscovery = udpDiscovery ?? throw new ArgumentNullException(nameof(udpDiscovery));
|
_udpDiscovery = udpDiscovery ?? throw new ArgumentNullException(nameof(udpDiscovery));
|
||||||
_peerConfigurationStore = peerConfigurationStore ?? throw new ArgumentNullException(nameof(peerConfigurationStore));
|
_peerConfigurationStore =
|
||||||
|
peerConfigurationStore ?? throw new ArgumentNullException(nameof(peerConfigurationStore));
|
||||||
_logger = logger ?? NullLogger<CompositeDiscoveryService>.Instance;
|
_logger = logger ?? NullLogger<CompositeDiscoveryService>.Instance;
|
||||||
_refreshInterval = refreshInterval ?? TimeSpan.FromMinutes(5);
|
_refreshInterval = refreshInterval ?? TimeSpan.FromMinutes(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the currently active peers from LAN discovery and configured remote peers.
|
/// Gets the currently active peers from LAN discovery and configured remote peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A sequence of active peer nodes.</returns>
|
/// <returns>A sequence of active peer nodes.</returns>
|
||||||
public IEnumerable<PeerNode> GetActivePeers()
|
public IEnumerable<PeerNode> GetActivePeers()
|
||||||
@@ -64,7 +60,7 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts peer discovery and the remote peer refresh loop.
|
/// Starts peer discovery and the remote peer refresh loop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
@@ -76,6 +72,7 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
_logger.LogWarning("Composite discovery service already started");
|
_logger.LogWarning("Composite discovery service already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +100,7 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops peer discovery and the remote peer refresh loop.
|
/// Stops peer discovery and the remote peer refresh loop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
public async Task Stop()
|
public async Task Stop()
|
||||||
@@ -143,7 +140,6 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
private async Task RefreshLoopAsync(CancellationToken cancellationToken)
|
private async Task RefreshLoopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(_refreshInterval, cancellationToken);
|
await Task.Delay(_refreshInterval, cancellationToken);
|
||||||
@@ -158,7 +154,6 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error during remote peer refresh");
|
_logger.LogError(ex, "Error during remote peer refresh");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshRemotePeersAsync()
|
private async Task RefreshRemotePeersAsync()
|
||||||
@@ -178,18 +173,18 @@ public class CompositeDiscoveryService : IDiscoveryService
|
|||||||
config.NodeId,
|
config.NodeId,
|
||||||
config.Address,
|
config.Address,
|
||||||
now, // LastSeen is now for persistent peers (always considered active)
|
now, // LastSeen is now for persistent peers (always considered active)
|
||||||
config.Type,
|
config.Type // Remote peers are always members, never gateways
|
||||||
NodeRole.Member // Remote peers are always members, never gateways
|
|
||||||
);
|
);
|
||||||
|
|
||||||
_remotePeers[config.NodeId] = peerNode;
|
_remotePeers[config.NodeId] = peerNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Refreshed remote peers: {Count} enabled peers loaded from synchronized collection", _remotePeers.Count);
|
_logger.LogInformation("Refreshed remote peers: {Count} enabled peers loaded from synchronized collection",
|
||||||
|
_remotePeers.Count);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to refresh remote peers from database");
|
_logger.LogError(ex, "Failed to refresh remote peers from database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
public static class CompressionHelper
|
public static class CompressionHelper
|
||||||
{
|
{
|
||||||
public const int THRESHOLD = 1024; // 1KB
|
public const int THRESHOLD = 1024; // 1KB
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether Brotli compression is supported on the current target framework.
|
/// Gets a value indicating whether Brotli compression is supported on the current target framework.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsBrotliSupported
|
public static bool IsBrotliSupported
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
@@ -20,16 +18,16 @@ public static class CompressionHelper
|
|||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Compresses the specified data when Brotli is supported and the payload exceeds the threshold.
|
/// Compresses the specified data when Brotli is supported and the payload exceeds the threshold.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data">The input data to compress.</param>
|
/// <param name="data">The input data to compress.</param>
|
||||||
/// <returns>The compressed payload, or the original payload if compression is skipped.</returns>
|
/// <returns>The compressed payload, or the original payload if compression is skipped.</returns>
|
||||||
public static byte[] Compress(byte[] data)
|
public static byte[] Compress(byte[] data)
|
||||||
{
|
{
|
||||||
if (data.Length < THRESHOLD || !IsBrotliSupported) return data;
|
if (data.Length < THRESHOLD || !IsBrotliSupported) return data;
|
||||||
|
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
@@ -38,19 +36,20 @@ public static class CompressionHelper
|
|||||||
{
|
{
|
||||||
brotli.Write(data, 0, data.Length);
|
brotli.Write(data, 0, data.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.ToArray();
|
return output.ToArray();
|
||||||
#else
|
#else
|
||||||
return data;
|
return data;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompresses Brotli-compressed data.
|
/// Decompresses Brotli-compressed data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="compressedData">The compressed payload.</param>
|
/// <param name="compressedData">The compressed payload.</param>
|
||||||
/// <returns>The decompressed payload.</returns>
|
/// <returns>The decompressed payload.</returns>
|
||||||
public static byte[] Decompress(byte[] compressedData)
|
public static byte[] Decompress(byte[] compressedData)
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
using var input = new MemoryStream(compressedData);
|
using var input = new MemoryStream(compressedData);
|
||||||
using var output = new MemoryStream();
|
using var output = new MemoryStream();
|
||||||
@@ -58,9 +57,10 @@ public static class CompressionHelper
|
|||||||
{
|
{
|
||||||
brotli.CopyTo(output);
|
brotli.CopyTo(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
return output.ToArray();
|
return output.ToArray();
|
||||||
#else
|
#else
|
||||||
throw new NotSupportedException("Brotli decompression not supported on this platform.");
|
throw new NotSupportedException("Brotli decompression not supported on this platform.");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,36 @@
|
|||||||
using System.Threading.Tasks;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network
|
|
||||||
{
|
|
||||||
public interface ICBDDCNode
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the node address.
|
|
||||||
/// </summary>
|
|
||||||
NodeAddress Address { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the discovery service.
|
|
||||||
/// </summary>
|
|
||||||
IDiscoveryService Discovery { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the synchronization orchestrator.
|
|
||||||
/// </summary>
|
|
||||||
ISyncOrchestrator Orchestrator { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the synchronization server.
|
|
||||||
/// </summary>
|
|
||||||
ISyncServer Server { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
public interface ICBDDCNode
|
||||||
/// Starts the node services.
|
{
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// Gets the node address.
|
||||||
Task Start();
|
/// </summary>
|
||||||
/// <summary>
|
NodeAddress Address { get; }
|
||||||
/// Stops the node services.
|
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// Gets the discovery service.
|
||||||
Task Stop();
|
/// </summary>
|
||||||
}
|
IDiscoveryService Discovery { get; }
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the synchronization orchestrator.
|
||||||
|
/// </summary>
|
||||||
|
ISyncOrchestrator Orchestrator { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the synchronization server.
|
||||||
|
/// </summary>
|
||||||
|
ISyncServer Server { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the node services.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
|
Task Start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the node services.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
|
Task Stop();
|
||||||
|
}
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network
|
/// <summary>
|
||||||
|
/// Defines peer discovery operations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IDiscoveryService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines peer discovery operations.
|
/// Gets the currently active peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDiscoveryService
|
/// <returns>The active peer nodes.</returns>
|
||||||
{
|
IEnumerable<PeerNode> GetActivePeers();
|
||||||
/// <summary>
|
|
||||||
/// Gets the currently active peers.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The active peer nodes.</returns>
|
|
||||||
IEnumerable<PeerNode> GetActivePeers();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the discovery service.
|
/// Starts the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Start();
|
Task Start();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the discovery service.
|
/// Stops the discovery service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Stop();
|
Task Stop();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the effective oplog prune cutoff for maintenance.
|
/// Calculates the effective oplog prune cutoff for maintenance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IOplogPruneCutoffCalculator
|
public interface IOplogPruneCutoffCalculator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates the effective prune cutoff for the provided node configuration.
|
/// Calculates the effective prune cutoff for the provided node configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">The local node configuration.</param>
|
/// <param name="configuration">The local node configuration.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -18,4 +16,4 @@ public interface IOplogPruneCutoffCalculator
|
|||||||
Task<OplogPruneCutoffDecision> CalculateEffectiveCutoffAsync(
|
Task<OplogPruneCutoffDecision> CalculateEffectiveCutoffAsync(
|
||||||
PeerNodeConfiguration configuration,
|
PeerNodeConfiguration configuration,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,19 @@
|
|||||||
using System.Threading.Tasks;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network
|
/// <summary>
|
||||||
|
/// Defines lifecycle operations for synchronization orchestration.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISyncOrchestrator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Defines lifecycle operations for synchronization orchestration.
|
/// Starts synchronization orchestration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISyncOrchestrator
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
{
|
Task Start();
|
||||||
/// <summary>
|
|
||||||
/// Starts synchronization orchestration.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
|
||||||
Task Start();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops synchronization orchestration.
|
/// Stops synchronization orchestration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
Task Stop();
|
Task Stop();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -1,31 +1,33 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
|
||||||
|
/// <summary>
|
||||||
/// <summary>
|
/// Defines the contract for a server that supports starting, stopping, and reporting its listening network endpoint
|
||||||
/// Defines the contract for a server that supports starting, stopping, and reporting its listening network endpoint for
|
/// for
|
||||||
/// synchronization operations.
|
/// synchronization operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Implementations of this interface are expected to provide asynchronous methods for starting and
|
/// <remarks>
|
||||||
/// stopping the server. The listening endpoint may be null if the server is not currently active or has not been
|
/// Implementations of this interface are expected to provide asynchronous methods for starting and
|
||||||
/// started.</remarks>
|
/// stopping the server. The listening endpoint may be null if the server is not currently active or has not been
|
||||||
|
/// started.
|
||||||
|
/// </remarks>
|
||||||
public interface ISyncServer
|
public interface ISyncServer
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the synchronization server.
|
/// Gets the network endpoint currently used by the server for listening.
|
||||||
|
/// </summary>
|
||||||
|
IPEndPoint? ListeningEndpoint { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the synchronization server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Start();
|
Task Start();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the synchronization server.
|
/// Stops the synchronization server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
Task Stop();
|
Task Stop();
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Gets the network endpoint currently used by the server for listening.
|
|
||||||
/// </summary>
|
|
||||||
IPEndPoint? ListeningEndpoint { get; }
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,26 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Leadership;
|
namespace ZB.MOM.WW.CBDDC.Network.Leadership;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Implements the Bully algorithm for leader election.
|
/// Implements the Bully algorithm for leader election.
|
||||||
/// The node with the lexicographically smallest NodeId becomes the cloud gateway (leader).
|
/// The node with the lexicographically smallest NodeId becomes the cloud gateway (leader).
|
||||||
/// Elections run periodically (every 5 seconds) to adapt to cluster changes.
|
/// Elections run periodically (every 5 seconds) to adapt to cluster changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BullyLeaderElectionService : ILeaderElectionService
|
public class BullyLeaderElectionService : ILeaderElectionService
|
||||||
{
|
{
|
||||||
private readonly IDiscoveryService _discoveryService;
|
|
||||||
private readonly IPeerNodeConfigurationProvider _configProvider;
|
private readonly IPeerNodeConfigurationProvider _configProvider;
|
||||||
private readonly ILogger<BullyLeaderElectionService> _logger;
|
private readonly IDiscoveryService _discoveryService;
|
||||||
private readonly TimeSpan _electionInterval;
|
private readonly TimeSpan _electionInterval;
|
||||||
|
private readonly ILogger<BullyLeaderElectionService> _logger;
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private string? _localNodeId;
|
private string? _localNodeId;
|
||||||
private string? _currentGatewayNodeId;
|
|
||||||
private bool _isCloudGateway;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether this node is currently the cloud gateway leader.
|
/// Initializes a new instance of the BullyLeaderElectionService class.
|
||||||
/// </summary>
|
|
||||||
public bool IsCloudGateway => _isCloudGateway;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current gateway node identifier.
|
|
||||||
/// </summary>
|
|
||||||
public string? CurrentGatewayNodeId => _currentGatewayNodeId;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Occurs when leadership changes.
|
|
||||||
/// </summary>
|
|
||||||
public event EventHandler<LeadershipChangedEventArgs>? LeadershipChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the BullyLeaderElectionService class.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="discoveryService">Service providing active peer information.</param>
|
/// <param name="discoveryService">Service providing active peer information.</param>
|
||||||
/// <param name="configProvider">Provider for local node configuration.</param>
|
/// <param name="configProvider">Provider for local node configuration.</param>
|
||||||
@@ -61,7 +39,22 @@ public class BullyLeaderElectionService : ILeaderElectionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the leader election loop.
|
/// Gets a value indicating whether this node is currently the cloud gateway leader.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCloudGateway { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current gateway node identifier.
|
||||||
|
/// </summary>
|
||||||
|
public string? CurrentGatewayNodeId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when leadership changes.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LeadershipChangedEventArgs>? LeadershipChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the leader election loop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
@@ -82,7 +75,7 @@ public class BullyLeaderElectionService : ILeaderElectionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the leader election loop.
|
/// Stops the leader election loop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
public Task Stop()
|
public Task Stop()
|
||||||
@@ -100,7 +93,6 @@ public class BullyLeaderElectionService : ILeaderElectionService
|
|||||||
private async Task ElectionLoopAsync(CancellationToken cancellationToken)
|
private async Task ElectionLoopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
while (!cancellationToken.IsCancellationRequested)
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(_electionInterval, cancellationToken);
|
await Task.Delay(_electionInterval, cancellationToken);
|
||||||
@@ -115,7 +107,6 @@ public class BullyLeaderElectionService : ILeaderElectionService
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error during leader election");
|
_logger.LogError(ex, "Error during leader election");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RunElection()
|
private void RunElection()
|
||||||
@@ -132,35 +123,31 @@ public class BullyLeaderElectionService : ILeaderElectionService
|
|||||||
lanPeers.Add(_localNodeId);
|
lanPeers.Add(_localNodeId);
|
||||||
|
|
||||||
// Bully algorithm: smallest NodeId wins (lexicographic comparison)
|
// Bully algorithm: smallest NodeId wins (lexicographic comparison)
|
||||||
var newLeader = lanPeers.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
|
string? newLeader = lanPeers.OrderBy(id => id, StringComparer.Ordinal).FirstOrDefault();
|
||||||
|
|
||||||
if (newLeader == null)
|
if (newLeader == null)
|
||||||
{
|
|
||||||
// No peers available, local node is leader by default
|
// No peers available, local node is leader by default
|
||||||
newLeader = _localNodeId;
|
newLeader = _localNodeId;
|
||||||
}
|
|
||||||
|
|
||||||
// Check if leadership changed
|
// Check if leadership changed
|
||||||
if (newLeader != _currentGatewayNodeId)
|
if (newLeader != CurrentGatewayNodeId)
|
||||||
{
|
{
|
||||||
var wasLeader = _isCloudGateway;
|
bool wasLeader = IsCloudGateway;
|
||||||
_currentGatewayNodeId = newLeader;
|
CurrentGatewayNodeId = newLeader;
|
||||||
_isCloudGateway = newLeader == _localNodeId;
|
IsCloudGateway = newLeader == _localNodeId;
|
||||||
|
|
||||||
if (wasLeader != _isCloudGateway)
|
if (wasLeader != IsCloudGateway)
|
||||||
{
|
{
|
||||||
if (_isCloudGateway)
|
if (IsCloudGateway)
|
||||||
{
|
_logger.LogInformation(
|
||||||
_logger.LogInformation("🔐 This node is now the CLOUD GATEWAY (Leader) - Will sync with remote cloud nodes");
|
"🔐 This node is now the CLOUD GATEWAY (Leader) - Will sync with remote cloud nodes");
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
_logger.LogInformation("👤 This node is now a MEMBER - Cloud sync handled by gateway: {Gateway}",
|
||||||
_logger.LogInformation("👤 This node is now a MEMBER - Cloud sync handled by gateway: {Gateway}", _currentGatewayNodeId);
|
CurrentGatewayNodeId);
|
||||||
}
|
|
||||||
|
|
||||||
// Raise event
|
// Raise event
|
||||||
LeadershipChanged?.Invoke(this, new LeadershipChangedEventArgs(_currentGatewayNodeId, _isCloudGateway));
|
LeadershipChanged?.Invoke(this, new LeadershipChangedEventArgs(CurrentGatewayNodeId, IsCloudGateway));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,65 +1,65 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Leadership;
|
namespace ZB.MOM.WW.CBDDC.Network.Leadership;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event arguments for leadership change events.
|
/// Event arguments for leadership change events.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LeadershipChangedEventArgs : EventArgs
|
public class LeadershipChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the NodeId of the current cloud gateway (leader).
|
/// Initializes a new instance of the LeadershipChangedEventArgs class.
|
||||||
/// Null if no leader is elected.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CurrentGatewayNodeId { get; }
|
/// <param name="currentGatewayNodeId">
|
||||||
|
/// The NodeId of the current gateway node, or <see langword="null" /> when none is
|
||||||
/// <summary>
|
/// elected.
|
||||||
/// Gets whether the local node is now the cloud gateway.
|
/// </param>
|
||||||
/// </summary>
|
|
||||||
public bool IsLocalNodeGateway { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the LeadershipChangedEventArgs class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="currentGatewayNodeId">The NodeId of the current gateway node, or <see langword="null"/> when none is elected.</param>
|
|
||||||
/// <param name="isLocalNodeGateway">A value indicating whether the local node is the gateway.</param>
|
/// <param name="isLocalNodeGateway">A value indicating whether the local node is the gateway.</param>
|
||||||
public LeadershipChangedEventArgs(string? currentGatewayNodeId, bool isLocalNodeGateway)
|
public LeadershipChangedEventArgs(string? currentGatewayNodeId, bool isLocalNodeGateway)
|
||||||
{
|
{
|
||||||
CurrentGatewayNodeId = currentGatewayNodeId;
|
CurrentGatewayNodeId = currentGatewayNodeId;
|
||||||
IsLocalNodeGateway = isLocalNodeGateway;
|
IsLocalNodeGateway = isLocalNodeGateway;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the NodeId of the current cloud gateway (leader).
|
||||||
|
/// Null if no leader is elected.
|
||||||
|
/// </summary>
|
||||||
|
public string? CurrentGatewayNodeId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether the local node is now the cloud gateway.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLocalNodeGateway { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service for managing leader election in a distributed cluster.
|
/// Service for managing leader election in a distributed cluster.
|
||||||
/// Uses the Bully algorithm where the node with the lexicographically smallest NodeId becomes the leader.
|
/// Uses the Bully algorithm where the node with the lexicographically smallest NodeId becomes the leader.
|
||||||
/// Only the leader (Cloud Gateway) synchronizes with remote cloud nodes.
|
/// Only the leader (Cloud Gateway) synchronizes with remote cloud nodes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ILeaderElectionService
|
public interface ILeaderElectionService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the local node is currently the cloud gateway (leader).
|
/// Gets whether the local node is currently the cloud gateway (leader).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsCloudGateway { get; }
|
bool IsCloudGateway { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the NodeId of the current cloud gateway, or null if no gateway is elected.
|
/// Gets the NodeId of the current cloud gateway, or null if no gateway is elected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string? CurrentGatewayNodeId { get; }
|
string? CurrentGatewayNodeId { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event raised when leadership changes.
|
/// Event raised when leadership changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<LeadershipChangedEventArgs>? LeadershipChanged;
|
event EventHandler<LeadershipChangedEventArgs>? LeadershipChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the leader election service.
|
/// Starts the leader election service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task Start();
|
Task Start();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the leader election service.
|
/// Stops the leader election service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task Stop();
|
Task Stop();
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
@@ -10,7 +5,7 @@ using ZB.MOM.WW.CBDDC.Core.Storage;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default implementation for effective oplog prune cutoff calculation.
|
/// Default implementation for effective oplog prune cutoff calculation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
||||||
{
|
{
|
||||||
@@ -18,7 +13,7 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
private readonly IPeerOplogConfirmationStore? _peerOplogConfirmationStore;
|
private readonly IPeerOplogConfirmationStore? _peerOplogConfirmationStore;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="OplogPruneCutoffCalculator"/> class.
|
/// Initializes a new instance of the <see cref="OplogPruneCutoffCalculator" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="oplogStore">The oplog store.</param>
|
/// <param name="oplogStore">The oplog store.</param>
|
||||||
/// <param name="peerOplogConfirmationStore">The optional peer confirmation store.</param>
|
/// <param name="peerOplogConfirmationStore">The optional peer confirmation store.</param>
|
||||||
@@ -39,23 +34,19 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
|
|
||||||
var retentionCutoff = BuildRetentionCutoff(configuration);
|
var retentionCutoff = BuildRetentionCutoff(configuration);
|
||||||
if (_peerOplogConfirmationStore == null)
|
if (_peerOplogConfirmationStore == null)
|
||||||
{
|
|
||||||
return OplogPruneCutoffDecision.WithCutoff(
|
return OplogPruneCutoffDecision.WithCutoff(
|
||||||
retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: null,
|
null,
|
||||||
effectiveCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
reason: "Confirmation tracking is not configured.");
|
"Confirmation tracking is not configured.");
|
||||||
}
|
|
||||||
|
|
||||||
var relevantSources = await GetRelevantSourceNodesAsync(cancellationToken);
|
var relevantSources = await GetRelevantSourceNodesAsync(cancellationToken);
|
||||||
if (relevantSources.Count == 0)
|
if (relevantSources.Count == 0)
|
||||||
{
|
|
||||||
return OplogPruneCutoffDecision.WithCutoff(
|
return OplogPruneCutoffDecision.WithCutoff(
|
||||||
retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: null,
|
null,
|
||||||
effectiveCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
reason: "No local non-default oplog/vector-clock sources were found.");
|
"No local non-default oplog/vector-clock sources were found.");
|
||||||
}
|
|
||||||
|
|
||||||
var activeTrackedPeers = (await _peerOplogConfirmationStore.GetActiveTrackedPeersAsync(cancellationToken))
|
var activeTrackedPeers = (await _peerOplogConfirmationStore.GetActiveTrackedPeersAsync(cancellationToken))
|
||||||
.Where(peerNodeId => !string.IsNullOrWhiteSpace(peerNodeId))
|
.Where(peerNodeId => !string.IsNullOrWhiteSpace(peerNodeId))
|
||||||
@@ -63,19 +54,18 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (activeTrackedPeers.Count == 0)
|
if (activeTrackedPeers.Count == 0)
|
||||||
{
|
|
||||||
return OplogPruneCutoffDecision.WithCutoff(
|
return OplogPruneCutoffDecision.WithCutoff(
|
||||||
retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: null,
|
null,
|
||||||
effectiveCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
reason: "No active tracked peers found for confirmation gating.");
|
"No active tracked peers found for confirmation gating.");
|
||||||
}
|
|
||||||
|
|
||||||
HlcTimestamp? confirmationCutoff = null;
|
HlcTimestamp? confirmationCutoff = null;
|
||||||
|
|
||||||
foreach (var peerNodeId in activeTrackedPeers)
|
foreach (string peerNodeId in activeTrackedPeers)
|
||||||
{
|
{
|
||||||
var confirmationsForPeer = (await _peerOplogConfirmationStore.GetConfirmationsForPeerAsync(peerNodeId, cancellationToken))
|
var confirmationsForPeer =
|
||||||
|
(await _peerOplogConfirmationStore.GetConfirmationsForPeerAsync(peerNodeId, cancellationToken))
|
||||||
.Where(confirmation => confirmation.IsActive)
|
.Where(confirmation => confirmation.IsActive)
|
||||||
.Where(confirmation => !string.IsNullOrWhiteSpace(confirmation.SourceNodeId))
|
.Where(confirmation => !string.IsNullOrWhiteSpace(confirmation.SourceNodeId))
|
||||||
.GroupBy(confirmation => confirmation.SourceNodeId, StringComparer.Ordinal)
|
.GroupBy(confirmation => confirmation.SourceNodeId, StringComparer.Ordinal)
|
||||||
@@ -87,30 +77,25 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
.Last(),
|
.Last(),
|
||||||
StringComparer.Ordinal);
|
StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var sourceNodeId in relevantSources)
|
foreach (string sourceNodeId in relevantSources)
|
||||||
{
|
{
|
||||||
if (!confirmationsForPeer.TryGetValue(sourceNodeId, out var confirmedTimestamp) || confirmedTimestamp == default)
|
if (!confirmationsForPeer.TryGetValue(sourceNodeId, out var confirmedTimestamp) ||
|
||||||
{
|
confirmedTimestamp == default)
|
||||||
return OplogPruneCutoffDecision.NoCutoff(
|
return OplogPruneCutoffDecision.NoCutoff(
|
||||||
retentionCutoff,
|
retentionCutoff,
|
||||||
$"Active tracked peer '{peerNodeId}' is missing confirmation for source '{sourceNodeId}'.");
|
$"Active tracked peer '{peerNodeId}' is missing confirmation for source '{sourceNodeId}'.");
|
||||||
}
|
|
||||||
|
|
||||||
if (!confirmationCutoff.HasValue || confirmedTimestamp < confirmationCutoff.Value)
|
if (!confirmationCutoff.HasValue || confirmedTimestamp < confirmationCutoff.Value)
|
||||||
{
|
|
||||||
confirmationCutoff = confirmedTimestamp;
|
confirmationCutoff = confirmedTimestamp;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirmationCutoff.HasValue)
|
if (!confirmationCutoff.HasValue)
|
||||||
{
|
|
||||||
return OplogPruneCutoffDecision.WithCutoff(
|
return OplogPruneCutoffDecision.WithCutoff(
|
||||||
retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: null,
|
null,
|
||||||
effectiveCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
reason: "No confirmation cutoff could be determined.");
|
"No confirmation cutoff could be determined.");
|
||||||
}
|
|
||||||
|
|
||||||
var effectiveCutoff = retentionCutoff <= confirmationCutoff.Value
|
var effectiveCutoff = retentionCutoff <= confirmationCutoff.Value
|
||||||
? retentionCutoff
|
? retentionCutoff
|
||||||
@@ -124,7 +109,7 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
|
|
||||||
private static HlcTimestamp BuildRetentionCutoff(PeerNodeConfiguration configuration)
|
private static HlcTimestamp BuildRetentionCutoff(PeerNodeConfiguration configuration)
|
||||||
{
|
{
|
||||||
var retentionTimestamp = DateTimeOffset.UtcNow
|
long retentionTimestamp = DateTimeOffset.UtcNow
|
||||||
.AddHours(-configuration.OplogRetentionHours)
|
.AddHours(-configuration.OplogRetentionHours)
|
||||||
.ToUnixTimeMilliseconds();
|
.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
@@ -135,18 +120,12 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
{
|
{
|
||||||
var localVectorClock = await _oplogStore.GetVectorClockAsync(cancellationToken);
|
var localVectorClock = await _oplogStore.GetVectorClockAsync(cancellationToken);
|
||||||
var sourceNodes = new HashSet<string>(StringComparer.Ordinal);
|
var sourceNodes = new HashSet<string>(StringComparer.Ordinal);
|
||||||
foreach (var sourceNodeId in localVectorClock.NodeIds)
|
foreach (string sourceNodeId in localVectorClock.NodeIds)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(sourceNodeId))
|
if (string.IsNullOrWhiteSpace(sourceNodeId)) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var timestamp = localVectorClock.GetTimestamp(sourceNodeId);
|
var timestamp = localVectorClock.GetTimestamp(sourceNodeId);
|
||||||
if (timestamp == default)
|
if (timestamp == default) continue;
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceNodes.Add(sourceNodeId);
|
sourceNodes.Add(sourceNodeId);
|
||||||
}
|
}
|
||||||
@@ -161,4 +140,4 @@ public class OplogPruneCutoffCalculator : IOplogPruneCutoffCalculator
|
|||||||
confirmation.ConfirmedLogic,
|
confirmation.ConfirmedLogic,
|
||||||
confirmation.SourceNodeId ?? string.Empty);
|
confirmation.SourceNodeId ?? string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.CBDDC.Core;
|
|||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the prune cutoff decision for an oplog maintenance cycle.
|
/// Represents the prune cutoff decision for an oplog maintenance cycle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OplogPruneCutoffDecision
|
public sealed class OplogPruneCutoffDecision
|
||||||
{
|
{
|
||||||
@@ -22,32 +22,32 @@ public sealed class OplogPruneCutoffDecision
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether pruning is allowed for this decision.
|
/// Gets a value indicating whether pruning is allowed for this decision.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasCutoff { get; }
|
public bool HasCutoff { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the retention-based cutoff.
|
/// Gets the retention-based cutoff.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HlcTimestamp RetentionCutoff { get; }
|
public HlcTimestamp RetentionCutoff { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the confirmation-based cutoff, when available.
|
/// Gets the confirmation-based cutoff, when available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HlcTimestamp? ConfirmationCutoff { get; }
|
public HlcTimestamp? ConfirmationCutoff { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the effective cutoff to use for pruning when <see cref="HasCutoff"/> is true.
|
/// Gets the effective cutoff to use for pruning when <see cref="HasCutoff" /> is true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HlcTimestamp? EffectiveCutoff { get; }
|
public HlcTimestamp? EffectiveCutoff { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the explanatory reason for skip/special handling decisions.
|
/// Gets the explanatory reason for skip/special handling decisions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Reason { get; }
|
public string Reason { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a prune-allowed decision with the provided cutoffs.
|
/// Creates a prune-allowed decision with the provided cutoffs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="retentionCutoff">The cutoff derived from retention policy.</param>
|
/// <param name="retentionCutoff">The cutoff derived from retention policy.</param>
|
||||||
/// <param name="confirmationCutoff">The cutoff derived from peer confirmations, if available.</param>
|
/// <param name="confirmationCutoff">The cutoff derived from peer confirmations, if available.</param>
|
||||||
@@ -60,25 +60,25 @@ public sealed class OplogPruneCutoffDecision
|
|||||||
string reason = "")
|
string reason = "")
|
||||||
{
|
{
|
||||||
return new OplogPruneCutoffDecision(
|
return new OplogPruneCutoffDecision(
|
||||||
hasCutoff: true,
|
true,
|
||||||
retentionCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: confirmationCutoff,
|
confirmationCutoff,
|
||||||
effectiveCutoff: effectiveCutoff,
|
effectiveCutoff,
|
||||||
reason: reason);
|
reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a prune-blocked decision.
|
/// Creates a prune-blocked decision.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="retentionCutoff">The cutoff derived from retention policy.</param>
|
/// <param name="retentionCutoff">The cutoff derived from retention policy.</param>
|
||||||
/// <param name="reason">The explanatory reason associated with the decision.</param>
|
/// <param name="reason">The explanatory reason associated with the decision.</param>
|
||||||
public static OplogPruneCutoffDecision NoCutoff(HlcTimestamp retentionCutoff, string reason)
|
public static OplogPruneCutoffDecision NoCutoff(HlcTimestamp retentionCutoff, string reason)
|
||||||
{
|
{
|
||||||
return new OplogPruneCutoffDecision(
|
return new OplogPruneCutoffDecision(
|
||||||
hasCutoff: false,
|
false,
|
||||||
retentionCutoff: retentionCutoff,
|
retentionCutoff,
|
||||||
confirmationCutoff: null,
|
null,
|
||||||
effectiveCutoff: null,
|
null,
|
||||||
reason: reason);
|
reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,55 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network; // For IMeshNetwork if we implement it
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Hosting;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using System;
|
using ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
|
// For IMeshNetwork if we implement it
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
public static class CBDDCNetworkExtensions
|
public static class CBDDCNetworkExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds CBDDC network services to the service collection.
|
/// Adds CBDDC network services to the service collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TPeerNodeConfigurationProvider">The peer node configuration provider implementation type.</typeparam>
|
/// <typeparam name="TPeerNodeConfigurationProvider">The peer node configuration provider implementation type.</typeparam>
|
||||||
/// <param name="services">The service collection to register services into.</param>
|
/// <param name="services">The service collection to register services into.</param>
|
||||||
/// <param name="useHostedService">If true, registers CBDDCNodeService as IHostedService to automatically start/stop the node.</param>
|
/// <param name="useHostedService">
|
||||||
public static IServiceCollection AddCBDDCNetwork<TPeerNodeConfigurationProvider>(
|
/// If true, registers CBDDCNodeService as IHostedService to automatically start/stop the
|
||||||
this IServiceCollection services,
|
/// node.
|
||||||
bool useHostedService = true)
|
/// </param>
|
||||||
|
public static IServiceCollection AddCBDDCNetwork<TPeerNodeConfigurationProvider>(
|
||||||
|
this IServiceCollection services,
|
||||||
|
bool useHostedService = true)
|
||||||
where TPeerNodeConfigurationProvider : class, IPeerNodeConfigurationProvider
|
where TPeerNodeConfigurationProvider : class, IPeerNodeConfigurationProvider
|
||||||
{
|
{
|
||||||
services.TryAddSingleton<IPeerNodeConfigurationProvider, TPeerNodeConfigurationProvider>();
|
services.TryAddSingleton<IPeerNodeConfigurationProvider, TPeerNodeConfigurationProvider>();
|
||||||
|
|
||||||
services.TryAddSingleton<IAuthenticator, ClusterKeyAuthenticator>();
|
services.TryAddSingleton<IAuthenticator, ClusterKeyAuthenticator>();
|
||||||
|
|
||||||
services.TryAddSingleton<IPeerHandshakeService, SecureHandshakeService>();
|
services.TryAddSingleton<IPeerHandshakeService, SecureHandshakeService>();
|
||||||
|
|
||||||
services.TryAddSingleton<IDiscoveryService, UdpDiscoveryService>();
|
services.TryAddSingleton<IDiscoveryService, UdpDiscoveryService>();
|
||||||
|
|
||||||
services.TryAddSingleton<ZB.MOM.WW.CBDDC.Network.Telemetry.INetworkTelemetryService>(sp =>
|
services.TryAddSingleton<INetworkTelemetryService>(sp =>
|
||||||
{
|
{
|
||||||
var logger = sp.GetRequiredService<ILogger<ZB.MOM.WW.CBDDC.Network.Telemetry.NetworkTelemetryService>>();
|
var logger = sp.GetRequiredService<ILogger<NetworkTelemetryService>>();
|
||||||
var path = System.IO.Path.Combine(System.AppContext.BaseDirectory, "cbddc_metrics.bin");
|
string path = Path.Combine(AppContext.BaseDirectory, "cbddc_metrics.bin");
|
||||||
return new ZB.MOM.WW.CBDDC.Network.Telemetry.NetworkTelemetryService(logger, path);
|
return new NetworkTelemetryService(logger, path);
|
||||||
});
|
});
|
||||||
|
|
||||||
services.TryAddSingleton<ISyncServer, TcpSyncServer>();
|
services.TryAddSingleton<ISyncServer, TcpSyncServer>();
|
||||||
|
|
||||||
services.TryAddSingleton<IOplogPruneCutoffCalculator, OplogPruneCutoffCalculator>();
|
services.TryAddSingleton<IOplogPruneCutoffCalculator, OplogPruneCutoffCalculator>();
|
||||||
|
|
||||||
services.TryAddSingleton<ISyncOrchestrator, SyncOrchestrator>();
|
services.TryAddSingleton<ISyncOrchestrator, SyncOrchestrator>();
|
||||||
|
|
||||||
services.TryAddSingleton<ICBDDCNode, CBDDCNode>();
|
services.TryAddSingleton<ICBDDCNode, CBDDCNode>();
|
||||||
|
|
||||||
// Optionally register hosted service for automatic node lifecycle management
|
// Optionally register hosted service for automatic node lifecycle management
|
||||||
if (useHostedService)
|
if (useHostedService) services.AddHostedService<CBDDCNodeService>();
|
||||||
{
|
|
||||||
services.AddHostedService<CBDDCNodeService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,259 +1,252 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Proto;
|
using ZB.MOM.WW.CBDDC.Network.Proto;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Security;
|
using ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Protocol
|
namespace ZB.MOM.WW.CBDDC.Network.Protocol;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles the low-level framing, compression, encryption, and serialization of CBDDC messages.
|
||||||
|
/// Encapsulates the wire format: [Length (4)] [Type (1)] [Compression (1)] [Payload (N)]
|
||||||
|
/// </summary>
|
||||||
|
internal class ProtocolHandler
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<ProtocolHandler> _logger;
|
||||||
|
private readonly SemaphoreSlim _readLock = new(1, 1);
|
||||||
|
private readonly INetworkTelemetryService? _telemetry;
|
||||||
|
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the low-level framing, compression, encryption, and serialization of CBDDC messages.
|
/// Initializes a new instance of the <see cref="ProtocolHandler" /> class.
|
||||||
/// Encapsulates the wire format: [Length (4)] [Type (1)] [Compression (1)] [Payload (N)]
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ProtocolHandler
|
/// <param name="logger">The logger used for protocol diagnostics.</param>
|
||||||
{
|
/// <param name="telemetry">An optional telemetry service used to record network metrics.</param>
|
||||||
private readonly ILogger<ProtocolHandler> _logger;
|
public ProtocolHandler(ILogger<ProtocolHandler> logger, INetworkTelemetryService? telemetry = null)
|
||||||
private readonly INetworkTelemetryService? _telemetry;
|
{
|
||||||
private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
|
_logger = logger;
|
||||||
private readonly SemaphoreSlim _readLock = new SemaphoreSlim(1, 1);
|
_telemetry = telemetry;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ProtocolHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger used for protocol diagnostics.</param>
|
|
||||||
/// <param name="telemetry">An optional telemetry service used to record network metrics.</param>
|
|
||||||
public ProtocolHandler(ILogger<ProtocolHandler> logger, INetworkTelemetryService? telemetry = null)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_telemetry = telemetry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ProtocolHandler"/> class using a non-generic logger.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger used for protocol diagnostics.</param>
|
|
||||||
/// <param name="telemetry">An optional telemetry service used to record network metrics.</param>
|
|
||||||
internal ProtocolHandler(ILogger logger, INetworkTelemetryService? telemetry = null)
|
|
||||||
: this(new ForwardingLogger(logger), telemetry)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Serializes and sends a protocol message to the provided stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream">The destination stream.</param>
|
|
||||||
/// <param name="type">The protocol message type.</param>
|
|
||||||
/// <param name="message">The message payload to serialize.</param>
|
|
||||||
/// <param name="useCompression">Whether payload compression should be attempted.</param>
|
|
||||||
/// <param name="cipherState">Optional cipher state used to encrypt outgoing payloads.</param>
|
|
||||||
/// <param name="token">Cancellation token.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous send operation.</returns>
|
|
||||||
public async Task SendMessageAsync(Stream stream, MessageType type, IMessage message, bool useCompression, CipherState? cipherState, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
if (stream == null) throw new ArgumentNullException(nameof(stream));
|
|
||||||
|
|
||||||
// 1. Serialize
|
/// <summary>
|
||||||
byte[] payloadBytes = message.ToByteArray();
|
/// Initializes a new instance of the <see cref="ProtocolHandler" /> class using a non-generic logger.
|
||||||
int originalSize = payloadBytes.Length;
|
/// </summary>
|
||||||
byte compressionFlag = 0x00;
|
/// <param name="logger">The logger used for protocol diagnostics.</param>
|
||||||
|
/// <param name="telemetry">An optional telemetry service used to record network metrics.</param>
|
||||||
|
internal ProtocolHandler(ILogger logger, INetworkTelemetryService? telemetry = null)
|
||||||
|
: this(new ForwardingLogger(logger), telemetry)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Compress (inner payload)
|
/// <summary>
|
||||||
if (useCompression && payloadBytes.Length > CompressionHelper.THRESHOLD && type != MessageType.SecureEnv)
|
/// Serializes and sends a protocol message to the provided stream.
|
||||||
{
|
/// </summary>
|
||||||
// Measure Compression Time
|
/// <param name="stream">The destination stream.</param>
|
||||||
// using var _ = _telemetry?.StartMetric(MetricType.CompressionTime); // Oops, MetricType.CompressionTime not defined? Wait, user asked for "Compression Ratio".
|
/// <param name="type">The protocol message type.</param>
|
||||||
// User asked for "performance della compressione brotli (% media di compressione)".
|
/// <param name="message">The message payload to serialize.</param>
|
||||||
// That usually means ratio. But time is also good?
|
/// <param name="useCompression">Whether payload compression should be attempted.</param>
|
||||||
// Plan said: "MetricType: CompressionRatio, EncryptionTime..."
|
/// <param name="cipherState">Optional cipher state used to encrypt outgoing payloads.</param>
|
||||||
|
/// <param name="token">Cancellation token.</param>
|
||||||
// byte[] compressed; // Removed unused variable
|
/// <returns>A task that represents the asynchronous send operation.</returns>
|
||||||
// using (_telemetry?.StartMetric(MetricType.CompressionTime)) // Let's stick to Time if relevant? NO, MetricType only has Ratio.
|
public async Task SendMessageAsync(Stream stream, MessageType type, IMessage message, bool useCompression,
|
||||||
// Ah I see MetricType enum: CompressionRatio, EncryptionTime, DecryptionTime, RoundTripTime.
|
CipherState? cipherState, CancellationToken token = default)
|
||||||
// So for compression we only record Ratio.
|
{
|
||||||
|
if (stream == null) throw new ArgumentNullException(nameof(stream));
|
||||||
payloadBytes = CompressionHelper.Compress(payloadBytes);
|
|
||||||
compressionFlag = 0x01; // Brotli
|
|
||||||
|
|
||||||
if (_telemetry != null && originalSize > 0)
|
|
||||||
{
|
|
||||||
double ratio = (double)payloadBytes.Length / originalSize;
|
|
||||||
_telemetry.RecordValue(MetricType.CompressionRatio, ratio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Encrypt
|
// 1. Serialize
|
||||||
if (cipherState != null)
|
byte[] payloadBytes = message.ToByteArray();
|
||||||
|
int originalSize = payloadBytes.Length;
|
||||||
|
byte compressionFlag = 0x00;
|
||||||
|
|
||||||
|
// 2. Compress (inner payload)
|
||||||
|
if (useCompression && payloadBytes.Length > CompressionHelper.THRESHOLD && type != MessageType.SecureEnv)
|
||||||
|
{
|
||||||
|
// Measure Compression Time
|
||||||
|
// using var _ = _telemetry?.StartMetric(MetricType.CompressionTime); // Oops, MetricType.CompressionTime not defined? Wait, user asked for "Compression Ratio".
|
||||||
|
// User asked for "performance della compressione brotli (% media di compressione)".
|
||||||
|
// That usually means ratio. But time is also good?
|
||||||
|
// Plan said: "MetricType: CompressionRatio, EncryptionTime..."
|
||||||
|
|
||||||
|
// byte[] compressed; // Removed unused variable
|
||||||
|
// using (_telemetry?.StartMetric(MetricType.CompressionTime)) // Let's stick to Time if relevant? NO, MetricType only has Ratio.
|
||||||
|
// Ah I see MetricType enum: CompressionRatio, EncryptionTime, DecryptionTime, RoundTripTime.
|
||||||
|
// So for compression we only record Ratio.
|
||||||
|
|
||||||
|
payloadBytes = CompressionHelper.Compress(payloadBytes);
|
||||||
|
compressionFlag = 0x01; // Brotli
|
||||||
|
|
||||||
|
if (_telemetry != null && originalSize > 0)
|
||||||
{
|
{
|
||||||
using (_telemetry?.StartMetric(MetricType.EncryptionTime))
|
double ratio = (double)payloadBytes.Length / originalSize;
|
||||||
{
|
_telemetry.RecordValue(MetricType.CompressionRatio, ratio);
|
||||||
// Inner data: [Type (1)] [Compression (1)] [Payload (N)]
|
|
||||||
var dataToEncrypt = new byte[2 + payloadBytes.Length];
|
|
||||||
dataToEncrypt[0] = (byte)type;
|
|
||||||
dataToEncrypt[1] = compressionFlag;
|
|
||||||
Buffer.BlockCopy(payloadBytes, 0, dataToEncrypt, 2, payloadBytes.Length);
|
|
||||||
|
|
||||||
var (ciphertext, iv, tag) = CryptoHelper.Encrypt(dataToEncrypt, cipherState.EncryptKey);
|
|
||||||
|
|
||||||
var env = new SecureEnvelope
|
|
||||||
{
|
|
||||||
Ciphertext = ByteString.CopyFrom(ciphertext),
|
|
||||||
Nonce = ByteString.CopyFrom(iv),
|
|
||||||
AuthTag = ByteString.CopyFrom(tag)
|
|
||||||
};
|
|
||||||
|
|
||||||
payloadBytes = env.ToByteArray();
|
|
||||||
type = MessageType.SecureEnv;
|
|
||||||
compressionFlag = 0x00; // Outer envelope is not compressed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Thread-Safe Write
|
|
||||||
await _writeLock.WaitAsync(token);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Sending Message {Type}, OrgSize: {Org}, WireSize: {Wire}", type, originalSize, payloadBytes.Length);
|
|
||||||
|
|
||||||
// Framing: [Length (4)] [Type (1)] [Compression (1)] [Payload (N)]
|
|
||||||
var lengthBytes = BitConverter.GetBytes(payloadBytes.Length);
|
|
||||||
await stream.WriteAsync(lengthBytes, 0, 4, token);
|
|
||||||
stream.WriteByte((byte)type);
|
|
||||||
stream.WriteByte(compressionFlag);
|
|
||||||
await stream.WriteAsync(payloadBytes, 0, payloadBytes.Length, token);
|
|
||||||
await stream.FlushAsync(token);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_writeLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads and decodes the next protocol message from the provided stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream">The source stream.</param>
|
|
||||||
/// <param name="cipherState">Optional cipher state used to decrypt incoming payloads.</param>
|
|
||||||
/// <param name="token">Cancellation token.</param>
|
|
||||||
/// <returns>A tuple containing the decoded message type and payload bytes.</returns>
|
|
||||||
public async Task<(MessageType, byte[])> ReadMessageAsync(Stream stream, CipherState? cipherState, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
await _readLock.WaitAsync(token);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var lenBuf = new byte[4];
|
|
||||||
int read = await ReadExactAsync(stream, lenBuf, 0, 4, token);
|
|
||||||
if (read == 0) return (MessageType.Unknown, null!);
|
|
||||||
|
|
||||||
int length = BitConverter.ToInt32(lenBuf, 0);
|
|
||||||
|
|
||||||
int typeByte = stream.ReadByte();
|
|
||||||
if (typeByte == -1) throw new EndOfStreamException("Connection closed abruptly (type byte)");
|
|
||||||
|
|
||||||
int compByte = stream.ReadByte();
|
|
||||||
if (compByte == -1) throw new EndOfStreamException("Connection closed abruptly (comp byte)");
|
|
||||||
|
|
||||||
var payload = new byte[length];
|
|
||||||
await ReadExactAsync(stream, payload, 0, length, token);
|
|
||||||
|
|
||||||
var msgType = (MessageType)typeByte;
|
|
||||||
|
|
||||||
// Handle Secure Envelope
|
|
||||||
if (msgType == MessageType.SecureEnv)
|
|
||||||
{
|
|
||||||
if (cipherState == null) throw new InvalidOperationException("Received encrypted message but no cipher state established");
|
|
||||||
|
|
||||||
byte[] decrypted;
|
|
||||||
using (_telemetry?.StartMetric(MetricType.DecryptionTime))
|
|
||||||
{
|
|
||||||
var env = SecureEnvelope.Parser.ParseFrom(payload);
|
|
||||||
decrypted = CryptoHelper.Decrypt(
|
|
||||||
env.Ciphertext.ToByteArray(),
|
|
||||||
env.Nonce.ToByteArray(),
|
|
||||||
env.AuthTag.ToByteArray(),
|
|
||||||
cipherState.DecryptKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decrypted.Length < 2) throw new InvalidDataException("Decrypted payload too short");
|
|
||||||
|
|
||||||
msgType = (MessageType)decrypted[0];
|
|
||||||
int innerComp = decrypted[1];
|
|
||||||
|
|
||||||
var innerPayload = new byte[decrypted.Length - 2];
|
|
||||||
Buffer.BlockCopy(decrypted, 2, innerPayload, 0, innerPayload.Length);
|
|
||||||
|
|
||||||
if (innerComp == 0x01)
|
|
||||||
{
|
|
||||||
innerPayload = CompressionHelper.Decompress(innerPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (msgType, innerPayload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Unencrypted Compression
|
|
||||||
if (compByte == 0x01)
|
|
||||||
{
|
|
||||||
payload = CompressionHelper.Decompress(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Read Message {Type}, Size: {Size}", msgType, payload.Length);
|
|
||||||
return (msgType, payload);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_readLock.Release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken token)
|
// 3. Encrypt
|
||||||
{
|
if (cipherState != null)
|
||||||
int total = 0;
|
using (_telemetry?.StartMetric(MetricType.EncryptionTime))
|
||||||
while (total < count)
|
{
|
||||||
{
|
// Inner data: [Type (1)] [Compression (1)] [Payload (N)]
|
||||||
int read = await stream.ReadAsync(buffer, offset + total, count - total, token);
|
var dataToEncrypt = new byte[2 + payloadBytes.Length];
|
||||||
if (read == 0) return 0; // EOF
|
dataToEncrypt[0] = (byte)type;
|
||||||
total += read;
|
dataToEncrypt[1] = compressionFlag;
|
||||||
}
|
Buffer.BlockCopy(payloadBytes, 0, dataToEncrypt, 2, payloadBytes.Length);
|
||||||
return total;
|
|
||||||
}
|
(byte[] ciphertext, byte[] iv, byte[] tag) =
|
||||||
|
CryptoHelper.Encrypt(dataToEncrypt, cipherState.EncryptKey);
|
||||||
private sealed class ForwardingLogger : ILogger<ProtocolHandler>
|
|
||||||
{
|
var env = new SecureEnvelope
|
||||||
private readonly ILogger _inner;
|
{
|
||||||
|
Ciphertext = ByteString.CopyFrom(ciphertext),
|
||||||
/// <summary>
|
Nonce = ByteString.CopyFrom(iv),
|
||||||
/// Initializes a new instance of the <see cref="ForwardingLogger"/> class.
|
AuthTag = ByteString.CopyFrom(tag)
|
||||||
/// </summary>
|
};
|
||||||
/// <param name="inner">The underlying logger instance.</param>
|
|
||||||
public ForwardingLogger(ILogger inner)
|
payloadBytes = env.ToByteArray();
|
||||||
{
|
type = MessageType.SecureEnv;
|
||||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
compressionFlag = 0x00; // Outer envelope is not compressed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
// 4. Thread-Safe Write
|
||||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
await _writeLock.WaitAsync(token);
|
||||||
{
|
try
|
||||||
return _inner.BeginScope(state);
|
{
|
||||||
}
|
_logger.LogDebug("Sending Message {Type}, OrgSize: {Org}, WireSize: {Wire}", type, originalSize,
|
||||||
|
payloadBytes.Length);
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
// Framing: [Length (4)] [Type (1)] [Compression (1)] [Payload (N)]
|
||||||
{
|
byte[] lengthBytes = BitConverter.GetBytes(payloadBytes.Length);
|
||||||
return _inner.IsEnabled(logLevel);
|
await stream.WriteAsync(lengthBytes, 0, 4, token);
|
||||||
}
|
stream.WriteByte((byte)type);
|
||||||
|
stream.WriteByte(compressionFlag);
|
||||||
/// <inheritdoc />
|
await stream.WriteAsync(payloadBytes, 0, payloadBytes.Length, token);
|
||||||
public void Log<TState>(
|
await stream.FlushAsync(token);
|
||||||
LogLevel logLevel,
|
}
|
||||||
EventId eventId,
|
finally
|
||||||
TState state,
|
{
|
||||||
Exception? exception,
|
_writeLock.Release();
|
||||||
Func<TState, Exception?, string> formatter)
|
}
|
||||||
{
|
}
|
||||||
_inner.Log(logLevel, eventId, state, exception, formatter);
|
|
||||||
}
|
/// <summary>
|
||||||
}
|
/// Reads and decodes the next protocol message from the provided stream.
|
||||||
}
|
/// </summary>
|
||||||
}
|
/// <param name="stream">The source stream.</param>
|
||||||
|
/// <param name="cipherState">Optional cipher state used to decrypt incoming payloads.</param>
|
||||||
|
/// <param name="token">Cancellation token.</param>
|
||||||
|
/// <returns>A tuple containing the decoded message type and payload bytes.</returns>
|
||||||
|
public async Task<(MessageType, byte[])> ReadMessageAsync(Stream stream, CipherState? cipherState,
|
||||||
|
CancellationToken token = default)
|
||||||
|
{
|
||||||
|
await _readLock.WaitAsync(token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var lenBuf = new byte[4];
|
||||||
|
int read = await ReadExactAsync(stream, lenBuf, 0, 4, token);
|
||||||
|
if (read == 0) return (MessageType.Unknown, null!);
|
||||||
|
|
||||||
|
var length = BitConverter.ToInt32(lenBuf, 0);
|
||||||
|
|
||||||
|
int typeByte = stream.ReadByte();
|
||||||
|
if (typeByte == -1) throw new EndOfStreamException("Connection closed abruptly (type byte)");
|
||||||
|
|
||||||
|
int compByte = stream.ReadByte();
|
||||||
|
if (compByte == -1) throw new EndOfStreamException("Connection closed abruptly (comp byte)");
|
||||||
|
|
||||||
|
var payload = new byte[length];
|
||||||
|
await ReadExactAsync(stream, payload, 0, length, token);
|
||||||
|
|
||||||
|
var msgType = (MessageType)typeByte;
|
||||||
|
|
||||||
|
// Handle Secure Envelope
|
||||||
|
if (msgType == MessageType.SecureEnv)
|
||||||
|
{
|
||||||
|
if (cipherState == null)
|
||||||
|
throw new InvalidOperationException("Received encrypted message but no cipher state established");
|
||||||
|
|
||||||
|
byte[] decrypted;
|
||||||
|
using (_telemetry?.StartMetric(MetricType.DecryptionTime))
|
||||||
|
{
|
||||||
|
var env = SecureEnvelope.Parser.ParseFrom(payload);
|
||||||
|
decrypted = CryptoHelper.Decrypt(
|
||||||
|
env.Ciphertext.ToByteArray(),
|
||||||
|
env.Nonce.ToByteArray(),
|
||||||
|
env.AuthTag.ToByteArray(),
|
||||||
|
cipherState.DecryptKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decrypted.Length < 2) throw new InvalidDataException("Decrypted payload too short");
|
||||||
|
|
||||||
|
msgType = (MessageType)decrypted[0];
|
||||||
|
int innerComp = decrypted[1];
|
||||||
|
|
||||||
|
var innerPayload = new byte[decrypted.Length - 2];
|
||||||
|
Buffer.BlockCopy(decrypted, 2, innerPayload, 0, innerPayload.Length);
|
||||||
|
|
||||||
|
if (innerComp == 0x01) innerPayload = CompressionHelper.Decompress(innerPayload);
|
||||||
|
|
||||||
|
return (msgType, innerPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Unencrypted Compression
|
||||||
|
if (compByte == 0x01) payload = CompressionHelper.Decompress(payload);
|
||||||
|
|
||||||
|
_logger.LogDebug("Read Message {Type}, Size: {Size}", msgType, payload.Length);
|
||||||
|
return (msgType, payload);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_readLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken token)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (total < count)
|
||||||
|
{
|
||||||
|
int read = await stream.ReadAsync(buffer, offset + total, count - total, token);
|
||||||
|
if (read == 0) return 0; // EOF
|
||||||
|
total += read;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ForwardingLogger : ILogger<ProtocolHandler>
|
||||||
|
{
|
||||||
|
private readonly ILogger _inner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ForwardingLogger" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inner">The underlying logger instance.</param>
|
||||||
|
public ForwardingLogger(ILogger inner)
|
||||||
|
{
|
||||||
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||||
|
{
|
||||||
|
return _inner.BeginScope(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
|
{
|
||||||
|
return _inner.IsEnabled(logLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Log<TState>(
|
||||||
|
LogLevel logLevel,
|
||||||
|
EventId eventId,
|
||||||
|
TState state,
|
||||||
|
Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
_inner.Log(logLevel, eventId, state, exception, formatter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,12 +48,15 @@ node.Start();
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Automatic Discovery
|
### Automatic Discovery
|
||||||
|
|
||||||
Nodes broadcast their presence via UDP and automatically connect to peers on the same network.
|
Nodes broadcast their presence via UDP and automatically connect to peers on the same network.
|
||||||
|
|
||||||
### Secure Synchronization
|
### Secure Synchronization
|
||||||
|
|
||||||
All nodes must share the same authentication token to sync data.
|
All nodes must share the same authentication token to sync data.
|
||||||
|
|
||||||
### Scalable Gossip
|
### Scalable Gossip
|
||||||
|
|
||||||
Updates propagate exponentially - each node tells multiple peers, ensuring fast network-wide propagation.
|
Updates propagate exponentially - each node tells multiple peers, ensuring fast network-wide propagation.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography;
|
using System.Text;
|
||||||
using System.Text;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticator implementation that uses a shared secret (pre-shared key) to validate nodes.
|
/// Authenticator implementation that uses a shared secret (pre-shared key) to validate nodes.
|
||||||
/// Both nodes must possess the same key to successfully handshake.
|
/// Both nodes must possess the same key to successfully handshake.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClusterKeyAuthenticator : IAuthenticator
|
public class ClusterKeyAuthenticator : IAuthenticator
|
||||||
{
|
{
|
||||||
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
private readonly IPeerNodeConfigurationProvider _peerNodeConfigurationProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ClusterKeyAuthenticator"/> class.
|
/// Initializes a new instance of the <see cref="ClusterKeyAuthenticator" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeConfigurationProvider">The provider for peer node configuration.</param>
|
/// <param name="peerNodeConfigurationProvider">The provider for peer node configuration.</param>
|
||||||
public ClusterKeyAuthenticator(IPeerNodeConfigurationProvider peerNodeConfigurationProvider)
|
public ClusterKeyAuthenticator(IPeerNodeConfigurationProvider peerNodeConfigurationProvider)
|
||||||
@@ -23,11 +22,11 @@ public class ClusterKeyAuthenticator : IAuthenticator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> ValidateAsync(string nodeId, string token)
|
public async Task<bool> ValidateAsync(string nodeId, string token)
|
||||||
{
|
{
|
||||||
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
var config = await _peerNodeConfigurationProvider.GetConfiguration();
|
||||||
var configuredHash = SHA256.HashData(Encoding.UTF8.GetBytes(config.AuthToken ?? string.Empty));
|
byte[] configuredHash = SHA256.HashData(Encoding.UTF8.GetBytes(config.AuthToken ?? string.Empty));
|
||||||
var presentedHash = SHA256.HashData(Encoding.UTF8.GetBytes(token ?? string.Empty));
|
byte[] presentedHash = SHA256.HashData(Encoding.UTF8.GetBytes(token ?? string.Empty));
|
||||||
return CryptographicOperations.FixedTimeEquals(configuredHash, presentedHash);
|
return CryptographicOperations.FixedTimeEquals(configuredHash, presentedHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
|
||||||
public static class CryptoHelper
|
public static class CryptoHelper
|
||||||
{
|
{
|
||||||
private const int KeySize = 32; // 256 bits
|
private const int KeySize = 32; // 256 bits
|
||||||
private const int BlockSize = 16; // 128 bits
|
private const int BlockSize = 16; // 128 bits
|
||||||
private const int MacSize = 32; // 256 bits (HMACSHA256)
|
private const int MacSize = 32; // 256 bits (HMACSHA256)
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Encrypts plaintext and computes an authentication tag.
|
/// Encrypts plaintext and computes an authentication tag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="plaintext">The plaintext bytes to encrypt.</param>
|
/// <param name="plaintext">The plaintext bytes to encrypt.</param>
|
||||||
/// <param name="key">The encryption and HMAC key.</param>
|
/// <param name="key">The encryption and HMAC key.</param>
|
||||||
/// <returns>The ciphertext, IV, and authentication tag.</returns>
|
/// <returns>The ciphertext, IV, and authentication tag.</returns>
|
||||||
public static (byte[] ciphertext, byte[] iv, byte[] tag) Encrypt(byte[] plaintext, byte[] key)
|
public static (byte[] ciphertext, byte[] iv, byte[] tag) Encrypt(byte[] plaintext, byte[] key)
|
||||||
{
|
{
|
||||||
using var aes = Aes.Create();
|
using var aes = Aes.Create();
|
||||||
aes.Key = key;
|
aes.Key = key;
|
||||||
aes.GenerateIV();
|
aes.GenerateIV();
|
||||||
var iv = aes.IV;
|
byte[] iv = aes.IV;
|
||||||
|
|
||||||
using var encryptor = aes.CreateEncryptor();
|
using var encryptor = aes.CreateEncryptor();
|
||||||
var ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
|
byte[] ciphertext = encryptor.TransformFinalBlock(plaintext, 0, plaintext.Length);
|
||||||
|
|
||||||
// Compute HMAC
|
// Compute HMAC
|
||||||
using var hmac = new HMACSHA256(key);
|
using var hmac = new HMACSHA256(key);
|
||||||
@@ -32,32 +30,30 @@ public static class CryptoHelper
|
|||||||
var toSign = new byte[iv.Length + ciphertext.Length];
|
var toSign = new byte[iv.Length + ciphertext.Length];
|
||||||
Buffer.BlockCopy(iv, 0, toSign, 0, iv.Length);
|
Buffer.BlockCopy(iv, 0, toSign, 0, iv.Length);
|
||||||
Buffer.BlockCopy(ciphertext, 0, toSign, iv.Length, ciphertext.Length);
|
Buffer.BlockCopy(ciphertext, 0, toSign, iv.Length, ciphertext.Length);
|
||||||
var tag = hmac.ComputeHash(toSign);
|
byte[] tag = hmac.ComputeHash(toSign);
|
||||||
|
|
||||||
return (ciphertext, iv, tag);
|
return (ciphertext, iv, tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verifies and decrypts ciphertext.
|
/// Verifies and decrypts ciphertext.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="ciphertext">The encrypted bytes.</param>
|
/// <param name="ciphertext">The encrypted bytes.</param>
|
||||||
/// <param name="iv">The initialization vector used during encryption.</param>
|
/// <param name="iv">The initialization vector used during encryption.</param>
|
||||||
/// <param name="tag">The authentication tag for integrity verification.</param>
|
/// <param name="tag">The authentication tag for integrity verification.</param>
|
||||||
/// <param name="key">The encryption and HMAC key.</param>
|
/// <param name="key">The encryption and HMAC key.</param>
|
||||||
/// <returns>The decrypted plaintext bytes.</returns>
|
/// <returns>The decrypted plaintext bytes.</returns>
|
||||||
public static byte[] Decrypt(byte[] ciphertext, byte[] iv, byte[] tag, byte[] key)
|
public static byte[] Decrypt(byte[] ciphertext, byte[] iv, byte[] tag, byte[] key)
|
||||||
{
|
{
|
||||||
// Verify HMAC
|
// Verify HMAC
|
||||||
using var hmac = new HMACSHA256(key);
|
using var hmac = new HMACSHA256(key);
|
||||||
var toVerify = new byte[iv.Length + ciphertext.Length];
|
var toVerify = new byte[iv.Length + ciphertext.Length];
|
||||||
Buffer.BlockCopy(iv, 0, toVerify, 0, iv.Length);
|
Buffer.BlockCopy(iv, 0, toVerify, 0, iv.Length);
|
||||||
Buffer.BlockCopy(ciphertext, 0, toVerify, iv.Length, ciphertext.Length);
|
Buffer.BlockCopy(ciphertext, 0, toVerify, iv.Length, ciphertext.Length);
|
||||||
var computedTag = hmac.ComputeHash(toVerify);
|
byte[] computedTag = hmac.ComputeHash(toVerify);
|
||||||
|
|
||||||
if (!FixedTimeEquals(tag, computedTag))
|
if (!FixedTimeEquals(tag, computedTag))
|
||||||
{
|
|
||||||
throw new CryptographicException("Authentication failed (HMAC mismatch)");
|
throw new CryptographicException("Authentication failed (HMAC mismatch)");
|
||||||
}
|
|
||||||
|
|
||||||
using var aes = Aes.Create();
|
using var aes = Aes.Create();
|
||||||
aes.Key = key;
|
aes.Key = key;
|
||||||
@@ -78,4 +74,4 @@ public static class CryptoHelper
|
|||||||
return res == 0;
|
return res == 0;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
using System.Threading.Tasks;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
|
|
||||||
public interface IAuthenticator
|
public interface IAuthenticator
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates an authentication token for a node identifier.
|
/// Validates an authentication token for a node identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier to validate.</param>
|
/// <param name="nodeId">The node identifier to validate.</param>
|
||||||
/// <param name="token">The authentication token to validate.</param>
|
/// <param name="token">The authentication token to validate.</param>
|
||||||
/// <returns><see langword="true"/> if the token is valid for the node; otherwise <see langword="false"/>.</returns>
|
/// <returns><see langword="true" /> if the token is valid for the node; otherwise <see langword="false" />.</returns>
|
||||||
Task<bool> ValidateAsync(string nodeId, string token);
|
Task<bool> ValidateAsync(string nodeId, string token);
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,25 @@
|
|||||||
using System.Threading;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
|
|
||||||
public interface IPeerHandshakeService
|
public interface IPeerHandshakeService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a handshake to establishing identity and optional security context.
|
/// Performs a handshake to establishing identity and optional security context.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream">The transport stream used for handshake message exchange.</param>
|
/// <param name="stream">The transport stream used for handshake message exchange.</param>
|
||||||
/// <param name="isInitiator">A value indicating whether the caller initiated the connection.</param>
|
/// <param name="isInitiator">A value indicating whether the caller initiated the connection.</param>
|
||||||
/// <param name="myNodeId">The local node identifier.</param>
|
/// <param name="myNodeId">The local node identifier.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>A CipherState if encryption is established, or null if plaintext.</returns>
|
/// <returns>A CipherState if encryption is established, or null if plaintext.</returns>
|
||||||
Task<CipherState?> HandshakeAsync(System.IO.Stream stream, bool isInitiator, string myNodeId, CancellationToken token);
|
Task<CipherState?> HandshakeAsync(Stream stream, bool isInitiator, string myNodeId, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CipherState
|
public class CipherState
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets the key used to encrypt outgoing messages.
|
|
||||||
/// </summary>
|
|
||||||
public byte[] EncryptKey { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the key used to decrypt incoming messages.
|
|
||||||
/// </summary>
|
|
||||||
public byte[] DecryptKey { get; }
|
|
||||||
// For simplicity using IV chaining or explicit IVs.
|
// For simplicity using IV chaining or explicit IVs.
|
||||||
// We'll store just the keys here and let the encryption helper handle IVs.
|
// We'll store just the keys here and let the encryption helper handle IVs.
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CipherState"/> class.
|
/// Initializes a new instance of the <see cref="CipherState" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="encryptKey">The key used for encrypting outgoing payloads.</param>
|
/// <param name="encryptKey">The key used for encrypting outgoing payloads.</param>
|
||||||
/// <param name="decryptKey">The key used for decrypting incoming payloads.</param>
|
/// <param name="decryptKey">The key used for decrypting incoming payloads.</param>
|
||||||
@@ -39,4 +28,14 @@ public class CipherState
|
|||||||
EncryptKey = encryptKey;
|
EncryptKey = encryptKey;
|
||||||
DecryptKey = decryptKey;
|
DecryptKey = decryptKey;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the key used to encrypt outgoing messages.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] EncryptKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the key used to decrypt incoming messages.
|
||||||
|
/// </summary>
|
||||||
|
public byte[] DecryptKey { get; }
|
||||||
|
}
|
||||||
@@ -1,29 +1,32 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides a no-operation implementation of the peer handshake service that performs no handshake and always returns
|
/// Provides a no-operation implementation of the peer handshake service that performs no handshake and always returns
|
||||||
/// null.
|
/// null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>This class can be used in scenarios where a handshake is not required or for testing purposes. All
|
/// <remarks>
|
||||||
/// handshake attempts using this service will result in no cipher state being established.</remarks>
|
/// This class can be used in scenarios where a handshake is not required or for testing purposes. All
|
||||||
|
/// handshake attempts using this service will result in no cipher state being established.
|
||||||
|
/// </remarks>
|
||||||
public class NoOpHandshakeService : IPeerHandshakeService
|
public class NoOpHandshakeService : IPeerHandshakeService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a handshake over the specified stream to establish a secure communication channel between two nodes
|
/// Performs a handshake over the specified stream to establish a secure communication channel between two nodes
|
||||||
/// asynchronously.
|
/// asynchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream">The stream used for exchanging handshake messages between nodes. Must be readable and writable.</param>
|
/// <param name="stream">The stream used for exchanging handshake messages between nodes. Must be readable and writable.</param>
|
||||||
/// <param name="isInitiator">true to initiate the handshake as the local node; otherwise, false to respond as the remote node.</param>
|
/// <param name="isInitiator">
|
||||||
|
/// true to initiate the handshake as the local node; otherwise, false to respond as the remote
|
||||||
|
/// node.
|
||||||
|
/// </param>
|
||||||
/// <param name="myNodeId">The unique identifier of the local node participating in the handshake. Cannot be null.</param>
|
/// <param name="myNodeId">The unique identifier of the local node participating in the handshake. Cannot be null.</param>
|
||||||
/// <param name="token">A cancellation token that can be used to cancel the handshake operation.</param>
|
/// <param name="token">A cancellation token that can be used to cancel the handshake operation.</param>
|
||||||
/// <returns>A task that represents the asynchronous handshake operation. The task result contains a CipherState if the
|
/// <returns>
|
||||||
/// handshake succeeds; otherwise, null.</returns>
|
/// A task that represents the asynchronous handshake operation. The task result contains a CipherState if the
|
||||||
|
/// handshake succeeds; otherwise, null.
|
||||||
|
/// </returns>
|
||||||
public Task<CipherState?> HandshakeAsync(Stream stream, bool isInitiator, string myNodeId, CancellationToken token)
|
public Task<CipherState?> HandshakeAsync(Stream stream, bool isInitiator, string myNodeId, CancellationToken token)
|
||||||
{
|
{
|
||||||
return Task.FromResult<CipherState?>(null);
|
return Task.FromResult<CipherState?>(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
namespace ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
@@ -12,7 +8,7 @@ public class SecureHandshakeService : IPeerHandshakeService
|
|||||||
private readonly ILogger<SecureHandshakeService>? _logger;
|
private readonly ILogger<SecureHandshakeService>? _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SecureHandshakeService"/> class.
|
/// Initializes a new instance of the <see cref="SecureHandshakeService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The optional logger instance.</param>
|
/// <param name="logger">The optional logger instance.</param>
|
||||||
public SecureHandshakeService(ILogger<SecureHandshakeService>? logger = null)
|
public SecureHandshakeService(ILogger<SecureHandshakeService>? logger = null)
|
||||||
@@ -26,44 +22,42 @@ public class SecureHandshakeService : IPeerHandshakeService
|
|||||||
// Both derive shared secret -> Split into SendKey/RecvKey using HKDF
|
// Both derive shared secret -> Split into SendKey/RecvKey using HKDF
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs a secure key exchange handshake over the provided stream.
|
/// Performs a secure key exchange handshake over the provided stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream">The transport stream used for the handshake.</param>
|
/// <param name="stream">The transport stream used for the handshake.</param>
|
||||||
/// <param name="isInitiator">A value indicating whether the local node initiated the handshake.</param>
|
/// <param name="isInitiator">A value indicating whether the local node initiated the handshake.</param>
|
||||||
/// <param name="myNodeId">The local node identifier.</param>
|
/// <param name="myNodeId">The local node identifier.</param>
|
||||||
/// <param name="token">A token used to cancel the handshake.</param>
|
/// <param name="token">A token used to cancel the handshake.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A task that returns the negotiated <see cref="CipherState"/>, or <see langword="null"/> if unavailable.
|
/// A task that returns the negotiated <see cref="CipherState" />, or <see langword="null" /> if unavailable.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public async Task<CipherState?> HandshakeAsync(Stream stream, bool isInitiator, string myNodeId, CancellationToken token)
|
public async Task<CipherState?> HandshakeAsync(Stream stream, bool isInitiator, string myNodeId,
|
||||||
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
#if NET6_0_OR_GREATER
|
#if NET6_0_OR_GREATER
|
||||||
using var ecdh = ECDiffieHellman.Create();
|
using var ecdh = ECDiffieHellman.Create();
|
||||||
ecdh.KeySize = 256;
|
ecdh.KeySize = 256;
|
||||||
|
|
||||||
// 1. ExportAsync & Send Public Key
|
// 1. ExportAsync & Send Public Key
|
||||||
var myPublicKey = ecdh.ExportSubjectPublicKeyInfo();
|
byte[] myPublicKey = ecdh.ExportSubjectPublicKeyInfo();
|
||||||
var lenBytes = BitConverter.GetBytes(myPublicKey.Length);
|
byte[] lenBytes = BitConverter.GetBytes(myPublicKey.Length);
|
||||||
await stream.WriteAsync(lenBytes, 0, 4, token);
|
await stream.WriteAsync(lenBytes, 0, 4, token);
|
||||||
await stream.WriteAsync(myPublicKey, 0, myPublicKey.Length, token);
|
await stream.WriteAsync(myPublicKey, 0, myPublicKey.Length, token);
|
||||||
await stream.FlushAsync(token); // CRITICAL: Ensure data is sent immediately
|
await stream.FlushAsync(token); // CRITICAL: Ensure data is sent immediately
|
||||||
|
|
||||||
// 2. Receive Peer Public Key
|
// 2. Receive Peer Public Key
|
||||||
var peerLenBuf = new byte[4];
|
var peerLenBuf = new byte[4];
|
||||||
await ReadExactAsync(stream, peerLenBuf, 0, 4, token);
|
await ReadExactAsync(stream, peerLenBuf, 0, 4, token);
|
||||||
int peerLen = BitConverter.ToInt32(peerLenBuf, 0);
|
var peerLen = BitConverter.ToInt32(peerLenBuf, 0);
|
||||||
|
|
||||||
// Validate peer key length to prevent DoS
|
// Validate peer key length to prevent DoS
|
||||||
if (peerLen <= 0 || peerLen > 10000)
|
if (peerLen <= 0 || peerLen > 10000) throw new InvalidOperationException($"Invalid peer key length: {peerLen}");
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Invalid peer key length: {peerLen}");
|
var peerKeyBytes = new byte[peerLen];
|
||||||
}
|
await ReadExactAsync(stream, peerKeyBytes, 0, peerLen, token);
|
||||||
|
|
||||||
var peerKeyBytes = new byte[peerLen];
|
// 3. Import Peer Key & Derive Shared Secret
|
||||||
await ReadExactAsync(stream, peerKeyBytes, 0, peerLen, token);
|
using var peerEcdh = ECDiffieHellman.Create();
|
||||||
|
|
||||||
// 3. Import Peer Key & Derive Shared Secret
|
|
||||||
using var peerEcdh = ECDiffieHellman.Create();
|
|
||||||
peerEcdh.ImportSubjectPublicKeyInfo(peerKeyBytes, out _);
|
peerEcdh.ImportSubjectPublicKeyInfo(peerKeyBytes, out _);
|
||||||
|
|
||||||
byte[] sharedSecret = ecdh.DeriveKeyMaterial(peerEcdh.PublicKey);
|
byte[] sharedSecret = ecdh.DeriveKeyMaterial(peerEcdh.PublicKey);
|
||||||
@@ -74,39 +68,40 @@ public class SecureHandshakeService : IPeerHandshakeService
|
|||||||
|
|
||||||
using var sha = SHA256.Create();
|
using var sha = SHA256.Create();
|
||||||
|
|
||||||
var k1Input = new byte[sharedSecret.Length + 1];
|
var k1Input = new byte[sharedSecret.Length + 1];
|
||||||
Buffer.BlockCopy(sharedSecret, 0, k1Input, 0, sharedSecret.Length);
|
Buffer.BlockCopy(sharedSecret, 0, k1Input, 0, sharedSecret.Length);
|
||||||
k1Input[sharedSecret.Length] = 0; // "0"
|
k1Input[sharedSecret.Length] = 0; // "0"
|
||||||
var key1 = sha.ComputeHash(k1Input);
|
byte[] key1 = sha.ComputeHash(k1Input);
|
||||||
|
|
||||||
var k2Input = new byte[sharedSecret.Length + 1];
|
var k2Input = new byte[sharedSecret.Length + 1];
|
||||||
Buffer.BlockCopy(sharedSecret, 0, k2Input, 0, sharedSecret.Length);
|
Buffer.BlockCopy(sharedSecret, 0, k2Input, 0, sharedSecret.Length);
|
||||||
k2Input[sharedSecret.Length] = 1; // "1"
|
k2Input[sharedSecret.Length] = 1; // "1"
|
||||||
var key2 = sha.ComputeHash(k2Input);
|
byte[] key2 = sha.ComputeHash(k2Input);
|
||||||
|
|
||||||
// If initiator: Encrypt with Key1, Decrypt with Key2
|
// If initiator: Encrypt with Key1, Decrypt with Key2
|
||||||
// If responder: Encrypt with Key2, Decrypt with Key1
|
// If responder: Encrypt with Key2, Decrypt with Key1
|
||||||
|
|
||||||
var encryptKey = isInitiator ? key1 : key2;
|
byte[] encryptKey = isInitiator ? key1 : key2;
|
||||||
var decryptKey = isInitiator ? key2 : key1;
|
byte[] decryptKey = isInitiator ? key2 : key1;
|
||||||
|
|
||||||
return new CipherState(encryptKey, decryptKey);
|
return new CipherState(encryptKey, decryptKey);
|
||||||
#else
|
#else
|
||||||
// For netstandard2.0, standard ECDH import is broken/hard without external libs.
|
// For netstandard2.0, standard ECDH import is broken/hard without external libs.
|
||||||
// Returning null or throwing.
|
// Returning null or throwing.
|
||||||
throw new PlatformNotSupportedException("Secure handshake requires .NET 6.0+");
|
throw new PlatformNotSupportedException("Secure handshake requires .NET 6.0+");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken token)
|
private async Task<int> ReadExactAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken token)
|
||||||
{
|
{
|
||||||
int total = 0;
|
var total = 0;
|
||||||
while (total < count)
|
while (total < count)
|
||||||
{
|
{
|
||||||
int read = await stream.ReadAsync(buffer, offset + total, count - total, token);
|
int read = await stream.ReadAsync(buffer, offset + total, count - total, token);
|
||||||
if (read == 0) throw new EndOfStreamException();
|
if (read == 0) throw new EndOfStreamException();
|
||||||
total += read;
|
total += read;
|
||||||
}
|
}
|
||||||
return total;
|
|
||||||
}
|
return total;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,60 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Google.Protobuf;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Proto;
|
using ZB.MOM.WW.CBDDC.Network.Proto;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network.Protocol;
|
using ZB.MOM.WW.CBDDC.Network.Protocol;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a TCP client connection to a remote peer for synchronization.
|
/// Represents a TCP client connection to a remote peer for synchronization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class TcpPeerClient : IDisposable
|
public class TcpPeerClient : IDisposable
|
||||||
{
|
{
|
||||||
private readonly TcpClient _client;
|
|
||||||
private readonly string _peerAddress;
|
|
||||||
private readonly ILogger<TcpPeerClient> _logger;
|
|
||||||
private readonly IPeerHandshakeService? _handshakeService;
|
|
||||||
private NetworkStream? _stream;
|
|
||||||
private CipherState? _cipherState;
|
|
||||||
private readonly object _connectionLock = new object();
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
private const int ConnectionTimeoutMs = 5000;
|
private const int ConnectionTimeoutMs = 5000;
|
||||||
private const int OperationTimeoutMs = 30000;
|
private const int OperationTimeoutMs = 30000;
|
||||||
|
private readonly TcpClient _client;
|
||||||
private readonly ProtocolHandler _protocol;
|
private readonly object _connectionLock = new();
|
||||||
|
private readonly IPeerHandshakeService? _handshakeService;
|
||||||
/// <summary>
|
private readonly ILogger<TcpPeerClient> _logger;
|
||||||
/// Gets a value indicating whether the client currently has an active connection.
|
private readonly string _peerAddress;
|
||||||
/// </summary>
|
|
||||||
public bool IsConnected
|
private readonly ProtocolHandler _protocol;
|
||||||
{
|
|
||||||
|
private readonly INetworkTelemetryService? _telemetry;
|
||||||
|
private CipherState? _cipherState;
|
||||||
|
private bool _disposed;
|
||||||
|
private List<string> _remoteInterests = new();
|
||||||
|
private NetworkStream? _stream;
|
||||||
|
|
||||||
|
private bool _useCompression; // Negotiated after handshake
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TcpPeerClient" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="peerAddress">The remote peer address in <c>host:port</c> format.</param>
|
||||||
|
/// <param name="logger">The logger used for connection and protocol events.</param>
|
||||||
|
/// <param name="handshakeService">The optional handshake service used to establish secure sessions.</param>
|
||||||
|
/// <param name="telemetry">The optional telemetry service for network metrics.</param>
|
||||||
|
public TcpPeerClient(string peerAddress, ILogger<TcpPeerClient> logger,
|
||||||
|
IPeerHandshakeService? handshakeService = null, INetworkTelemetryService? telemetry = null)
|
||||||
|
{
|
||||||
|
_client = new TcpClient();
|
||||||
|
_peerAddress = peerAddress;
|
||||||
|
_logger = logger;
|
||||||
|
_handshakeService = handshakeService;
|
||||||
|
_telemetry = telemetry;
|
||||||
|
_protocol = new ProtocolHandler(logger, telemetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the client currently has an active connection.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsConnected
|
||||||
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_connectionLock)
|
lock (_connectionLock)
|
||||||
@@ -46,89 +62,97 @@ public class TcpPeerClient : IDisposable
|
|||||||
return _client != null && _client.Connected && _stream != null && !_disposed;
|
return _client != null && _client.Connected && _stream != null && !_disposed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the handshake with the remote peer has completed successfully.
|
/// Gets a value indicating whether the handshake with the remote peer has completed successfully.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasHandshaked { get; private set; }
|
public bool HasHandshaked { get; private set; }
|
||||||
|
|
||||||
private readonly INetworkTelemetryService? _telemetry;
|
/// <summary>
|
||||||
|
/// Gets the list of collections the remote peer is interested in.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Initializes a new instance of the <see cref="TcpPeerClient"/> class.
|
public IReadOnlyList<string> RemoteInterests => _remoteInterests.AsReadOnly();
|
||||||
/// </summary>
|
|
||||||
/// <param name="peerAddress">The remote peer address in <c>host:port</c> format.</param>
|
/// <summary>
|
||||||
/// <param name="logger">The logger used for connection and protocol events.</param>
|
/// Releases resources used by the peer client.
|
||||||
/// <param name="handshakeService">The optional handshake service used to establish secure sessions.</param>
|
/// </summary>
|
||||||
/// <param name="telemetry">The optional telemetry service for network metrics.</param>
|
public void Dispose()
|
||||||
public TcpPeerClient(string peerAddress, ILogger<TcpPeerClient> logger, IPeerHandshakeService? handshakeService = null, INetworkTelemetryService? telemetry = null)
|
{
|
||||||
{
|
|
||||||
_client = new TcpClient();
|
|
||||||
_peerAddress = peerAddress;
|
|
||||||
_logger = logger;
|
|
||||||
_handshakeService = handshakeService;
|
|
||||||
_telemetry = telemetry;
|
|
||||||
_protocol = new ProtocolHandler(logger, telemetry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connects to the configured remote peer.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token">A token used to cancel the connection attempt.</param>
|
|
||||||
/// <returns>A task that represents the asynchronous connect operation.</returns>
|
|
||||||
public async Task ConnectAsync(CancellationToken token)
|
|
||||||
{
|
|
||||||
lock (_connectionLock)
|
lock (_connectionLock)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed) return;
|
||||||
{
|
_disposed = true;
|
||||||
throw new ObjectDisposedException(nameof(TcpPeerClient));
|
}
|
||||||
}
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stream?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error disposing network stream");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_client?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error disposing TCP client");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Disposed connection to peer: {Address}", _peerAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Connects to the configured remote peer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">A token used to cancel the connection attempt.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous connect operation.</returns>
|
||||||
|
public async Task ConnectAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
lock (_connectionLock)
|
||||||
|
{
|
||||||
|
if (_disposed) throw new ObjectDisposedException(nameof(TcpPeerClient));
|
||||||
|
|
||||||
if (IsConnected) return;
|
if (IsConnected) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = _peerAddress.Split(':');
|
string[] parts = _peerAddress.Split(':');
|
||||||
if (parts.Length != 2)
|
if (parts.Length != 2)
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid address format: {_peerAddress}. Expected format: host:port");
|
throw new ArgumentException($"Invalid address format: {_peerAddress}. Expected format: host:port");
|
||||||
}
|
|
||||||
|
|
||||||
if (!int.TryParse(parts[1], out int port) || port <= 0 || port > 65535)
|
if (!int.TryParse(parts[1], out int port) || port <= 0 || port > 65535)
|
||||||
{
|
|
||||||
throw new ArgumentException($"Invalid port number: {parts[1]}");
|
throw new ArgumentException($"Invalid port number: {parts[1]}");
|
||||||
}
|
|
||||||
|
|
||||||
// Connect with timeout
|
// Connect with timeout
|
||||||
using var timeoutCts = new CancellationTokenSource(ConnectionTimeoutMs);
|
using var timeoutCts = new CancellationTokenSource(ConnectionTimeoutMs);
|
||||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutCts.Token);
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token, timeoutCts.Token);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _client.ConnectAsync(parts[0], port);
|
await _client.ConnectAsync(parts[0], port);
|
||||||
|
|
||||||
lock (_connectionLock)
|
lock (_connectionLock)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed) throw new ObjectDisposedException(nameof(TcpPeerClient));
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(nameof(TcpPeerClient));
|
_stream = _client.GetStream();
|
||||||
}
|
|
||||||
|
// CRITICAL for Android: Disable Nagle's algorithm to prevent buffering delays
|
||||||
_stream = _client.GetStream();
|
// This ensures immediate packet transmission for handshake data
|
||||||
|
_client.NoDelay = true;
|
||||||
// CRITICAL for Android: Disable Nagle's algorithm to prevent buffering delays
|
|
||||||
// This ensures immediate packet transmission for handshake data
|
// Configure TCP keepalive
|
||||||
_client.NoDelay = true;
|
_client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||||
|
|
||||||
// Configure TCP keepalive
|
// Set read/write timeouts
|
||||||
_client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
|
||||||
|
|
||||||
// Set read/write timeouts
|
|
||||||
_stream.ReadTimeout = OperationTimeoutMs;
|
_stream.ReadTimeout = OperationTimeoutMs;
|
||||||
_stream.WriteTimeout = OperationTimeoutMs;
|
_stream.WriteTimeout = OperationTimeoutMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Connected to peer: {Address} (NoDelay=true for immediate send)", _peerAddress);
|
_logger.LogDebug("Connected to peer: {Address} (NoDelay=true for immediate send)", _peerAddress);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||||||
@@ -138,13 +162,7 @@ public class TcpPeerClient : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the list of collections the remote peer is interested in.
|
/// Performs authentication handshake with the remote peer.
|
||||||
/// </summary>
|
|
||||||
public System.Collections.Generic.IReadOnlyList<string> RemoteInterests => _remoteInterests.AsReadOnly();
|
|
||||||
private List<string> _remoteInterests = new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs authentication handshake with the remote peer.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="myNodeId">The local node identifier.</param>
|
/// <param name="myNodeId">The local node identifier.</param>
|
||||||
/// <param name="authToken">The authentication token.</param>
|
/// <param name="authToken">The authentication token.</param>
|
||||||
@@ -155,46 +173,38 @@ public class TcpPeerClient : IDisposable
|
|||||||
return await HandshakeAsync(myNodeId, authToken, null, token);
|
return await HandshakeAsync(myNodeId, authToken, null, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs authentication handshake with the remote peer, including collection interests.
|
/// Performs authentication handshake with the remote peer, including collection interests.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="myNodeId">The local node identifier.</param>
|
/// <param name="myNodeId">The local node identifier.</param>
|
||||||
/// <param name="authToken">The authentication token.</param>
|
/// <param name="authToken">The authentication token.</param>
|
||||||
/// <param name="interestingCollections">Optional collection names this node is interested in receiving.</param>
|
/// <param name="interestingCollections">Optional collection names this node is interested in receiving.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> if handshake was accepted; otherwise <see langword="false"/>.</returns>
|
/// <returns><see langword="true" /> if handshake was accepted; otherwise <see langword="false" />.</returns>
|
||||||
public async Task<bool> HandshakeAsync(string myNodeId, string authToken, IEnumerable<string>? interestingCollections, CancellationToken token)
|
public async Task<bool> HandshakeAsync(string myNodeId, string authToken,
|
||||||
{
|
IEnumerable<string>? interestingCollections, CancellationToken token)
|
||||||
|
{
|
||||||
if (HasHandshaked) return true;
|
if (HasHandshaked) return true;
|
||||||
|
|
||||||
if (_handshakeService != null)
|
if (_handshakeService != null)
|
||||||
{
|
|
||||||
// Perform secure handshake if service is available
|
// Perform secure handshake if service is available
|
||||||
// We assume we are initiator here
|
// We assume we are initiator here
|
||||||
_cipherState = await _handshakeService.HandshakeAsync(_stream!, true, myNodeId, token);
|
_cipherState = await _handshakeService.HandshakeAsync(_stream!, true, myNodeId, token);
|
||||||
}
|
|
||||||
|
|
||||||
var req = new HandshakeRequest { NodeId = myNodeId, AuthToken = authToken ?? "" };
|
var req = new HandshakeRequest { NodeId = myNodeId, AuthToken = authToken ?? "" };
|
||||||
|
|
||||||
if (interestingCollections != null)
|
if (interestingCollections != null)
|
||||||
{
|
foreach (string coll in interestingCollections)
|
||||||
foreach (var coll in interestingCollections)
|
|
||||||
{
|
|
||||||
req.InterestingCollections.Add(coll);
|
req.InterestingCollections.Add(coll);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CompressionHelper.IsBrotliSupported)
|
if (CompressionHelper.IsBrotliSupported) req.SupportedCompression.Add("brotli");
|
||||||
{
|
|
||||||
req.SupportedCompression.Add("brotli");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Sending HandshakeReq to {Address}", _peerAddress);
|
_logger.LogDebug("Sending HandshakeReq to {Address}", _peerAddress);
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.HandshakeReq, req, false, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.HandshakeReq, req, false, _cipherState, token);
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
_logger.LogDebug("Received Handshake response type: {Type}", type);
|
_logger.LogDebug("Received Handshake response type: {Type}", type);
|
||||||
|
|
||||||
if (type != MessageType.HandshakeRes) return false;
|
if (type != MessageType.HandshakeRes) return false;
|
||||||
|
|
||||||
var res = HandshakeResponse.Parser.ParseFrom(payload);
|
var res = HandshakeResponse.Parser.ParseFrom(payload);
|
||||||
@@ -213,18 +223,19 @@ public class TcpPeerClient : IDisposable
|
|||||||
return res.Accepted;
|
return res.Accepted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the remote peer's latest HLC timestamp.
|
/// Retrieves the remote peer's latest HLC timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>The latest remote hybrid logical clock timestamp.</returns>
|
/// <returns>The latest remote hybrid logical clock timestamp.</returns>
|
||||||
public async Task<HlcTimestamp> GetClockAsync(CancellationToken token)
|
public async Task<HlcTimestamp> GetClockAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
using (_telemetry?.StartMetric(MetricType.RoundTripTime))
|
using (_telemetry?.StartMetric(MetricType.RoundTripTime))
|
||||||
{
|
{
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.GetClockReq, new GetClockRequest(), _useCompression, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.GetClockReq, new GetClockRequest(), _useCompression,
|
||||||
|
_cipherState, token);
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.ClockRes) throw new Exception("Unexpected response");
|
if (type != MessageType.ClockRes) throw new Exception("Unexpected response");
|
||||||
|
|
||||||
var res = ClockResponse.Parser.ParseFrom(payload);
|
var res = ClockResponse.Parser.ParseFrom(payload);
|
||||||
@@ -232,69 +243,67 @@ public class TcpPeerClient : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the remote peer's vector clock (latest timestamp per node).
|
/// Retrieves the remote peer's vector clock (latest timestamp per node).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>The remote vector clock.</returns>
|
/// <returns>The remote vector clock.</returns>
|
||||||
public async Task<VectorClock> GetVectorClockAsync(CancellationToken token)
|
public async Task<VectorClock> GetVectorClockAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
using (_telemetry?.StartMetric(MetricType.RoundTripTime))
|
using (_telemetry?.StartMetric(MetricType.RoundTripTime))
|
||||||
{
|
{
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.GetVectorClockReq, new GetVectorClockRequest(), _useCompression, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.GetVectorClockReq, new GetVectorClockRequest(),
|
||||||
|
_useCompression, _cipherState, token);
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.VectorClockRes) throw new Exception("Unexpected response");
|
if (type != MessageType.VectorClockRes) throw new Exception("Unexpected response");
|
||||||
|
|
||||||
var res = VectorClockResponse.Parser.ParseFrom(payload);
|
var res = VectorClockResponse.Parser.ParseFrom(payload);
|
||||||
var vectorClock = new VectorClock();
|
var vectorClock = new VectorClock();
|
||||||
|
|
||||||
foreach (var entry in res.Entries)
|
foreach (var entry in res.Entries)
|
||||||
{
|
|
||||||
vectorClock.SetTimestamp(entry.NodeId, new HlcTimestamp(entry.HlcWall, entry.HlcLogic, entry.NodeId));
|
vectorClock.SetTimestamp(entry.NodeId, new HlcTimestamp(entry.HlcWall, entry.HlcLogic, entry.NodeId));
|
||||||
}
|
|
||||||
|
|
||||||
return vectorClock;
|
return vectorClock;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pulls oplog changes from the remote peer since the specified timestamp.
|
/// Pulls oplog changes from the remote peer since the specified timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="since">The starting timestamp for requested changes.</param>
|
/// <param name="since">The starting timestamp for requested changes.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
||||||
public async Task<List<OplogEntry>> PullChangesAsync(HlcTimestamp since, CancellationToken token)
|
public async Task<List<OplogEntry>> PullChangesAsync(HlcTimestamp since, CancellationToken token)
|
||||||
{
|
{
|
||||||
return await PullChangesAsync(since, null, token);
|
return await PullChangesAsync(since, null, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pulls oplog changes from the remote peer since the specified timestamp, filtered by collections.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="since">The starting timestamp for requested changes.</param>
|
|
||||||
/// <param name="collections">Optional collection names used to filter the returned entries.</param>
|
|
||||||
/// <param name="token">Cancellation token.</param>
|
|
||||||
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
|
||||||
public async Task<List<OplogEntry>> PullChangesAsync(HlcTimestamp since, IEnumerable<string>? collections, CancellationToken token)
|
|
||||||
{
|
|
||||||
var req = new PullChangesRequest
|
|
||||||
{
|
|
||||||
SinceWall = since.PhysicalTime,
|
|
||||||
SinceLogic = since.LogicalCounter,
|
|
||||||
// Empty SinceNode indicates a global pull (not source-node filtered).
|
|
||||||
SinceNode = string.Empty
|
|
||||||
};
|
|
||||||
if (collections != null)
|
|
||||||
{
|
|
||||||
foreach (var coll in collections)
|
|
||||||
{
|
|
||||||
req.Collections.Add(coll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.PullChangesReq, req, _useCompression, _cipherState, token);
|
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
/// <summary>
|
||||||
|
/// Pulls oplog changes from the remote peer since the specified timestamp, filtered by collections.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="since">The starting timestamp for requested changes.</param>
|
||||||
|
/// <param name="collections">Optional collection names used to filter the returned entries.</param>
|
||||||
|
/// <param name="token">Cancellation token.</param>
|
||||||
|
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
||||||
|
public async Task<List<OplogEntry>> PullChangesAsync(HlcTimestamp since, IEnumerable<string>? collections,
|
||||||
|
CancellationToken token)
|
||||||
|
{
|
||||||
|
var req = new PullChangesRequest
|
||||||
|
{
|
||||||
|
SinceWall = since.PhysicalTime,
|
||||||
|
SinceLogic = since.LogicalCounter,
|
||||||
|
// Empty SinceNode indicates a global pull (not source-node filtered).
|
||||||
|
SinceNode = string.Empty
|
||||||
|
};
|
||||||
|
if (collections != null)
|
||||||
|
foreach (string coll in collections)
|
||||||
|
req.Collections.Add(coll);
|
||||||
|
|
||||||
|
await _protocol.SendMessageAsync(_stream!, MessageType.PullChangesReq, req, _useCompression, _cipherState,
|
||||||
|
token);
|
||||||
|
|
||||||
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.ChangeSetRes) throw new Exception("Unexpected response");
|
if (type != MessageType.ChangeSetRes) throw new Exception("Unexpected response");
|
||||||
|
|
||||||
var res = ChangeSetResponse.Parser.ParseFrom(payload);
|
var res = ChangeSetResponse.Parser.ParseFrom(payload);
|
||||||
@@ -303,35 +312,38 @@ public class TcpPeerClient : IDisposable
|
|||||||
e.Collection,
|
e.Collection,
|
||||||
e.Key,
|
e.Key,
|
||||||
ParseOp(e.Operation),
|
ParseOp(e.Operation),
|
||||||
string.IsNullOrEmpty(e.JsonData) ? default : System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(e.JsonData),
|
string.IsNullOrEmpty(e.JsonData) ? default : JsonSerializer.Deserialize<JsonElement>(e.JsonData),
|
||||||
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
||||||
e.PreviousHash,
|
e.PreviousHash,
|
||||||
e.Hash // Pass the received hash to preserve integrity reference
|
e.Hash // Pass the received hash to preserve integrity reference
|
||||||
)).ToList();
|
)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pulls oplog changes for a specific node from the remote peer since the specified timestamp.
|
/// Pulls oplog changes for a specific node from the remote peer since the specified timestamp.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="nodeId">The node identifier to filter changes by.</param>
|
/// <param name="nodeId">The node identifier to filter changes by.</param>
|
||||||
/// <param name="since">The starting timestamp for requested changes.</param>
|
/// <param name="since">The starting timestamp for requested changes.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
||||||
public async Task<List<OplogEntry>> PullChangesFromNodeAsync(string nodeId, HlcTimestamp since, CancellationToken token)
|
public async Task<List<OplogEntry>> PullChangesFromNodeAsync(string nodeId, HlcTimestamp since,
|
||||||
{
|
CancellationToken token)
|
||||||
return await PullChangesFromNodeAsync(nodeId, since, null, token);
|
{
|
||||||
}
|
return await PullChangesFromNodeAsync(nodeId, since, null, token);
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Pulls oplog changes for a specific node from the remote peer since the specified timestamp, filtered by collections.
|
/// <summary>
|
||||||
/// </summary>
|
/// Pulls oplog changes for a specific node from the remote peer since the specified timestamp, filtered by
|
||||||
/// <param name="nodeId">The node identifier to filter changes by.</param>
|
/// collections.
|
||||||
/// <param name="since">The starting timestamp for requested changes.</param>
|
/// </summary>
|
||||||
/// <param name="collections">Optional collection names used to filter the returned entries.</param>
|
/// <param name="nodeId">The node identifier to filter changes by.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="since">The starting timestamp for requested changes.</param>
|
||||||
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
/// <param name="collections">Optional collection names used to filter the returned entries.</param>
|
||||||
public async Task<List<OplogEntry>> PullChangesFromNodeAsync(string nodeId, HlcTimestamp since, IEnumerable<string>? collections, CancellationToken token)
|
/// <param name="token">Cancellation token.</param>
|
||||||
{
|
/// <returns>The list of oplog entries returned by the remote peer.</returns>
|
||||||
|
public async Task<List<OplogEntry>> PullChangesFromNodeAsync(string nodeId, HlcTimestamp since,
|
||||||
|
IEnumerable<string>? collections, CancellationToken token)
|
||||||
|
{
|
||||||
var req = new PullChangesRequest
|
var req = new PullChangesRequest
|
||||||
{
|
{
|
||||||
SinceNode = nodeId,
|
SinceNode = nodeId,
|
||||||
@@ -339,15 +351,13 @@ public class TcpPeerClient : IDisposable
|
|||||||
SinceLogic = since.LogicalCounter
|
SinceLogic = since.LogicalCounter
|
||||||
};
|
};
|
||||||
if (collections != null)
|
if (collections != null)
|
||||||
{
|
foreach (string coll in collections)
|
||||||
foreach (var coll in collections)
|
|
||||||
{
|
|
||||||
req.Collections.Add(coll);
|
req.Collections.Add(coll);
|
||||||
}
|
|
||||||
}
|
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.PullChangesReq, req, _useCompression, _cipherState, token);
|
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.PullChangesReq, req, _useCompression, _cipherState,
|
||||||
|
token);
|
||||||
|
|
||||||
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.ChangeSetRes) throw new Exception("Unexpected response");
|
if (type != MessageType.ChangeSetRes) throw new Exception("Unexpected response");
|
||||||
|
|
||||||
var res = ChangeSetResponse.Parser.ParseFrom(payload);
|
var res = ChangeSetResponse.Parser.ParseFrom(payload);
|
||||||
@@ -356,26 +366,28 @@ public class TcpPeerClient : IDisposable
|
|||||||
e.Collection,
|
e.Collection,
|
||||||
e.Key,
|
e.Key,
|
||||||
ParseOp(e.Operation),
|
ParseOp(e.Operation),
|
||||||
string.IsNullOrEmpty(e.JsonData) ? default : System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(e.JsonData),
|
string.IsNullOrEmpty(e.JsonData) ? default : JsonSerializer.Deserialize<JsonElement>(e.JsonData),
|
||||||
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
||||||
e.PreviousHash,
|
e.PreviousHash,
|
||||||
e.Hash
|
e.Hash
|
||||||
)).ToList();
|
)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a range of oplog entries connecting two hashes (Gap Recovery).
|
/// Retrieves a range of oplog entries connecting two hashes (Gap Recovery).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="startHash">The starting hash in the chain.</param>
|
/// <param name="startHash">The starting hash in the chain.</param>
|
||||||
/// <param name="endHash">The ending hash in the chain.</param>
|
/// <param name="endHash">The ending hash in the chain.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>The chain entries connecting the requested hash range.</returns>
|
/// <returns>The chain entries connecting the requested hash range.</returns>
|
||||||
public virtual async Task<List<OplogEntry>> GetChainRangeAsync(string startHash, string endHash, CancellationToken token)
|
public virtual async Task<List<OplogEntry>> GetChainRangeAsync(string startHash, string endHash,
|
||||||
{
|
CancellationToken token)
|
||||||
|
{
|
||||||
var req = new GetChainRangeRequest { StartHash = startHash, EndHash = endHash };
|
var req = new GetChainRangeRequest { StartHash = startHash, EndHash = endHash };
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.GetChainRangeReq, req, _useCompression, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.GetChainRangeReq, req, _useCompression, _cipherState,
|
||||||
|
token);
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.ChainRangeRes) throw new Exception($"Unexpected response for ChainRange: {type}");
|
if (type != MessageType.ChainRangeRes) throw new Exception($"Unexpected response for ChainRange: {type}");
|
||||||
|
|
||||||
var res = ChainRangeResponse.Parser.ParseFrom(payload);
|
var res = ChainRangeResponse.Parser.ParseFrom(payload);
|
||||||
@@ -386,27 +398,26 @@ public class TcpPeerClient : IDisposable
|
|||||||
e.Collection,
|
e.Collection,
|
||||||
e.Key,
|
e.Key,
|
||||||
ParseOp(e.Operation),
|
ParseOp(e.Operation),
|
||||||
string.IsNullOrEmpty(e.JsonData) ? default : System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(e.JsonData),
|
string.IsNullOrEmpty(e.JsonData) ? default : JsonSerializer.Deserialize<JsonElement>(e.JsonData),
|
||||||
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
||||||
e.PreviousHash,
|
e.PreviousHash,
|
||||||
e.Hash
|
e.Hash
|
||||||
)).ToList();
|
)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pushes local oplog changes to the remote peer.
|
/// Pushes local oplog changes to the remote peer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="entries">The oplog entries to push.</param>
|
/// <param name="entries">The oplog entries to push.</param>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="token">Cancellation token.</param>
|
||||||
/// <returns>A task that represents the asynchronous push operation.</returns>
|
/// <returns>A task that represents the asynchronous push operation.</returns>
|
||||||
public async Task PushChangesAsync(IEnumerable<OplogEntry> entries, CancellationToken token)
|
public async Task PushChangesAsync(IEnumerable<OplogEntry> entries, CancellationToken token)
|
||||||
{
|
{
|
||||||
var req = new PushChangesRequest();
|
var req = new PushChangesRequest();
|
||||||
var entryList = entries.ToList();
|
var entryList = entries.ToList();
|
||||||
if (entryList.Count == 0) return;
|
if (entryList.Count == 0) return;
|
||||||
|
|
||||||
foreach (var e in entryList)
|
foreach (var e in entryList)
|
||||||
{
|
|
||||||
req.Entries.Add(new ProtoOplogEntry
|
req.Entries.Add(new ProtoOplogEntry
|
||||||
{
|
{
|
||||||
Collection = e.Collection,
|
Collection = e.Collection,
|
||||||
@@ -419,11 +430,11 @@ public class TcpPeerClient : IDisposable
|
|||||||
Hash = e.Hash,
|
Hash = e.Hash,
|
||||||
PreviousHash = e.PreviousHash
|
PreviousHash = e.PreviousHash
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.PushChangesReq, req, _useCompression, _cipherState, token);
|
await _protocol.SendMessageAsync(_stream!, MessageType.PushChangesReq, req, _useCompression, _cipherState,
|
||||||
|
token);
|
||||||
|
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.AckRes) throw new Exception("Push failed");
|
if (type != MessageType.AckRes) throw new Exception("Push failed");
|
||||||
|
|
||||||
var res = AckResponse.Parser.ParseFrom(payload);
|
var res = AckResponse.Parser.ParseFrom(payload);
|
||||||
@@ -431,72 +442,43 @@ public class TcpPeerClient : IDisposable
|
|||||||
if (!res.Success) throw new Exception("Push failed");
|
if (!res.Success) throw new Exception("Push failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool _useCompression = false; // Negotiated after handshake
|
private OperationType ParseOp(string op)
|
||||||
|
{
|
||||||
private OperationType ParseOp(string op) => Enum.TryParse<OperationType>(op, out var val) ? val : OperationType.Put;
|
return Enum.TryParse<OperationType>(op, out var val) ? val : OperationType.Put;
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Downloads a full snapshot from the remote peer to the provided destination stream.
|
/// <summary>
|
||||||
/// </summary>
|
/// Downloads a full snapshot from the remote peer to the provided destination stream.
|
||||||
/// <param name="destination">The stream that receives snapshot bytes.</param>
|
/// </summary>
|
||||||
/// <param name="token">Cancellation token.</param>
|
/// <param name="destination">The stream that receives snapshot bytes.</param>
|
||||||
/// <returns>A task that represents the asynchronous snapshot transfer operation.</returns>
|
/// <param name="token">Cancellation token.</param>
|
||||||
public async Task GetSnapshotAsync(Stream destination, CancellationToken token)
|
/// <returns>A task that represents the asynchronous snapshot transfer operation.</returns>
|
||||||
{
|
public async Task GetSnapshotAsync(Stream destination, CancellationToken token)
|
||||||
await _protocol.SendMessageAsync(_stream!, MessageType.GetSnapshotReq, new GetSnapshotRequest(), _useCompression, _cipherState, token);
|
{
|
||||||
|
await _protocol.SendMessageAsync(_stream!, MessageType.GetSnapshotReq, new GetSnapshotRequest(),
|
||||||
|
_useCompression, _cipherState, token);
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
var (type, payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
(var type, byte[] payload) = await _protocol.ReadMessageAsync(_stream!, _cipherState, token);
|
||||||
if (type != MessageType.SnapshotChunkMsg) throw new Exception($"Unexpected message type during snapshot: {type}");
|
if (type != MessageType.SnapshotChunkMsg)
|
||||||
|
throw new Exception($"Unexpected message type during snapshot: {type}");
|
||||||
var chunk = SnapshotChunk.Parser.ParseFrom(payload);
|
|
||||||
if (chunk.Data.Length > 0)
|
var chunk = SnapshotChunk.Parser.ParseFrom(payload);
|
||||||
{
|
if (chunk.Data.Length > 0)
|
||||||
await destination.WriteAsync(chunk.Data.ToByteArray(), 0, chunk.Data.Length, token);
|
await destination.WriteAsync(chunk.Data.ToByteArray(), 0, chunk.Data.Length, token);
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.IsLast) break;
|
if (chunk.IsLast) break;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases resources used by the peer client.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (_connectionLock)
|
|
||||||
{
|
|
||||||
if (_disposed) return;
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Error disposing network stream");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_client?.Dispose();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Error disposing TCP client");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Disposed connection to peer: {Address}", _peerAddress);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SnapshotRequiredException : Exception
|
public class SnapshotRequiredException : Exception
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SnapshotRequiredException"/> class.
|
/// Initializes a new instance of the <see cref="SnapshotRequiredException" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SnapshotRequiredException() : base("Peer requires a full snapshot sync.") { }
|
public SnapshotRequiredException() : base("Peer requires a full snapshot sync.")
|
||||||
}
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,69 +1,67 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Google.Protobuf;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Serilog.Context;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Proto;
|
using ZB.MOM.WW.CBDDC.Network.Proto;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Security;
|
|
||||||
using ZB.MOM.WW.CBDDC.Network.Protocol;
|
using ZB.MOM.WW.CBDDC.Network.Protocol;
|
||||||
|
using ZB.MOM.WW.CBDDC.Network.Security;
|
||||||
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
using ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
using Google.Protobuf;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Serilog.Context;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// TCP server that handles incoming synchronization requests from remote peers.
|
/// TCP server that handles incoming synchronization requests from remote peers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class TcpSyncServer : ISyncServer
|
internal class TcpSyncServer : ISyncServer
|
||||||
{
|
{
|
||||||
private readonly IOplogStore _oplogStore;
|
|
||||||
private readonly IDocumentStore _documentStore;
|
|
||||||
private readonly ISnapshotService _snapshotStore;
|
|
||||||
private readonly ILogger<TcpSyncServer> _logger;
|
|
||||||
private readonly IPeerNodeConfigurationProvider _configProvider;
|
|
||||||
private CancellationTokenSource? _cts;
|
|
||||||
private TcpListener? _listener;
|
|
||||||
private readonly object _startStopLock = new object();
|
|
||||||
private int _activeConnections = 0;
|
|
||||||
|
|
||||||
internal int MaxConnections = 100;
|
|
||||||
private const int ClientOperationTimeoutMs = 60000;
|
private const int ClientOperationTimeoutMs = 60000;
|
||||||
|
|
||||||
private readonly IAuthenticator _authenticator;
|
private readonly IAuthenticator _authenticator;
|
||||||
|
private readonly IPeerNodeConfigurationProvider _configProvider;
|
||||||
|
private readonly IDocumentStore _documentStore;
|
||||||
private readonly IPeerHandshakeService _handshakeService;
|
private readonly IPeerHandshakeService _handshakeService;
|
||||||
|
private readonly ILogger<TcpSyncServer> _logger;
|
||||||
|
private readonly IOplogStore _oplogStore;
|
||||||
|
private readonly ISnapshotService _snapshotStore;
|
||||||
|
private readonly object _startStopLock = new();
|
||||||
private readonly INetworkTelemetryService? _telemetry;
|
private readonly INetworkTelemetryService? _telemetry;
|
||||||
|
private int _activeConnections;
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private TcpListener? _listener;
|
||||||
|
|
||||||
|
internal int MaxConnections = 100;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the TcpSyncServer class with the specified peer oplogStore, configuration provider,
|
/// Initializes a new instance of the TcpSyncServer class with the specified peer oplogStore, configuration provider,
|
||||||
/// logger, and authenticator.
|
/// logger, and authenticator.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>The server automatically restarts when the configuration provided by
|
/// <remarks>
|
||||||
/// peerNodeConfigurationProvider changes. This ensures that configuration updates are applied without requiring
|
/// The server automatically restarts when the configuration provided by
|
||||||
/// manual intervention.</remarks>
|
/// peerNodeConfigurationProvider changes. This ensures that configuration updates are applied without requiring
|
||||||
/// <param name="oplogStore">The peer oplogStore used to manage and persist peer information for the server.</param>
|
/// manual intervention.
|
||||||
/// <param name="documentStore">The document store used to read and apply synchronized documents.</param>
|
/// </remarks>
|
||||||
/// <param name="snapshotStore">The snapshot store used to create and manage database snapshots for synchronization.</param>
|
/// <param name="oplogStore">The peer oplogStore used to manage and persist peer information for the server.</param>
|
||||||
/// <param name="peerNodeConfigurationProvider">The provider that supplies configuration settings for the peer node and notifies the server of configuration
|
/// <param name="documentStore">The document store used to read and apply synchronized documents.</param>
|
||||||
/// changes.</param>
|
/// <param name="snapshotStore">The snapshot store used to create and manage database snapshots for synchronization.</param>
|
||||||
/// <param name="logger">The logger used to record informational and error messages for the server instance.</param>
|
/// <param name="peerNodeConfigurationProvider">
|
||||||
/// <param name="authenticator">The authenticator responsible for validating peer connections to the server.</param>
|
/// The provider that supplies configuration settings for the peer node and notifies the server of configuration
|
||||||
/// <param name="handshakeService">The service used to perform secure handshake (optional).</param>
|
/// changes.
|
||||||
/// <param name="telemetry">The optional telemetry service used to record network performance metrics.</param>
|
/// </param>
|
||||||
public TcpSyncServer(
|
/// <param name="logger">The logger used to record informational and error messages for the server instance.</param>
|
||||||
IOplogStore oplogStore,
|
/// <param name="authenticator">The authenticator responsible for validating peer connections to the server.</param>
|
||||||
IDocumentStore documentStore,
|
/// <param name="handshakeService">The service used to perform secure handshake (optional).</param>
|
||||||
|
/// <param name="telemetry">The optional telemetry service used to record network performance metrics.</param>
|
||||||
|
public TcpSyncServer(
|
||||||
|
IOplogStore oplogStore,
|
||||||
|
IDocumentStore documentStore,
|
||||||
ISnapshotService snapshotStore,
|
ISnapshotService snapshotStore,
|
||||||
IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
||||||
ILogger<TcpSyncServer> logger,
|
ILogger<TcpSyncServer> logger,
|
||||||
IAuthenticator authenticator,
|
IAuthenticator authenticator,
|
||||||
IPeerHandshakeService handshakeService,
|
IPeerHandshakeService handshakeService,
|
||||||
INetworkTelemetryService? telemetry = null)
|
INetworkTelemetryService? telemetry = null)
|
||||||
@@ -85,10 +83,17 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the TCP synchronization server and begins listening for incoming connections asynchronously.
|
/// Gets the port on which the server is listening.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>If the server is already running, this method returns immediately without starting a new
|
public int? ListeningPort => ListeningEndpoint?.Port;
|
||||||
/// listener. The server will listen on the TCP port specified in the current configuration.</remarks>
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts the TCP synchronization server and begins listening for incoming connections asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// If the server is already running, this method returns immediately without starting a new
|
||||||
|
/// listener. The server will listen on the TCP port specified in the current configuration.
|
||||||
|
/// </remarks>
|
||||||
/// <returns>A task that represents the asynchronous start operation.</returns>
|
/// <returns>A task that represents the asynchronous start operation.</returns>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
@@ -101,6 +106,7 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
_logger.LogWarning("TCP Sync Server already started");
|
_logger.LogWarning("TCP Sync Server already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,31 +132,33 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the listener and cancels any pending operations.
|
/// Stops the listener and cancels any pending operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>After calling this method, the listener will no longer accept new connections or process
|
/// <remarks>
|
||||||
/// requests. This method is safe to call multiple times; subsequent calls have no effect if the listener is already
|
/// After calling this method, the listener will no longer accept new connections or process
|
||||||
/// stopped.</remarks>
|
/// requests. This method is safe to call multiple times; subsequent calls have no effect if the listener is already
|
||||||
|
/// stopped.
|
||||||
|
/// </remarks>
|
||||||
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
/// <returns>A task that represents the asynchronous stop operation.</returns>
|
||||||
public async Task Stop()
|
public async Task Stop()
|
||||||
{
|
{
|
||||||
CancellationTokenSource? ctsToDispose = null;
|
CancellationTokenSource? ctsToDispose = null;
|
||||||
TcpListener? listenerToStop = null;
|
TcpListener? listenerToStop = null;
|
||||||
|
|
||||||
lock (_startStopLock)
|
lock (_startStopLock)
|
||||||
{
|
{
|
||||||
if (_cts == null)
|
if (_cts == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("TCP Sync Server already stopped or never started");
|
_logger.LogWarning("TCP Sync Server already stopped or never started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctsToDispose = _cts;
|
ctsToDispose = _cts;
|
||||||
listenerToStop = _listener;
|
listenerToStop = _listener;
|
||||||
_cts = null;
|
_cts = null;
|
||||||
_listener = null;
|
_listener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ctsToDispose.Cancel();
|
ctsToDispose.Cancel();
|
||||||
@@ -162,32 +170,26 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ctsToDispose.Dispose();
|
ctsToDispose.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
listenerToStop?.Stop();
|
listenerToStop?.Stop();
|
||||||
|
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the full local endpoint on which the server is listening.
|
/// Gets the full local endpoint on which the server is listening.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IPEndPoint? ListeningEndpoint => _listener?.LocalEndpoint as IPEndPoint;
|
public IPEndPoint? ListeningEndpoint => _listener?.LocalEndpoint as IPEndPoint;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the port on which the server is listening.
|
|
||||||
/// </summary>
|
|
||||||
public int? ListeningPort => ListeningEndpoint?.Port;
|
|
||||||
|
|
||||||
private async Task ListenAsync(CancellationToken token)
|
private async Task ListenAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_listener == null) break;
|
if (_listener == null) break;
|
||||||
var client = await _listener.AcceptTcpClientAsync();
|
var client = await _listener.AcceptTcpClientAsync();
|
||||||
|
|
||||||
if (_activeConnections >= MaxConnections)
|
if (_activeConnections >= MaxConnections)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Max connections reached ({Max}). Rejecting client.", MaxConnections);
|
_logger.LogWarning("Max connections reached ({Max}). Rejecting client.", MaxConnections);
|
||||||
@@ -197,7 +199,7 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
|
|
||||||
Interlocked.Increment(ref _activeConnections);
|
Interlocked.Increment(ref _activeConnections);
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -209,46 +211,47 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) { break; }
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "TCP Accept Error");
|
_logger.LogError(ex, "TCP Accept Error");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleClientAsync(TcpClient client, CancellationToken token)
|
private async Task HandleClientAsync(TcpClient client, CancellationToken token)
|
||||||
{
|
{
|
||||||
var remoteEp = client.Client.RemoteEndPoint;
|
var remoteEp = client.Client.RemoteEndPoint;
|
||||||
using var operationContext = LogContext.PushProperty("OperationId", Guid.NewGuid().ToString("N"));
|
using var operationContext = LogContext.PushProperty("OperationId", Guid.NewGuid().ToString("N"));
|
||||||
using var endpointContext = LogContext.PushProperty("RemoteEndpoint", remoteEp?.ToString() ?? "unknown");
|
using var endpointContext = LogContext.PushProperty("RemoteEndpoint", remoteEp?.ToString() ?? "unknown");
|
||||||
_logger.LogDebug("Client Connected: {Endpoint}", remoteEp);
|
_logger.LogDebug("Client Connected: {Endpoint}", remoteEp);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (client)
|
using (client)
|
||||||
using (var stream = client.GetStream())
|
using (var stream = client.GetStream())
|
||||||
{
|
{
|
||||||
// CRITICAL for Android: Disable Nagle's algorithm for immediate packet send
|
// CRITICAL for Android: Disable Nagle's algorithm for immediate packet send
|
||||||
client.NoDelay = true;
|
client.NoDelay = true;
|
||||||
|
|
||||||
// Configure TCP keepalive
|
// Configure TCP keepalive
|
||||||
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
|
||||||
|
|
||||||
// Set stream timeouts
|
// Set stream timeouts
|
||||||
stream.ReadTimeout = ClientOperationTimeoutMs;
|
stream.ReadTimeout = ClientOperationTimeoutMs;
|
||||||
stream.WriteTimeout = ClientOperationTimeoutMs;
|
stream.WriteTimeout = ClientOperationTimeoutMs;
|
||||||
|
|
||||||
var protocol = new ProtocolHandler(_logger, _telemetry);
|
var protocol = new ProtocolHandler(_logger, _telemetry);
|
||||||
|
|
||||||
bool useCompression = false;
|
var useCompression = false;
|
||||||
CipherState? cipherState = null;
|
CipherState? cipherState = null;
|
||||||
List<string> remoteInterests = new();
|
List<string> remoteInterests = new();
|
||||||
|
|
||||||
// Perform Secure Handshake (if service is available)
|
// Perform Secure Handshake (if service is available)
|
||||||
var config = await _configProvider.GetConfiguration();
|
var config = await _configProvider.GetConfiguration();
|
||||||
if (_handshakeService != null)
|
if (_handshakeService != null)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// We are NOT initiator
|
// We are NOT initiator
|
||||||
@@ -261,40 +264,39 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
_logger.LogError(ex, "Secure Handshake failed check logic");
|
_logger.LogError(ex, "Secure Handshake failed check logic");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
while (client.Connected && !token.IsCancellationRequested)
|
while (client.Connected && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Re-fetch config if needed, though usually stable
|
// Re-fetch config if needed, though usually stable
|
||||||
config = await _configProvider.GetConfiguration();
|
config = await _configProvider.GetConfiguration();
|
||||||
|
|
||||||
var (type, payload) = await protocol.ReadMessageAsync(stream, cipherState, token);
|
(var type, byte[] payload) = await protocol.ReadMessageAsync(stream, cipherState, token);
|
||||||
if (type == MessageType.Unknown) break; // EOF or Error
|
if (type == MessageType.Unknown) break; // EOF or Error
|
||||||
|
|
||||||
// Handshake Loop
|
// Handshake Loop
|
||||||
if (type == MessageType.HandshakeReq)
|
if (type == MessageType.HandshakeReq)
|
||||||
{
|
{
|
||||||
var hReq = HandshakeRequest.Parser.ParseFrom(payload);
|
var hReq = HandshakeRequest.Parser.ParseFrom(payload);
|
||||||
_logger.LogDebug("Received HandshakeReq from Node {NodeId}", hReq.NodeId);
|
_logger.LogDebug("Received HandshakeReq from Node {NodeId}", hReq.NodeId);
|
||||||
|
|
||||||
// Track remote peer interests
|
// Track remote peer interests
|
||||||
remoteInterests = hReq.InterestingCollections.ToList();
|
remoteInterests = hReq.InterestingCollections.ToList();
|
||||||
|
|
||||||
bool valid = await _authenticator.ValidateAsync(hReq.NodeId, hReq.AuthToken);
|
bool valid = await _authenticator.ValidateAsync(hReq.NodeId, hReq.AuthToken);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Authentication failed for Node {NodeId}", hReq.NodeId);
|
_logger.LogWarning("Authentication failed for Node {NodeId}", hReq.NodeId);
|
||||||
await protocol.SendMessageAsync(stream, MessageType.HandshakeRes, new HandshakeResponse { NodeId = config.NodeId, Accepted = false }, false, cipherState, token);
|
await protocol.SendMessageAsync(stream, MessageType.HandshakeRes,
|
||||||
|
new HandshakeResponse { NodeId = config.NodeId, Accepted = false }, false, cipherState,
|
||||||
|
token);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hRes = new HandshakeResponse { NodeId = config.NodeId, Accepted = true };
|
var hRes = new HandshakeResponse { NodeId = config.NodeId, Accepted = true };
|
||||||
|
|
||||||
// Include local interests from IDocumentStore in response for push filtering
|
// Include local interests from IDocumentStore in response for push filtering
|
||||||
foreach (var coll in _documentStore.InterestedCollection)
|
foreach (string coll in _documentStore.InterestedCollection)
|
||||||
{
|
|
||||||
hRes.InterestingCollections.Add(coll);
|
hRes.InterestingCollections.Add(coll);
|
||||||
}
|
|
||||||
|
|
||||||
if (CompressionHelper.IsBrotliSupported && hReq.SupportedCompression.Contains("brotli"))
|
if (CompressionHelper.IsBrotliSupported && hReq.SupportedCompression.Contains("brotli"))
|
||||||
{
|
{
|
||||||
@@ -302,12 +304,13 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
useCompression = true;
|
useCompression = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await protocol.SendMessageAsync(stream, MessageType.HandshakeRes, hRes, false, cipherState, token);
|
await protocol.SendMessageAsync(stream, MessageType.HandshakeRes, hRes, false, cipherState,
|
||||||
|
token);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
IMessage? response = null;
|
IMessage? response = null;
|
||||||
MessageType resType = MessageType.Unknown;
|
var resType = MessageType.Unknown;
|
||||||
|
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
@@ -325,7 +328,7 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
case MessageType.GetVectorClockReq:
|
case MessageType.GetVectorClockReq:
|
||||||
var vectorClock = await _oplogStore.GetVectorClockAsync(token);
|
var vectorClock = await _oplogStore.GetVectorClockAsync(token);
|
||||||
var vcRes = new VectorClockResponse();
|
var vcRes = new VectorClockResponse();
|
||||||
foreach (var nodeId in vectorClock.NodeIds)
|
foreach (string nodeId in vectorClock.NodeIds)
|
||||||
{
|
{
|
||||||
var ts = vectorClock.GetTimestamp(nodeId);
|
var ts = vectorClock.GetTimestamp(nodeId);
|
||||||
vcRes.Entries.Add(new VectorClockEntry
|
vcRes.Entries.Add(new VectorClockEntry
|
||||||
@@ -335,23 +338,23 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
HlcLogic = ts.LogicalCounter
|
HlcLogic = ts.LogicalCounter
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
response = vcRes;
|
response = vcRes;
|
||||||
resType = MessageType.VectorClockRes;
|
resType = MessageType.VectorClockRes;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MessageType.PullChangesReq:
|
case MessageType.PullChangesReq:
|
||||||
var pReq = PullChangesRequest.Parser.ParseFrom(payload);
|
var pReq = PullChangesRequest.Parser.ParseFrom(payload);
|
||||||
var since = new HlcTimestamp(pReq.SinceWall, pReq.SinceLogic, pReq.SinceNode);
|
var since = new HlcTimestamp(pReq.SinceWall, pReq.SinceLogic, pReq.SinceNode);
|
||||||
|
|
||||||
// Use collection filter from request
|
// Use collection filter from request
|
||||||
var filter = pReq.Collections.Any() ? pReq.Collections : null;
|
var filter = pReq.Collections.Any() ? pReq.Collections : null;
|
||||||
var oplog = string.IsNullOrWhiteSpace(pReq.SinceNode)
|
var oplog = string.IsNullOrWhiteSpace(pReq.SinceNode)
|
||||||
? await _oplogStore.GetOplogAfterAsync(since, filter, token)
|
? await _oplogStore.GetOplogAfterAsync(since, filter, token)
|
||||||
: await _oplogStore.GetOplogForNodeAfterAsync(pReq.SinceNode, since, filter, token);
|
: await _oplogStore.GetOplogForNodeAfterAsync(pReq.SinceNode, since, filter, token);
|
||||||
|
|
||||||
var csRes = new ChangeSetResponse();
|
var csRes = new ChangeSetResponse();
|
||||||
foreach (var e in oplog)
|
foreach (var e in oplog)
|
||||||
{
|
|
||||||
csRes.Entries.Add(new ProtoOplogEntry
|
csRes.Entries.Add(new ProtoOplogEntry
|
||||||
{
|
{
|
||||||
Collection = e.Collection,
|
Collection = e.Collection,
|
||||||
@@ -364,7 +367,6 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
Hash = e.Hash,
|
Hash = e.Hash,
|
||||||
PreviousHash = e.PreviousHash
|
PreviousHash = e.PreviousHash
|
||||||
});
|
});
|
||||||
}
|
|
||||||
response = csRes;
|
response = csRes;
|
||||||
resType = MessageType.ChangeSetRes;
|
resType = MessageType.ChangeSetRes;
|
||||||
break;
|
break;
|
||||||
@@ -375,10 +377,12 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
e.Collection,
|
e.Collection,
|
||||||
e.Key,
|
e.Key,
|
||||||
(OperationType)Enum.Parse(typeof(OperationType), e.Operation),
|
(OperationType)Enum.Parse(typeof(OperationType), e.Operation),
|
||||||
string.IsNullOrEmpty(e.JsonData) ? (System.Text.Json.JsonElement?)null : System.Text.Json.JsonSerializer.Deserialize<System.Text.Json.JsonElement>(e.JsonData),
|
string.IsNullOrEmpty(e.JsonData)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize<JsonElement>(e.JsonData),
|
||||||
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
new HlcTimestamp(e.HlcWall, e.HlcLogic, e.HlcNode),
|
||||||
e.PreviousHash, // Restore PreviousHash
|
e.PreviousHash, // Restore PreviousHash
|
||||||
e.Hash // Restore Hash
|
e.Hash // Restore Hash
|
||||||
));
|
));
|
||||||
|
|
||||||
await _oplogStore.ApplyBatchAsync(entries, token);
|
await _oplogStore.ApplyBatchAsync(entries, token);
|
||||||
@@ -389,18 +393,15 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
|
|
||||||
case MessageType.GetChainRangeReq:
|
case MessageType.GetChainRangeReq:
|
||||||
var rangeReq = GetChainRangeRequest.Parser.ParseFrom(payload);
|
var rangeReq = GetChainRangeRequest.Parser.ParseFrom(payload);
|
||||||
var rangeEntries = await _oplogStore.GetChainRangeAsync(rangeReq.StartHash, rangeReq.EndHash, token);
|
var rangeEntries =
|
||||||
var rangeRes = new ChainRangeResponse();
|
await _oplogStore.GetChainRangeAsync(rangeReq.StartHash, rangeReq.EndHash, token);
|
||||||
|
var rangeRes = new ChainRangeResponse();
|
||||||
|
|
||||||
if (!rangeEntries.Any() && rangeReq.StartHash != rangeReq.EndHash)
|
if (!rangeEntries.Any() && rangeReq.StartHash != rangeReq.EndHash)
|
||||||
{
|
|
||||||
// Gap cannot be filled (likely pruned or unknown branch)
|
// Gap cannot be filled (likely pruned or unknown branch)
|
||||||
rangeRes.SnapshotRequired = true;
|
rangeRes.SnapshotRequired = true;
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
foreach (var e in rangeEntries)
|
foreach (var e in rangeEntries)
|
||||||
{
|
|
||||||
rangeRes.Entries.Add(new ProtoOplogEntry
|
rangeRes.Entries.Add(new ProtoOplogEntry
|
||||||
{
|
{
|
||||||
Collection = e.Collection,
|
Collection = e.Collection,
|
||||||
@@ -413,52 +414,52 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
Hash = e.Hash,
|
Hash = e.Hash,
|
||||||
PreviousHash = e.PreviousHash
|
PreviousHash = e.PreviousHash
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
response = rangeRes;
|
response = rangeRes;
|
||||||
resType = MessageType.ChainRangeRes;
|
resType = MessageType.ChainRangeRes;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MessageType.GetSnapshotReq:
|
case MessageType.GetSnapshotReq:
|
||||||
_logger.LogInformation("Processing GetSnapshotReq from {Endpoint}", remoteEp);
|
_logger.LogInformation("Processing GetSnapshotReq from {Endpoint}", remoteEp);
|
||||||
var tempFile = Path.GetTempFileName();
|
string tempFile = Path.GetTempFileName();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create backup
|
// Create backup
|
||||||
using (var fs = File.Create(tempFile))
|
using (var fs = File.Create(tempFile))
|
||||||
{
|
{
|
||||||
await _snapshotStore.CreateSnapshotAsync(fs, token);
|
await _snapshotStore.CreateSnapshotAsync(fs, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var fs = File.OpenRead(tempFile))
|
using (var fs = File.OpenRead(tempFile))
|
||||||
{
|
{
|
||||||
byte[] buffer = new byte[80 * 1024]; // 80KB chunks
|
var buffer = new byte[80 * 1024]; // 80KB chunks
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
|
while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length, token)) > 0)
|
||||||
{
|
{
|
||||||
var chunk = new SnapshotChunk
|
var chunk = new SnapshotChunk
|
||||||
{
|
{
|
||||||
Data = ByteString.CopyFrom(buffer, 0, bytesRead),
|
Data = ByteString.CopyFrom(buffer, 0, bytesRead),
|
||||||
IsLast = false
|
IsLast = false
|
||||||
};
|
};
|
||||||
await protocol.SendMessageAsync(stream, MessageType.SnapshotChunkMsg, chunk, false, cipherState, token);
|
await protocol.SendMessageAsync(stream, MessageType.SnapshotChunkMsg, chunk,
|
||||||
}
|
false, cipherState, token);
|
||||||
|
}
|
||||||
// Send End of Snapshot
|
|
||||||
await protocol.SendMessageAsync(stream, MessageType.SnapshotChunkMsg, new SnapshotChunk { IsLast = true }, false, cipherState, token);
|
// Send End of Snapshot
|
||||||
|
await protocol.SendMessageAsync(stream, MessageType.SnapshotChunkMsg,
|
||||||
|
new SnapshotChunk { IsLast = true }, false, cipherState, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (File.Exists(tempFile)) File.Delete(tempFile);
|
if (File.Exists(tempFile)) File.Delete(tempFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response != null)
|
if (response != null)
|
||||||
{
|
|
||||||
await protocol.SendMessageAsync(stream, resType, response, useCompression, cipherState, token);
|
await protocol.SendMessageAsync(stream, resType, response, useCompression, cipherState, token);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -471,4 +472,4 @@ internal class TcpSyncServer : ISyncServer
|
|||||||
_logger.LogDebug("Client Disconnected: {Endpoint}", remoteEp);
|
_logger.LogDebug("Client Disconnected: {Endpoint}", remoteEp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,38 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics;
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Telemetry;
|
|
||||||
|
|
||||||
public interface INetworkTelemetryService
|
public interface INetworkTelemetryService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a metric value for the specified metric type.
|
/// Records a metric value for the specified metric type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The metric type to record.</param>
|
/// <param name="type">The metric type to record.</param>
|
||||||
/// <param name="value">The metric value.</param>
|
/// <param name="value">The metric value.</param>
|
||||||
void RecordValue(MetricType type, double value);
|
void RecordValue(MetricType type, double value);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts timing a metric for the specified metric type.
|
/// Starts timing a metric for the specified metric type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The metric type to time.</param>
|
/// <param name="type">The metric type to time.</param>
|
||||||
/// <returns>A timer that records elapsed time when disposed.</returns>
|
/// <returns>A timer that records elapsed time when disposed.</returns>
|
||||||
MetricTimer StartMetric(MetricType type);
|
MetricTimer StartMetric(MetricType type);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a snapshot of all recorded metric values.
|
/// Gets a snapshot of all recorded metric values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A dictionary of metric values grouped by metric type and bucket.</returns>
|
/// <returns>A dictionary of metric values grouped by metric type and bucket.</returns>
|
||||||
System.Collections.Generic.Dictionary<MetricType, System.Collections.Generic.Dictionary<int, double>> GetSnapshot();
|
Dictionary<MetricType, Dictionary<int, double>> GetSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct MetricTimer : IDisposable
|
public readonly struct MetricTimer : IDisposable
|
||||||
{
|
{
|
||||||
private readonly INetworkTelemetryService _service;
|
private readonly INetworkTelemetryService _service;
|
||||||
private readonly MetricType _type;
|
private readonly MetricType _type;
|
||||||
private readonly long _startTimestamp;
|
private readonly long _startTimestamp;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new metric timer.
|
/// Initializes a new metric timer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="service">The telemetry service that receives the recorded value.</param>
|
/// <param name="service">The telemetry service that receives the recorded value.</param>
|
||||||
/// <param name="type">The metric type being timed.</param>
|
/// <param name="type">The metric type being timed.</param>
|
||||||
@@ -45,16 +44,16 @@ public readonly struct MetricTimer : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops timing and records the elapsed duration.
|
/// Stops timing and records the elapsed duration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
var elapsed = Stopwatch.GetTimestamp() - _startTimestamp;
|
long elapsed = Stopwatch.GetTimestamp() - _startTimestamp;
|
||||||
// Convert ticks to milliseconds? Or keep as ticks?
|
// Convert ticks to milliseconds? Or keep as ticks?
|
||||||
// Plan said "latency", usually ms.
|
// Plan said "latency", usually ms.
|
||||||
// Stopwatch.Frequency depends on hardware.
|
// Stopwatch.Frequency depends on hardware.
|
||||||
// Let's store MS representation.
|
// Let's store MS representation.
|
||||||
double ms = (double)elapsed * 1000 / Stopwatch.Frequency;
|
double ms = (double)elapsed * 1000 / Stopwatch.Frequency;
|
||||||
_service.RecordValue(_type, ms);
|
_service.RecordValue(_type, ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,4 @@ public enum MetricType
|
|||||||
EncryptionTime = 1,
|
EncryptionTime = 1,
|
||||||
DecryptionTime = 2,
|
DecryptionTime = 2,
|
||||||
RoundTripTime = 3
|
RoundTripTime = 3
|
||||||
}
|
}
|
||||||
@@ -1,101 +1,99 @@
|
|||||||
using System;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network.Telemetry;
|
namespace ZB.MOM.WW.CBDDC.Network.Telemetry;
|
||||||
|
|
||||||
public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly Channel<(MetricType Type, double Value)> _metricChannel;
|
// Aggregation State
|
||||||
private readonly CancellationTokenSource _cts;
|
// We keep 30m of history with 1s resolution = 1800 buckets.
|
||||||
private readonly ILogger<NetworkTelemetryService> _logger;
|
private const int MaxHistorySeconds = 1800;
|
||||||
private readonly string _persistencePath;
|
|
||||||
|
|
||||||
// Aggregation State
|
|
||||||
// We keep 30m of history with 1s resolution = 1800 buckets.
|
|
||||||
private const int MaxHistorySeconds = 1800;
|
|
||||||
|
|
||||||
private readonly object _lock = new object();
|
|
||||||
private readonly MetricBucket[] _history;
|
|
||||||
private int _headIndex = 0; // Points to current second
|
|
||||||
private long _currentSecondTimestamp; // Unix timestamp of current bucket
|
|
||||||
|
|
||||||
// Rolling Averages (Last calculated)
|
// Rolling Averages (Last calculated)
|
||||||
private readonly Dictionary<string, double> _averages = new Dictionary<string, double>();
|
private readonly Dictionary<string, double> _averages = new();
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
private readonly MetricBucket[] _history;
|
||||||
|
|
||||||
/// <summary>
|
private readonly object _lock = new();
|
||||||
/// Initializes a new instance of the <see cref="NetworkTelemetryService"/> class.
|
private readonly ILogger<NetworkTelemetryService> _logger;
|
||||||
/// </summary>
|
private readonly Channel<(MetricType Type, double Value)> _metricChannel;
|
||||||
/// <param name="logger">The logger used to report telemetry processing and persistence errors.</param>
|
private readonly string _persistencePath;
|
||||||
/// <param name="persistencePath">The file path where persisted telemetry snapshots are written.</param>
|
private long _currentSecondTimestamp; // Unix timestamp of current bucket
|
||||||
public NetworkTelemetryService(ILogger<NetworkTelemetryService> logger, string persistencePath)
|
private int _headIndex; // Points to current second
|
||||||
{
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NetworkTelemetryService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger used to report telemetry processing and persistence errors.</param>
|
||||||
|
/// <param name="persistencePath">The file path where persisted telemetry snapshots are written.</param>
|
||||||
|
public NetworkTelemetryService(ILogger<NetworkTelemetryService> logger, string persistencePath)
|
||||||
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_persistencePath = persistencePath;
|
_persistencePath = persistencePath;
|
||||||
_metricChannel = Channel.CreateUnbounded<(MetricType, double)>(new UnboundedChannelOptions
|
_metricChannel = Channel.CreateUnbounded<(MetricType, double)>(new UnboundedChannelOptions
|
||||||
{
|
{
|
||||||
SingleReader = true,
|
SingleReader = true,
|
||||||
SingleWriter = false
|
SingleWriter = false
|
||||||
});
|
});
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
_history = new MetricBucket[MaxHistorySeconds];
|
_history = new MetricBucket[MaxHistorySeconds];
|
||||||
for (int i = 0; i < MaxHistorySeconds; i++) _history[i] = new MetricBucket();
|
for (var i = 0; i < MaxHistorySeconds; i++) _history[i] = new MetricBucket();
|
||||||
|
|
||||||
|
_currentSecondTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
|
||||||
_currentSecondTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
|
|
||||||
_ = Task.Run(ProcessMetricsLoop);
|
_ = Task.Run(ProcessMetricsLoop);
|
||||||
_ = Task.Run(PersistenceLoop);
|
_ = Task.Run(PersistenceLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Records a metric value for the specified metric type.
|
/// Releases resources used by the telemetry service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The metric category to update.</param>
|
public void Dispose()
|
||||||
/// <param name="value">The metric value to record.</param>
|
{
|
||||||
public void RecordValue(MetricType type, double value)
|
_cts.Cancel();
|
||||||
{
|
_cts.Dispose();
|
||||||
_metricChannel.Writer.TryWrite((type, value));
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
/// <summary>
|
/// Records a metric value for the specified metric type.
|
||||||
/// Starts a timer for the specified metric type.
|
/// </summary>
|
||||||
/// </summary>
|
/// <param name="type">The metric category to update.</param>
|
||||||
/// <param name="type">The metric category to time.</param>
|
/// <param name="value">The metric value to record.</param>
|
||||||
/// <returns>A metric timer that records elapsed time when disposed.</returns>
|
public void RecordValue(MetricType type, double value)
|
||||||
public MetricTimer StartMetric(MetricType type)
|
{
|
||||||
{
|
_metricChannel.Writer.TryWrite((type, value));
|
||||||
return new MetricTimer(this, type);
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
/// <summary>
|
/// Starts a timer for the specified metric type.
|
||||||
/// Gets a point-in-time snapshot of rolling averages for each metric type.
|
/// </summary>
|
||||||
/// </summary>
|
/// <param name="type">The metric category to time.</param>
|
||||||
/// <returns>A dictionary keyed by metric type containing average values by window size in seconds.</returns>
|
/// <returns>A metric timer that records elapsed time when disposed.</returns>
|
||||||
public Dictionary<MetricType, Dictionary<int, double>> GetSnapshot()
|
public MetricTimer StartMetric(MetricType type)
|
||||||
{
|
{
|
||||||
|
return new MetricTimer(this, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a point-in-time snapshot of rolling averages for each metric type.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A dictionary keyed by metric type containing average values by window size in seconds.</returns>
|
||||||
|
public Dictionary<MetricType, Dictionary<int, double>> GetSnapshot()
|
||||||
|
{
|
||||||
var snapshot = new Dictionary<MetricType, Dictionary<int, double>>();
|
var snapshot = new Dictionary<MetricType, Dictionary<int, double>>();
|
||||||
var windows = new[] { 60, 300, 600, 1800 };
|
var windows = new[] { 60, 300, 600, 1800 };
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
foreach (var type in Enum.GetValues(typeof(MetricType)).Cast<MetricType>())
|
foreach (var type in Enum.GetValues(typeof(MetricType)).Cast<MetricType>())
|
||||||
{
|
{
|
||||||
var typeDict = new Dictionary<int, double>();
|
var typeDict = new Dictionary<int, double>();
|
||||||
foreach (var w in windows)
|
foreach (int w in windows) typeDict[w] = CalculateAverage(type, w);
|
||||||
{
|
|
||||||
typeDict[w] = CalculateAverage(type, w);
|
|
||||||
}
|
|
||||||
snapshot[type] = typeDict;
|
snapshot[type] = typeDict;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,29 +101,26 @@ public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
|||||||
{
|
{
|
||||||
var reader = _metricChannel.Reader;
|
var reader = _metricChannel.Reader;
|
||||||
while (!_cts.IsCancellationRequested)
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await reader.WaitToReadAsync(_cts.Token))
|
if (await reader.WaitToReadAsync(_cts.Token))
|
||||||
{
|
|
||||||
while (reader.TryRead(out var item))
|
while (reader.TryRead(out var item))
|
||||||
{
|
|
||||||
AddMetricToCurrentBucket(item.Type, item.Value);
|
AddMetricToCurrentBucket(item.Type, item.Value);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { break; }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error processing metrics");
|
_logger.LogError(ex, "Error processing metrics");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddMetricToCurrentBucket(MetricType type, double value)
|
private void AddMetricToCurrentBucket(MetricType type, double value)
|
||||||
{
|
{
|
||||||
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
// Rotate bucket if second changed
|
// Rotate bucket if second changed
|
||||||
@@ -133,11 +128,12 @@ public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
|||||||
{
|
{
|
||||||
long diff = now - _currentSecondTimestamp;
|
long diff = now - _currentSecondTimestamp;
|
||||||
// Move head forward, clearing buckets in between if gap > 1s
|
// Move head forward, clearing buckets in between if gap > 1s
|
||||||
for (int i = 0; i < diff && i < MaxHistorySeconds; i++)
|
for (var i = 0; i < diff && i < MaxHistorySeconds; i++)
|
||||||
{
|
{
|
||||||
_headIndex = (_headIndex + 1) % MaxHistorySeconds;
|
_headIndex = (_headIndex + 1) % MaxHistorySeconds;
|
||||||
_history[_headIndex].Reset();
|
_history[_headIndex].Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentSecondTimestamp = now;
|
_currentSecondTimestamp = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,18 +144,19 @@ public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
|||||||
private async Task PersistenceLoop()
|
private async Task PersistenceLoop()
|
||||||
{
|
{
|
||||||
while (!_cts.IsCancellationRequested)
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromMinutes(1), _cts.Token);
|
await Task.Delay(TimeSpan.FromMinutes(1), _cts.Token);
|
||||||
CalculateAndPersist();
|
CalculateAndPersist();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { break; }
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error persisting metrics");
|
_logger.LogError(ex, "Error persisting metrics");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CalculateAndPersist()
|
private void CalculateAndPersist()
|
||||||
@@ -167,117 +164,116 @@ public class NetworkTelemetryService : INetworkTelemetryService, IDisposable
|
|||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
// Calculate averages
|
// Calculate averages
|
||||||
var windows = new[] { 60, 300, 600, 1800 }; // 1m, 5m, 10m, 30m
|
var windows = new[] { 60, 300, 600, 1800 }; // 1m, 5m, 10m, 30m
|
||||||
|
|
||||||
using var fs = new FileStream(_persistencePath, FileMode.Create, FileAccess.Write);
|
using var fs = new FileStream(_persistencePath, FileMode.Create, FileAccess.Write);
|
||||||
using var bw = new BinaryWriter(fs);
|
using var bw = new BinaryWriter(fs);
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
bw.Write((byte)1); // Version
|
bw.Write((byte)1); // Version
|
||||||
bw.Write(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); // Timestamp
|
bw.Write(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); // Timestamp
|
||||||
|
|
||||||
foreach (var type in Enum.GetValues(typeof(MetricType)).Cast<MetricType>())
|
foreach (var type in Enum.GetValues(typeof(MetricType)).Cast<MetricType>())
|
||||||
{
|
{
|
||||||
bw.Write((int)type);
|
bw.Write((int)type);
|
||||||
foreach (var w in windows)
|
foreach (int w in windows)
|
||||||
{
|
{
|
||||||
double avg = CalculateAverage(type, w);
|
double avg = CalculateAverage(type, w);
|
||||||
bw.Write(w); // Window Seconds
|
bw.Write(w); // Window Seconds
|
||||||
bw.Write(avg); // Average Value
|
bw.Write(avg); // Average Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Forces immediate calculation and persistence of telemetry data.
|
/// Forces immediate calculation and persistence of telemetry data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void ForcePersist()
|
internal void ForcePersist()
|
||||||
{
|
{
|
||||||
CalculateAndPersist();
|
CalculateAndPersist();
|
||||||
}
|
}
|
||||||
|
|
||||||
private double CalculateAverage(MetricType type, int seconds)
|
private double CalculateAverage(MetricType type, int seconds)
|
||||||
{
|
{
|
||||||
// Go backwards from head
|
// Go backwards from head
|
||||||
double sum = 0;
|
double sum = 0;
|
||||||
int count = 0;
|
var count = 0;
|
||||||
int scanned = 0;
|
var scanned = 0;
|
||||||
|
|
||||||
int idx = _headIndex;
|
int idx = _headIndex;
|
||||||
|
|
||||||
while (scanned < seconds && scanned < MaxHistorySeconds)
|
while (scanned < seconds && scanned < MaxHistorySeconds)
|
||||||
{
|
{
|
||||||
var bucket = _history[idx];
|
var bucket = _history[idx];
|
||||||
sum += bucket.GetSum(type);
|
sum += bucket.GetSum(type);
|
||||||
count += bucket.GetCount(type);
|
count += bucket.GetCount(type);
|
||||||
|
|
||||||
idx--;
|
idx--;
|
||||||
if (idx < 0) idx = MaxHistorySeconds - 1;
|
if (idx < 0) idx = MaxHistorySeconds - 1;
|
||||||
scanned++;
|
scanned++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count == 0 ? 0 : sum / count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return count == 0 ? 0 : sum / count;
|
||||||
/// Releases resources used by the telemetry service.
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_cts.Cancel();
|
|
||||||
_cts.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MetricBucket
|
internal class MetricBucket
|
||||||
{
|
{
|
||||||
|
private readonly int[] _counts;
|
||||||
|
|
||||||
// Simple lock-free or locked accumulation? Global lock handles it for now.
|
// Simple lock-free or locked accumulation? Global lock handles it for now.
|
||||||
// Storing Sum and Count for each type
|
// Storing Sum and Count for each type
|
||||||
private readonly double[] _sums;
|
private readonly double[] _sums;
|
||||||
private readonly int[] _counts;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="MetricBucket"/> class.
|
/// Initializes a new instance of the <see cref="MetricBucket" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MetricBucket()
|
public MetricBucket()
|
||||||
{
|
{
|
||||||
var typeCount = Enum.GetValues(typeof(MetricType)).Length;
|
int typeCount = Enum.GetValues(typeof(MetricType)).Length;
|
||||||
_sums = new double[typeCount];
|
_sums = new double[typeCount];
|
||||||
_counts = new int[typeCount];
|
_counts = new int[typeCount];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all accumulated metric sums and counts in this bucket.
|
/// Clears all accumulated metric sums and counts in this bucket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Array.Clear(_sums, 0, _sums.Length);
|
Array.Clear(_sums, 0, _sums.Length);
|
||||||
Array.Clear(_counts, 0, _counts.Length);
|
Array.Clear(_counts, 0, _counts.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a metric value to the bucket.
|
/// Adds a metric value to the bucket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The metric category to update.</param>
|
/// <param name="type">The metric category to update.</param>
|
||||||
/// <param name="value">The value to accumulate.</param>
|
/// <param name="value">The value to accumulate.</param>
|
||||||
public void Add(MetricType type, double value)
|
public void Add(MetricType type, double value)
|
||||||
{
|
{
|
||||||
int idx = (int)type;
|
var idx = (int)type;
|
||||||
_sums[idx] += value;
|
_sums[idx] += value;
|
||||||
_counts[idx]++;
|
_counts[idx]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the accumulated sum for a metric type.
|
/// Gets the accumulated sum for a metric type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">The metric category to read.</param>
|
/// <param name="type">The metric category to read.</param>
|
||||||
/// <returns>The accumulated sum for the specified metric type.</returns>
|
/// <returns>The accumulated sum for the specified metric type.</returns>
|
||||||
public double GetSum(MetricType type) => _sums[(int)type];
|
public double GetSum(MetricType type)
|
||||||
/// <summary>
|
{
|
||||||
/// Gets the accumulated count for a metric type.
|
return _sums[(int)type];
|
||||||
/// </summary>
|
}
|
||||||
/// <param name="type">The metric category to read.</param>
|
|
||||||
/// <returns>The accumulated sample count for the specified metric type.</returns>
|
/// <summary>
|
||||||
public int GetCount(MetricType type) => _counts[(int)type];
|
/// Gets the accumulated count for a metric type.
|
||||||
}
|
/// </summary>
|
||||||
|
/// <param name="type">The metric category to read.</param>
|
||||||
|
/// <returns>The accumulated sample count for the specified metric type.</returns>
|
||||||
|
public int GetCount(MetricType type)
|
||||||
|
{
|
||||||
|
return _counts[(int)type];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +1,49 @@
|
|||||||
using System;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Concurrent;
|
using System.Net;
|
||||||
using System.Collections.Generic;
|
using System.Net.Sockets;
|
||||||
using System.Linq;
|
using System.Security.Cryptography;
|
||||||
using System.Net;
|
using System.Text;
|
||||||
using System.Net.Sockets;
|
using System.Text.Json;
|
||||||
using System.Text;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Network;
|
namespace ZB.MOM.WW.CBDDC.Network;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides UDP-based peer discovery for the CBDDC network.
|
/// Provides UDP-based peer discovery for the CBDDC network.
|
||||||
/// Broadcasts presence beacons and listens for other nodes on the local network.
|
/// Broadcasts presence beacons and listens for other nodes on the local network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class UdpDiscoveryService : IDiscoveryService
|
internal class UdpDiscoveryService : IDiscoveryService
|
||||||
{
|
{
|
||||||
private const int DiscoveryPort = 25000;
|
private const int DiscoveryPort = 25000;
|
||||||
private readonly ILogger<UdpDiscoveryService> _logger;
|
private readonly ConcurrentDictionary<string, PeerNode> _activePeers = new();
|
||||||
private readonly IPeerNodeConfigurationProvider _configProvider;
|
private readonly IPeerNodeConfigurationProvider _configProvider;
|
||||||
private readonly IDocumentStore _documentStore;
|
private readonly IDocumentStore _documentStore;
|
||||||
private CancellationTokenSource? _cts;
|
private readonly ILogger<UdpDiscoveryService> _logger;
|
||||||
private readonly ConcurrentDictionary<string, PeerNode> _activePeers = new();
|
private readonly object _startStopLock = new();
|
||||||
private readonly object _startStopLock = new object();
|
private CancellationTokenSource? _cts;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="UdpDiscoveryService"/> class.
|
/// Initializes a new instance of the <see cref="UdpDiscoveryService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="peerNodeConfigurationProvider">Provider for peer node configuration.</param>
|
/// <param name="peerNodeConfigurationProvider">Provider for peer node configuration.</param>
|
||||||
/// <param name="documentStore">Document store used to obtain collection interests.</param>
|
/// <param name="documentStore">Document store used to obtain collection interests.</param>
|
||||||
/// <param name="logger">Logger for discovery service events.</param>
|
/// <param name="logger">Logger for discovery service events.</param>
|
||||||
public UdpDiscoveryService(
|
public UdpDiscoveryService(
|
||||||
IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
IPeerNodeConfigurationProvider peerNodeConfigurationProvider,
|
||||||
IDocumentStore documentStore,
|
IDocumentStore documentStore,
|
||||||
ILogger<UdpDiscoveryService> logger)
|
ILogger<UdpDiscoveryService> logger)
|
||||||
{
|
{
|
||||||
_configProvider = peerNodeConfigurationProvider ?? throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
|
_configProvider = peerNodeConfigurationProvider ??
|
||||||
_documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
|
throw new ArgumentNullException(nameof(peerNodeConfigurationProvider));
|
||||||
_logger = logger;
|
_documentStore = documentStore ?? throw new ArgumentNullException(nameof(documentStore));
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the discovery service, initiating listener, broadcaster, and cleanup tasks.
|
/// Starts the discovery service, initiating listener, broadcaster, and cleanup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task Start()
|
public async Task Start()
|
||||||
{
|
{
|
||||||
@@ -57,11 +54,12 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
_logger.LogWarning("UDP Discovery Service already started");
|
_logger.LogWarning("UDP Discovery Service already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cts = new CancellationTokenSource();
|
_cts = new CancellationTokenSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = _cts.Token;
|
var token = _cts.Token;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -72,8 +70,8 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "UDP Listen task failed");
|
_logger.LogError(ex, "UDP Listen task failed");
|
||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -84,8 +82,8 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "UDP Broadcast task failed");
|
_logger.LogError(ex, "UDP Broadcast task failed");
|
||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -101,75 +99,26 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... Stop ...
|
/// <summary>
|
||||||
|
/// Stops the discovery service.
|
||||||
private async Task CleanupAsync(CancellationToken token)
|
/// </summary>
|
||||||
|
/// <returns>A task that completes when stop processing has finished.</returns>
|
||||||
|
public async Task Stop()
|
||||||
{
|
{
|
||||||
while (!token.IsCancellationRequested)
|
CancellationTokenSource? ctsToDispose = null;
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(10000, token); // Check every 10s
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
|
||||||
var expired = new List<string>();
|
|
||||||
|
|
||||||
foreach (var pair in _activePeers)
|
|
||||||
{
|
|
||||||
// Expiry: 15 seconds (broadcast is every 5s, so 3 missed beats = dead)
|
|
||||||
if ((now - pair.Value.LastSeen).TotalSeconds > 15)
|
|
||||||
{
|
|
||||||
expired.Add(pair.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in expired)
|
|
||||||
{
|
|
||||||
if (_activePeers.TryRemove(id, out var removed))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Peer Expired: {NodeId} at {Endpoint}", removed.NodeId, removed.Address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { break; }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Cleanup Loop Error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... Listen ...
|
|
||||||
|
|
||||||
private void HandleBeacon(DiscoveryBeacon beacon, IPAddress address)
|
|
||||||
{
|
|
||||||
var peerId = beacon.NodeId;
|
|
||||||
var endpoint = $"{address}:{beacon.TcpPort}";
|
|
||||||
|
|
||||||
var peer = new PeerNode(peerId, endpoint, DateTimeOffset.UtcNow, interestingCollections: beacon.InterestingCollections);
|
|
||||||
|
|
||||||
_activePeers.AddOrUpdate(peerId, peer, (key, old) => peer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops the discovery service.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A task that completes when stop processing has finished.</returns>
|
|
||||||
public async Task Stop()
|
|
||||||
{
|
|
||||||
CancellationTokenSource? ctsToDispose = null;
|
|
||||||
|
|
||||||
lock (_startStopLock)
|
lock (_startStopLock)
|
||||||
{
|
{
|
||||||
if (_cts == null)
|
if (_cts == null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("UDP Discovery Service already stopped or never started");
|
_logger.LogWarning("UDP Discovery Service already stopped or never started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctsToDispose = _cts;
|
ctsToDispose = _cts;
|
||||||
_cts = null;
|
_cts = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ctsToDispose.Cancel();
|
ctsToDispose.Cancel();
|
||||||
@@ -181,16 +130,62 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
ctsToDispose.Dispose();
|
ctsToDispose.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the currently active peers discovered on the network.
|
/// Gets the currently active peers discovered on the network.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The collection of active peers.</returns>
|
/// <returns>The collection of active peers.</returns>
|
||||||
public IEnumerable<PeerNode> GetActivePeers() => _activePeers.Values;
|
public IEnumerable<PeerNode> GetActivePeers()
|
||||||
|
{
|
||||||
|
return _activePeers.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Stop ...
|
||||||
|
|
||||||
|
private async Task CleanupAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(10000, token); // Check every 10s
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
var expired = new List<string>();
|
||||||
|
|
||||||
|
foreach (var pair in _activePeers)
|
||||||
|
// Expiry: 15 seconds (broadcast is every 5s, so 3 missed beats = dead)
|
||||||
|
if ((now - pair.Value.LastSeen).TotalSeconds > 15)
|
||||||
|
expired.Add(pair.Key);
|
||||||
|
|
||||||
|
foreach (string id in expired)
|
||||||
|
if (_activePeers.TryRemove(id, out var removed))
|
||||||
|
_logger.LogInformation("Peer Expired: {NodeId} at {Endpoint}", removed.NodeId, removed.Address);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Cleanup Loop Error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... Listen ...
|
||||||
|
|
||||||
|
private void HandleBeacon(DiscoveryBeacon beacon, IPAddress address)
|
||||||
|
{
|
||||||
|
string peerId = beacon.NodeId;
|
||||||
|
var endpoint = $"{address}:{beacon.TcpPort}";
|
||||||
|
|
||||||
|
var peer = new PeerNode(peerId, endpoint, DateTimeOffset.UtcNow,
|
||||||
|
interestingCollections: beacon.InterestingCollections);
|
||||||
|
|
||||||
|
_activePeers.AddOrUpdate(peerId, peer, (key, old) => peer);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ListenAsync(CancellationToken token)
|
private async Task ListenAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
@@ -201,28 +196,25 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
_logger.LogInformation("UDP Discovery Listening on port {Port}", DiscoveryPort);
|
_logger.LogInformation("UDP Discovery Listening on port {Port}", DiscoveryPort);
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await udp.ReceiveAsync();
|
var result = await udp.ReceiveAsync();
|
||||||
var json = Encoding.UTF8.GetString(result.Buffer);
|
string json = Encoding.UTF8.GetString(result.Buffer);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var config = await _configProvider.GetConfiguration();
|
var config = await _configProvider.GetConfiguration();
|
||||||
var _nodeId = config.NodeId;
|
string _nodeId = config.NodeId;
|
||||||
var localClusterHash = ComputeClusterHash(config.AuthToken);
|
string localClusterHash = ComputeClusterHash(config.AuthToken);
|
||||||
|
|
||||||
|
var beacon = JsonSerializer.Deserialize<DiscoveryBeacon>(json);
|
||||||
|
|
||||||
var beacon = JsonSerializer.Deserialize<DiscoveryBeacon>(json);
|
|
||||||
|
|
||||||
if (beacon != null && beacon.NodeId != _nodeId)
|
if (beacon != null && beacon.NodeId != _nodeId)
|
||||||
{
|
{
|
||||||
// Filter by ClusterHash to reduce congestion from different clusters
|
// Filter by ClusterHash to reduce congestion from different clusters
|
||||||
if (!string.Equals(beacon.ClusterHash, localClusterHash, StringComparison.Ordinal))
|
if (!string.Equals(beacon.ClusterHash, localClusterHash, StringComparison.Ordinal))
|
||||||
{
|
|
||||||
// Optional: Log trace if needed, but keeping it silent avoids flooding logs during congestion
|
// Optional: Log trace if needed, but keeping it silent avoids flooding logs during congestion
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
HandleBeacon(beacon, result.RemoteEndPoint.Address);
|
HandleBeacon(beacon, result.RemoteEndPoint.Address);
|
||||||
}
|
}
|
||||||
@@ -232,12 +224,14 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
_logger.LogWarning(ex, "Failed to parse beacon from {Address}", result.RemoteEndPoint.Address);
|
_logger.LogWarning(ex, "Failed to parse beacon from {Address}", result.RemoteEndPoint.Address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ObjectDisposedException) { break; }
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "UDP Listener Error");
|
_logger.LogError(ex, "UDP Listener Error");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BroadcastAsync(CancellationToken token)
|
private async Task BroadcastAsync(CancellationToken token)
|
||||||
@@ -252,18 +246,18 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Re-fetch config each time in case it changes (though usually static)
|
// Re-fetch config each time in case it changes (though usually static)
|
||||||
var conf = await _configProvider.GetConfiguration();
|
var conf = await _configProvider.GetConfiguration();
|
||||||
|
|
||||||
var beacon = new DiscoveryBeacon
|
var beacon = new DiscoveryBeacon
|
||||||
{
|
{
|
||||||
NodeId = conf.NodeId,
|
NodeId = conf.NodeId,
|
||||||
TcpPort = conf.TcpPort,
|
TcpPort = conf.TcpPort,
|
||||||
ClusterHash = ComputeClusterHash(conf.AuthToken),
|
ClusterHash = ComputeClusterHash(conf.AuthToken),
|
||||||
InterestingCollections = _documentStore.InterestedCollection.ToList()
|
InterestingCollections = _documentStore.InterestedCollection.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(beacon);
|
string json = JsonSerializer.Serialize(beacon);
|
||||||
var bytes = Encoding.UTF8.GetBytes(json);
|
byte[] bytes = Encoding.UTF8.GetBytes(json);
|
||||||
|
|
||||||
await udp.SendAsync(bytes, bytes.Length, endpoint);
|
await udp.SendAsync(bytes, bytes.Length, endpoint);
|
||||||
}
|
}
|
||||||
@@ -279,39 +273,38 @@ internal class UdpDiscoveryService : IDiscoveryService
|
|||||||
private string ComputeClusterHash(string authToken)
|
private string ComputeClusterHash(string authToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(authToken)) return "";
|
if (string.IsNullOrEmpty(authToken)) return "";
|
||||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
using var sha256 = SHA256.Create();
|
||||||
var bytes = Encoding.UTF8.GetBytes(authToken);
|
byte[] bytes = Encoding.UTF8.GetBytes(authToken);
|
||||||
var hash = sha256.ComputeHash(bytes);
|
byte[] hash = sha256.ComputeHash(bytes);
|
||||||
// Return first 8 chars (4 bytes hex) is enough for filtering
|
// Return first 8 chars (4 bytes hex) is enough for filtering
|
||||||
return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8);
|
return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class DiscoveryBeacon
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the broadcasting node identifier.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("node_id")]
|
||||||
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
private class DiscoveryBeacon
|
/// <summary>
|
||||||
{
|
/// Gets or sets the TCP port used by the broadcasting node.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Gets or sets the broadcasting node identifier.
|
[JsonPropertyName("tcp_port")]
|
||||||
/// </summary>
|
public int TcpPort { get; set; }
|
||||||
[System.Text.Json.Serialization.JsonPropertyName("node_id")]
|
|
||||||
public string NodeId { get; set; } = "";
|
/// <summary>
|
||||||
|
/// Gets or sets the cluster hash used for discovery filtering.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Gets or sets the TCP port used by the broadcasting node.
|
[JsonPropertyName("cluster_hash")]
|
||||||
/// </summary>
|
public string ClusterHash { get; set; } = "";
|
||||||
[System.Text.Json.Serialization.JsonPropertyName("tcp_port")]
|
|
||||||
public int TcpPort { get; set; }
|
/// <summary>
|
||||||
|
/// Gets or sets the collections the node is interested in.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Gets or sets the cluster hash used for discovery filtering.
|
[JsonPropertyName("interests")]
|
||||||
/// </summary>
|
public List<string> InterestingCollections { get; set; } = new();
|
||||||
[System.Text.Json.Serialization.JsonPropertyName("cluster_hash")]
|
}
|
||||||
public string ClusterHash { get; set; } = "";
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the collections the node is interested in.
|
|
||||||
/// </summary>
|
|
||||||
[System.Text.Json.Serialization.JsonPropertyName("interests")]
|
|
||||||
public List<string> InterestingCollections { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +1,52 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj" />
|
<ProjectReference Include="..\ZB.MOM.WW.CBDDC.Core\ZB.MOM.WW.CBDDC.Core.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.25.1" />
|
<PackageReference Include="Google.Protobuf" Version="3.25.1"/>
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
<PackageReference Include="Grpc.Tools" Version="2.76.0">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0"/>
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Protobuf Include="sync.proto" GrpcServices="None" />
|
<Protobuf Include="sync.proto" GrpcServices="None"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>ZB.MOM.WW.CBDDC.Network</AssemblyName>
|
<AssemblyName>ZB.MOM.WW.CBDDC.Network</AssemblyName>
|
||||||
<RootNamespace>ZB.MOM.WW.CBDDC.Network</RootNamespace>
|
<RootNamespace>ZB.MOM.WW.CBDDC.Network</RootNamespace>
|
||||||
<PackageId>ZB.MOM.WW.CBDDC.Network</PackageId>
|
<PackageId>ZB.MOM.WW.CBDDC.Network</PackageId>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>1.0.3</Version>
|
<Version>1.0.3</Version>
|
||||||
<Authors>MrDevRobot</Authors>
|
<Authors>MrDevRobot</Authors>
|
||||||
<Description>Networking layer (TCP/UDP/Gossip) for CBDDC.</Description>
|
<Description>Networking layer (TCP/UDP/Gossip) for CBDDC.</Description>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<PackageTags>p2p;mesh;network;gossip;lan;udp;tcp;discovery</PackageTags>
|
<PackageTags>p2p;mesh;network;gossip;lan;udp;tcp;discovery</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</PackageProjectUrl>
|
||||||
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
<RepositoryUrl>https://github.com/CBDDC/ZB.MOM.WW.CBDDC.Net</RepositoryUrl>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||||
<_Parameter1>ZB.MOM.WW.CBDDC.Network.Tests</_Parameter1>
|
<_Parameter1>ZB.MOM.WW.CBDDC.Network.Tests</_Parameter1>
|
||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite.Entities;
|
using ZB.MOM.WW.CBDDC.Persistence.BLite.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Persistence.BLite;
|
namespace ZB.MOM.WW.CBDDC.Persistence.BLite;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// BLite implementation of document metadata storage for sync tracking.
|
/// BLite implementation of document metadata storage for sync tracking.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TDbContext">The type of CBDDCDocumentDbContext.</typeparam>
|
/// <typeparam name="TDbContext">The type of CBDDCDocumentDbContext.</typeparam>
|
||||||
public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore where TDbContext : CBDDCDocumentDbContext
|
public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore where TDbContext : CBDDCDocumentDbContext
|
||||||
@@ -16,18 +16,20 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
private readonly ILogger<BLiteDocumentMetadataStore<TDbContext>> _logger;
|
private readonly ILogger<BLiteDocumentMetadataStore<TDbContext>> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BLiteDocumentMetadataStore{TDbContext}"/> class.
|
/// Initializes a new instance of the <see cref="BLiteDocumentMetadataStore{TDbContext}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The BLite document database context.</param>
|
/// <param name="context">The BLite document database context.</param>
|
||||||
/// <param name="logger">The optional logger instance.</param>
|
/// <param name="logger">The optional logger instance.</param>
|
||||||
public BLiteDocumentMetadataStore(TDbContext context, ILogger<BLiteDocumentMetadataStore<TDbContext>>? logger = null)
|
public BLiteDocumentMetadataStore(TDbContext context,
|
||||||
|
ILogger<BLiteDocumentMetadataStore<TDbContext>>? logger = null)
|
||||||
{
|
{
|
||||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
_logger = logger ?? NullLogger<BLiteDocumentMetadataStore<TDbContext>>.Instance;
|
_logger = logger ?? NullLogger<BLiteDocumentMetadataStore<TDbContext>>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<DocumentMetadata?> GetMetadataAsync(string collection, string key, CancellationToken cancellationToken = default)
|
public override async Task<DocumentMetadata?> GetMetadataAsync(string collection, string key,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var entity = _context.DocumentMetadatas
|
var entity = _context.DocumentMetadatas
|
||||||
.Find(m => m.Collection == collection && m.Key == key)
|
.Find(m => m.Collection == collection && m.Key == key)
|
||||||
@@ -37,7 +39,8 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection, CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return _context.DocumentMetadatas
|
return _context.DocumentMetadatas
|
||||||
.Find(m => m.Collection == collection)
|
.Find(m => m.Collection == collection)
|
||||||
@@ -46,7 +49,8 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task UpsertMetadataAsync(DocumentMetadata metadata, CancellationToken cancellationToken = default)
|
public override async Task UpsertMetadataAsync(DocumentMetadata metadata,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var existing = _context.DocumentMetadatas
|
var existing = _context.DocumentMetadatas
|
||||||
.Find(m => m.Collection == metadata.Collection && m.Key == metadata.Key)
|
.Find(m => m.Collection == metadata.Collection && m.Key == metadata.Key)
|
||||||
@@ -69,7 +73,8 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas, CancellationToken cancellationToken = default)
|
public override async Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var metadata in metadatas)
|
foreach (var metadata in metadatas)
|
||||||
{
|
{
|
||||||
@@ -95,7 +100,8 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp, CancellationToken cancellationToken = default)
|
public override async Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var existing = _context.DocumentMetadatas
|
var existing = _context.DocumentMetadatas
|
||||||
.Find(m => m.Collection == collection && m.Key == key)
|
.Find(m => m.Collection == collection && m.Key == key)
|
||||||
@@ -127,11 +133,12 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since, IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since,
|
||||||
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var query = _context.DocumentMetadatas.AsQueryable()
|
var query = _context.DocumentMetadatas.AsQueryable()
|
||||||
.Where(m => (m.HlcPhysicalTime > since.PhysicalTime) ||
|
.Where(m => m.HlcPhysicalTime > since.PhysicalTime ||
|
||||||
(m.HlcPhysicalTime == since.PhysicalTime && m.HlcLogicalCounter > since.LogicalCounter));
|
(m.HlcPhysicalTime == since.PhysicalTime && m.HlcLogicalCounter > since.LogicalCounter));
|
||||||
|
|
||||||
if (collections != null)
|
if (collections != null)
|
||||||
{
|
{
|
||||||
@@ -161,17 +168,16 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task ImportAsync(IEnumerable<DocumentMetadata> items, CancellationToken cancellationToken = default)
|
public override async Task ImportAsync(IEnumerable<DocumentMetadata> items,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var item in items)
|
foreach (var item in items) await _context.DocumentMetadatas.InsertAsync(ToEntity(item));
|
||||||
{
|
|
||||||
await _context.DocumentMetadatas.InsertAsync(ToEntity(item));
|
|
||||||
}
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task MergeAsync(IEnumerable<DocumentMetadata> items, CancellationToken cancellationToken = default)
|
public override async Task MergeAsync(IEnumerable<DocumentMetadata> items,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
@@ -186,7 +192,8 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update only if incoming is newer
|
// Update only if incoming is newer
|
||||||
var existingTs = new HlcTimestamp(existing.HlcPhysicalTime, existing.HlcLogicalCounter, existing.HlcNodeId);
|
var existingTs = new HlcTimestamp(existing.HlcPhysicalTime, existing.HlcLogicalCounter,
|
||||||
|
existing.HlcNodeId);
|
||||||
if (item.UpdatedAt.CompareTo(existingTs) > 0)
|
if (item.UpdatedAt.CompareTo(existingTs) > 0)
|
||||||
{
|
{
|
||||||
existing.HlcPhysicalTime = item.UpdatedAt.PhysicalTime;
|
existing.HlcPhysicalTime = item.UpdatedAt.PhysicalTime;
|
||||||
@@ -197,6 +204,7 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,4 +235,4 @@ public class BLiteDocumentMetadataStore<TDbContext> : DocumentMetadataStore wher
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`BLiteDocumentStore<TDbContext>` is an abstract base class that simplifies creating document stores for CBDDC with BLite persistence. It handles all Oplog management internally, so you only need to implement entity-to-JSON mapping methods.
|
`BLiteDocumentStore<TDbContext>` is an abstract base class that simplifies creating document stores for CBDDC with BLite
|
||||||
|
persistence. It handles all Oplog management internally, so you only need to implement entity-to-JSON mapping methods.
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
@@ -11,23 +12,24 @@
|
|||||||
- ? **No CDC Events Needed** - Direct Oplog management eliminates event loops
|
- ? **No CDC Events Needed** - Direct Oplog management eliminates event loops
|
||||||
- ? **Simple API** - Only 4 abstract methods to implement
|
- ? **Simple API** - Only 4 abstract methods to implement
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
User Code ? SampleDocumentStore (extends BLiteDocumentStore)
|
User Code ? SampleDocumentStore (extends BLiteDocumentStore)
|
||||||
?
|
?
|
||||||
BLiteDocumentStore
|
BLiteDocumentStore
|
||||||
??? _context.Users / TodoLists (read/write entities)
|
??? _context.Users / TodoLists (read/write entities)
|
||||||
??? _context.OplogEntries (write oplog directly)
|
??? _context.OplogEntries (write oplog directly)
|
||||||
|
|
||||||
Remote Sync ? OplogStore.ApplyBatchAsync()
|
Remote Sync ? OplogStore.ApplyBatchAsync()
|
||||||
?
|
?
|
||||||
BLiteDocumentStore.PutDocumentAsync(fromSync=true)
|
BLiteDocumentStore.PutDocumentAsync(fromSync=true)
|
||||||
??? _context.Users / TodoLists (write only)
|
??? _context.Users / TodoLists (write only)
|
||||||
??? _context.OplogEntries (skip - already exists)
|
??? _context.OplogEntries (skip - already exists)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key Advantage**: No circular dependency! `BLiteDocumentStore` writes directly to `CBDDCDocumentDbContext.OplogEntries` collection.
|
**Key Advantage**: No circular dependency! `BLiteDocumentStore` writes directly to `CBDDCDocumentDbContext.OplogEntries`
|
||||||
|
collection.
|
||||||
|
|
||||||
## Implementation Example
|
## Implementation Example
|
||||||
|
|
||||||
@@ -129,15 +131,15 @@ public class SampleDocumentStore : BLiteDocumentStore<SampleDbContext>
|
|||||||
|
|
||||||
## Usage in Application
|
## Usage in Application
|
||||||
|
|
||||||
### Setup (DI Container)
|
### Setup (DI Container)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
services.AddSingleton<SampleDbContext>(sp =>
|
services.AddSingleton<SampleDbContext>(sp =>
|
||||||
new SampleDbContext("data/sample.blite"));
|
new SampleDbContext("data/sample.blite"));
|
||||||
|
|
||||||
// No OplogStore dependency needed!
|
// No OplogStore dependency needed!
|
||||||
services.AddSingleton<IDocumentStore, SampleDocumentStore>();
|
services.AddSingleton<IDocumentStore, SampleDocumentStore>();
|
||||||
services.AddSingleton<IOplogStore, BLiteOplogStore<SampleDbContext>>();
|
services.AddSingleton<IOplogStore, BLiteOplogStore<SampleDbContext>>();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Local Changes (User operations)
|
### Local Changes (User operations)
|
||||||
@@ -180,6 +182,7 @@ using (documentStore.BeginRemoteSync()) // ? Suppresses Oplog creation
|
|||||||
## Migration from Old CDC-based Approach
|
## Migration from Old CDC-based Approach
|
||||||
|
|
||||||
### Before (with CDC Events)
|
### Before (with CDC Events)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// SampleDocumentStore subscribes to BLite CDC
|
// SampleDocumentStore subscribes to BLite CDC
|
||||||
// CDC emits events ? OplogCoordinator creates Oplog
|
// CDC emits events ? OplogCoordinator creates Oplog
|
||||||
@@ -187,6 +190,7 @@ using (documentStore.BeginRemoteSync()) // ? Suppresses Oplog creation
|
|||||||
```
|
```
|
||||||
|
|
||||||
### After (with BLiteDocumentStore)
|
### After (with BLiteDocumentStore)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// Direct Oplog management in DocumentStore
|
// Direct Oplog management in DocumentStore
|
||||||
// AsyncLocal flag prevents duplicates during sync
|
// AsyncLocal flag prevents duplicates during sync
|
||||||
@@ -203,6 +207,7 @@ using (documentStore.BeginRemoteSync()) // ? Suppresses Oplog creation
|
|||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
After implementing your DocumentStore:
|
After implementing your DocumentStore:
|
||||||
|
|
||||||
1. Remove CDC subscriptions from your code
|
1. Remove CDC subscriptions from your code
|
||||||
2. Remove `OplogCoordinator` from DI (no longer needed)
|
2. Remove `OplogCoordinator` from DI (no longer needed)
|
||||||
3. Test local operations create Oplog entries
|
3. Test local operations create Oplog entries
|
||||||
|
|||||||
@@ -1,56 +1,50 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using BLite.Core.CDC;
|
using BLite.Core.CDC;
|
||||||
using BLite.Core.Collections;
|
using BLite.Core.Collections;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ZB.MOM.WW.CBDDC.Core;
|
using ZB.MOM.WW.CBDDC.Core;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Network;
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Storage;
|
using ZB.MOM.WW.CBDDC.Core.Storage;
|
||||||
using ZB.MOM.WW.CBDDC.Core.Sync;
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
using ZB.MOM.WW.CBDDC.Persistence.BLite.Entities;
|
using ZB.MOM.WW.CBDDC.Persistence.BLite.Entities;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
using BLiteOperationType = BLite.Core.Transactions.OperationType;
|
using BLiteOperationType = BLite.Core.Transactions.OperationType;
|
||||||
|
|
||||||
namespace ZB.MOM.WW.CBDDC.Persistence.BLite;
|
namespace ZB.MOM.WW.CBDDC.Persistence.BLite;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abstract base class for BLite-based document stores.
|
/// Abstract base class for BLite-based document stores.
|
||||||
/// Handles Oplog creation internally - subclasses only implement entity mapping.
|
/// Handles Oplog creation internally - subclasses only implement entity mapping.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TDbContext">The BLite DbContext type.</typeparam>
|
/// <typeparam name="TDbContext">The BLite DbContext type.</typeparam>
|
||||||
public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposable
|
public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposable
|
||||||
where TDbContext : CBDDCDocumentDbContext
|
where TDbContext : CBDDCDocumentDbContext
|
||||||
{
|
{
|
||||||
protected readonly TDbContext _context;
|
private readonly List<IDisposable> _cdcWatchers = new();
|
||||||
|
private readonly object _clockLock = new();
|
||||||
protected readonly IPeerNodeConfigurationProvider _configProvider;
|
protected readonly IPeerNodeConfigurationProvider _configProvider;
|
||||||
protected readonly IConflictResolver _conflictResolver;
|
protected readonly IConflictResolver _conflictResolver;
|
||||||
protected readonly IVectorClockService _vectorClock;
|
protected readonly TDbContext _context;
|
||||||
protected readonly ILogger<BLiteDocumentStore<TDbContext>> _logger;
|
protected readonly ILogger<BLiteDocumentStore<TDbContext>> _logger;
|
||||||
|
private readonly HashSet<string> _registeredCollections = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Semaphore used to suppress CDC-triggered OplogEntry creation during remote sync.
|
/// Semaphore used to suppress CDC-triggered OplogEntry creation during remote sync.
|
||||||
/// CurrentCount == 0 ? sync in progress, CDC must skip.
|
/// CurrentCount == 0 ? sync in progress, CDC must skip.
|
||||||
/// CurrentCount == 1 ? no sync, CDC creates OplogEntry.
|
/// CurrentCount == 1 ? no sync, CDC creates OplogEntry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly SemaphoreSlim _remoteSyncGuard = new SemaphoreSlim(1, 1);
|
private readonly SemaphoreSlim _remoteSyncGuard = new(1, 1);
|
||||||
private readonly ConcurrentDictionary<string, int> _suppressedCdcEvents = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
private readonly List<IDisposable> _cdcWatchers = new();
|
private readonly ConcurrentDictionary<string, int> _suppressedCdcEvents = new(StringComparer.Ordinal);
|
||||||
private readonly HashSet<string> _registeredCollections = new();
|
protected readonly IVectorClockService _vectorClock;
|
||||||
|
|
||||||
// HLC state for generating timestamps for local changes
|
// HLC state for generating timestamps for local changes
|
||||||
private long _lastPhysicalTime;
|
private long _lastPhysicalTime;
|
||||||
private int _logicalCounter;
|
private int _logicalCounter;
|
||||||
private readonly object _clockLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BLiteDocumentStore{TDbContext}"/> class.
|
/// Initializes a new instance of the <see cref="BLiteDocumentStore{TDbContext}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">The BLite database context.</param>
|
/// <param name="context">The BLite database context.</param>
|
||||||
/// <param name="configProvider">The peer node configuration provider.</param>
|
/// <param name="configProvider">The peer node configuration provider.</param>
|
||||||
@@ -74,17 +68,29 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
_logicalCounter = 0;
|
_logicalCounter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases managed resources used by this document store.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var watcher in _cdcWatchers)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
watcher.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_cdcWatchers.Clear();
|
||||||
|
_remoteSyncGuard.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
private static ILogger<BLiteDocumentStore<TDbContext>> CreateTypedLogger(ILogger? logger)
|
private static ILogger<BLiteDocumentStore<TDbContext>> CreateTypedLogger(ILogger? logger)
|
||||||
{
|
{
|
||||||
if (logger is null)
|
if (logger is null) return NullLogger<BLiteDocumentStore<TDbContext>>.Instance;
|
||||||
{
|
|
||||||
return NullLogger<BLiteDocumentStore<TDbContext>>.Instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger is ILogger<BLiteDocumentStore<TDbContext>> typedLogger)
|
if (logger is ILogger<BLiteDocumentStore<TDbContext>> typedLogger) return typedLogger;
|
||||||
{
|
|
||||||
return typedLogger;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ForwardingLogger(logger);
|
return new ForwardingLogger(logger);
|
||||||
}
|
}
|
||||||
@@ -94,7 +100,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
private readonly ILogger _inner;
|
private readonly ILogger _inner;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ForwardingLogger"/> class.
|
/// Initializes a new instance of the <see cref="ForwardingLogger" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="inner">The underlying logger instance.</param>
|
/// <param name="inner">The underlying logger instance.</param>
|
||||||
public ForwardingLogger(ILogger inner)
|
public ForwardingLogger(ILogger inner)
|
||||||
@@ -135,35 +141,26 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
|
|
||||||
private void RegisterSuppressedCdcEvent(string collection, string key, OperationType operationType)
|
private void RegisterSuppressedCdcEvent(string collection, string key, OperationType operationType)
|
||||||
{
|
{
|
||||||
var suppressionKey = BuildSuppressionKey(collection, key, operationType);
|
string suppressionKey = BuildSuppressionKey(collection, key, operationType);
|
||||||
_suppressedCdcEvents.AddOrUpdate(suppressionKey, 1, (_, current) => current + 1);
|
_suppressedCdcEvents.AddOrUpdate(suppressionKey, 1, (_, current) => current + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryConsumeSuppressedCdcEvent(string collection, string key, OperationType operationType)
|
private bool TryConsumeSuppressedCdcEvent(string collection, string key, OperationType operationType)
|
||||||
{
|
{
|
||||||
var suppressionKey = BuildSuppressionKey(collection, key, operationType);
|
string suppressionKey = BuildSuppressionKey(collection, key, operationType);
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (!_suppressedCdcEvents.TryGetValue(suppressionKey, out var current))
|
if (!_suppressedCdcEvents.TryGetValue(suppressionKey, out int current)) return false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current <= 1)
|
if (current <= 1) return _suppressedCdcEvents.TryRemove(suppressionKey, out _);
|
||||||
{
|
|
||||||
return _suppressedCdcEvents.TryRemove(suppressionKey, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_suppressedCdcEvents.TryUpdate(suppressionKey, current - 1, current))
|
if (_suppressedCdcEvents.TryUpdate(suppressionKey, current - 1, current)) return true;
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers a BLite collection for CDC tracking.
|
/// Registers a BLite collection for CDC tracking.
|
||||||
/// Call in subclass constructor for each collection to sync.
|
/// Call in subclass constructor for each collection to sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TEntity">The entity type.</typeparam>
|
/// <typeparam name="TEntity">The entity type.</typeparam>
|
||||||
/// <param name="collectionName">The logical collection name used in Oplog.</param>
|
/// <param name="collectionName">The logical collection name used in Oplog.</param>
|
||||||
@@ -177,14 +174,14 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_registeredCollections.Add(collectionName);
|
_registeredCollections.Add(collectionName);
|
||||||
|
|
||||||
var watcher = collection.Watch(capturePayload: true)
|
var watcher = collection.Watch(true)
|
||||||
.Subscribe(new CdcObserver<TEntity>(collectionName, keySelector, this));
|
.Subscribe(new CdcObserver<TEntity>(collectionName, keySelector, this));
|
||||||
_cdcWatchers.Add(watcher);
|
_cdcWatchers.Add(watcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generic CDC observer. Forwards BLite change events to OnLocalChangeDetectedAsync.
|
/// Generic CDC observer. Forwards BLite change events to OnLocalChangeDetectedAsync.
|
||||||
/// Automatically skips events when remote sync is in progress.
|
/// Automatically skips events when remote sync is in progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private class CdcObserver<TEntity> : IObserver<ChangeStreamEvent<string, TEntity>>
|
private class CdcObserver<TEntity> : IObserver<ChangeStreamEvent<string, TEntity>>
|
||||||
where TEntity : class
|
where TEntity : class
|
||||||
@@ -194,7 +191,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
private readonly BLiteDocumentStore<TDbContext> _store;
|
private readonly BLiteDocumentStore<TDbContext> _store;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CdcObserver{TEntity}"/> class.
|
/// Initializes a new instance of the <see cref="CdcObserver{TEntity}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collectionName">The logical collection name.</param>
|
/// <param name="collectionName">The logical collection name.</param>
|
||||||
/// <param name="keySelector">The key selector for observed entities.</param>
|
/// <param name="keySelector">The key selector for observed entities.</param>
|
||||||
@@ -210,23 +207,20 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a change stream event from BLite CDC.
|
/// Handles a change stream event from BLite CDC.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="changeEvent">The change event payload.</param>
|
/// <param name="changeEvent">The change event payload.</param>
|
||||||
public void OnNext(ChangeStreamEvent<string, TEntity> changeEvent)
|
public void OnNext(ChangeStreamEvent<string, TEntity> changeEvent)
|
||||||
{
|
{
|
||||||
var operationType = changeEvent.Type == BLiteOperationType.Delete ? OperationType.Delete : OperationType.Put;
|
var operationType = changeEvent.Type == BLiteOperationType.Delete
|
||||||
|
? OperationType.Delete
|
||||||
|
: OperationType.Put;
|
||||||
|
|
||||||
var entityId = changeEvent.DocumentId?.ToString() ?? "";
|
string entityId = changeEvent.DocumentId ?? "";
|
||||||
if (operationType == OperationType.Put && changeEvent.Entity != null)
|
if (operationType == OperationType.Put && changeEvent.Entity != null)
|
||||||
{
|
|
||||||
entityId = _keySelector(changeEvent.Entity);
|
entityId = _keySelector(changeEvent.Entity);
|
||||||
}
|
|
||||||
|
|
||||||
if (_store.TryConsumeSuppressedCdcEvent(_collectionName, entityId, operationType))
|
if (_store.TryConsumeSuppressedCdcEvent(_collectionName, entityId, operationType)) return;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_store._remoteSyncGuard.CurrentCount == 0) return;
|
if (_store._remoteSyncGuard.CurrentCount == 0) return;
|
||||||
|
|
||||||
@@ -238,22 +232,26 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
else if (changeEvent.Entity != null)
|
else if (changeEvent.Entity != null)
|
||||||
{
|
{
|
||||||
var content = JsonSerializer.SerializeToElement(changeEvent.Entity);
|
var content = JsonSerializer.SerializeToElement(changeEvent.Entity);
|
||||||
var key = _keySelector(changeEvent.Entity);
|
string key = _keySelector(changeEvent.Entity);
|
||||||
_store.OnLocalChangeDetectedAsync(_collectionName, key, OperationType.Put, content)
|
_store.OnLocalChangeDetectedAsync(_collectionName, key, OperationType.Put, content)
|
||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles CDC observer errors.
|
/// Handles CDC observer errors.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The observed exception.</param>
|
/// <param name="error">The observed exception.</param>
|
||||||
public void OnError(Exception error) { }
|
public void OnError(Exception error)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles completion of the CDC stream.
|
/// Handles completion of the CDC stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void OnCompleted() { }
|
public void OnCompleted()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -261,8 +259,8 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
#region Abstract Methods - Implemented by subclass
|
#region Abstract Methods - Implemented by subclass
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies JSON content to a single entity (insert or update) and commits changes.
|
/// Applies JSON content to a single entity (insert or update) and commits changes.
|
||||||
/// Called for single-document operations.
|
/// Called for single-document operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
@@ -272,16 +270,17 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
string collection, string key, JsonElement content, CancellationToken cancellationToken);
|
string collection, string key, JsonElement content, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies JSON content to multiple entities (insert or update) with a single commit.
|
/// Applies JSON content to multiple entities (insert or update) with a single commit.
|
||||||
/// Called for batch operations. Must commit all changes in a single SaveChanges.
|
/// Called for batch operations. Must commit all changes in a single SaveChanges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The documents to apply in one batch.</param>
|
/// <param name="documents">The documents to apply in one batch.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task ApplyContentToEntitiesBatchAsync(
|
protected abstract Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents, CancellationToken cancellationToken);
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads an entity from the DbContext and returns it as JsonElement.
|
/// Reads an entity from the DbContext and returns it as JsonElement.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
@@ -290,7 +289,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
string collection, string key, CancellationToken cancellationToken);
|
string collection, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a single entity from the DbContext and commits changes.
|
/// Removes a single entity from the DbContext and commits changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
@@ -299,7 +298,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
string collection, string key, CancellationToken cancellationToken);
|
string collection, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes multiple entities from the DbContext with a single commit.
|
/// Removes multiple entities from the DbContext with a single commit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The documents to remove in one batch.</param>
|
/// <param name="documents">The documents to remove in one batch.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -307,45 +306,47 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken);
|
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads all entities from a collection as JsonElements.
|
/// Reads all entities from a collection as JsonElements.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
protected abstract Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
||||||
string collection, CancellationToken cancellationToken);
|
string collection, CancellationToken cancellationToken);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region IDocumentStore Implementation
|
#region IDocumentStore Implementation
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the collections registered via WatchCollection.
|
/// Returns the collections registered via WatchCollection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<string> InterestedCollection => _registeredCollections;
|
public IEnumerable<string> InterestedCollection => _registeredCollections;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a document by collection and key.
|
/// Gets a document by collection and key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The matching document, or <see langword="null"/> when not found.</returns>
|
/// <returns>The matching document, or <see langword="null" /> when not found.</returns>
|
||||||
public async Task<Document?> GetDocumentAsync(string collection, string key, CancellationToken cancellationToken = default)
|
public async Task<Document?> GetDocumentAsync(string collection, string key,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var content = await GetEntityAsJsonAsync(collection, key, cancellationToken);
|
var content = await GetEntityAsJsonAsync(collection, key, cancellationToken);
|
||||||
if (content == null) return null;
|
if (content == null) return null;
|
||||||
|
|
||||||
var timestamp = new HlcTimestamp(0, 0, ""); // Will be populated from metadata if needed
|
var timestamp = new HlcTimestamp(0, 0, ""); // Will be populated from metadata if needed
|
||||||
return new Document(collection, key, content.Value, timestamp, false);
|
return new Document(collection, key, content.Value, timestamp, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all documents for a collection.
|
/// Gets all documents for a collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The documents in the specified collection.</returns>
|
/// <returns>The documents in the specified collection.</returns>
|
||||||
public async Task<IEnumerable<Document>> GetDocumentsByCollectionAsync(string collection, CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<Document>> GetDocumentsByCollectionAsync(string collection,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var entities = await GetAllEntitiesAsJsonAsync(collection, cancellationToken);
|
var entities = await GetAllEntitiesAsJsonAsync(collection, cancellationToken);
|
||||||
var timestamp = new HlcTimestamp(0, 0, "");
|
var timestamp = new HlcTimestamp(0, 0, "");
|
||||||
@@ -353,31 +354,30 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets documents for the specified collection and key pairs.
|
/// Gets documents for the specified collection and key pairs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documentKeys">The collection and key pairs to resolve.</param>
|
/// <param name="documentKeys">The collection and key pairs to resolve.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The documents that were found.</returns>
|
/// <returns>The documents that were found.</returns>
|
||||||
public async Task<IEnumerable<Document>> GetDocumentsAsync(List<(string Collection, string Key)> documentKeys, CancellationToken cancellationToken)
|
public async Task<IEnumerable<Document>> GetDocumentsAsync(List<(string Collection, string Key)> documentKeys,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var documents = new List<Document>();
|
var documents = new List<Document>();
|
||||||
foreach (var (collection, key) in documentKeys)
|
foreach ((string collection, string key) in documentKeys)
|
||||||
{
|
{
|
||||||
var doc = await GetDocumentAsync(collection, key, cancellationToken);
|
var doc = await GetDocumentAsync(collection, key, cancellationToken);
|
||||||
if (doc != null)
|
if (doc != null) documents.Add(doc);
|
||||||
{
|
|
||||||
documents.Add(doc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return documents;
|
return documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inserts or updates a single document.
|
/// Inserts or updates a single document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="document">The document to persist.</param>
|
/// <param name="document">The document to persist.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> when the operation succeeds.</returns>
|
/// <returns><see langword="true" /> when the operation succeeds.</returns>
|
||||||
public async Task<bool> PutDocumentAsync(Document document, CancellationToken cancellationToken = default)
|
public async Task<bool> PutDocumentAsync(Document document, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
@@ -389,6 +389,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_remoteSyncGuard.Release();
|
_remoteSyncGuard.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,21 +400,20 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a batch of documents.
|
/// Updates a batch of documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The documents to update.</param>
|
/// <param name="documents">The documents to update.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> when the operation succeeds.</returns>
|
/// <returns><see langword="true" /> when the operation succeeds.</returns>
|
||||||
public async Task<bool> UpdateBatchDocumentsAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default)
|
public async Task<bool> UpdateBatchDocumentsAsync(IEnumerable<Document> documents,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var documentList = documents.ToList();
|
var documentList = documents.ToList();
|
||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var document in documentList)
|
foreach (var document in documentList)
|
||||||
{
|
|
||||||
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
||||||
}
|
|
||||||
|
|
||||||
await ApplyContentToEntitiesBatchAsync(
|
await ApplyContentToEntitiesBatchAsync(
|
||||||
documentList.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
documentList.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
||||||
@@ -422,25 +422,25 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_remoteSyncGuard.Release();
|
_remoteSyncGuard.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inserts a batch of documents.
|
/// Inserts a batch of documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documents">The documents to insert.</param>
|
/// <param name="documents">The documents to insert.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> when the operation succeeds.</returns>
|
/// <returns><see langword="true" /> when the operation succeeds.</returns>
|
||||||
public async Task<bool> InsertBatchDocumentsAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default)
|
public async Task<bool> InsertBatchDocumentsAsync(IEnumerable<Document> documents,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var documentList = documents.ToList();
|
var documentList = documents.ToList();
|
||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var document in documentList)
|
foreach (var document in documentList)
|
||||||
{
|
|
||||||
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
||||||
}
|
|
||||||
|
|
||||||
await ApplyContentToEntitiesBatchAsync(
|
await ApplyContentToEntitiesBatchAsync(
|
||||||
documentList.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
documentList.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
||||||
@@ -449,17 +449,19 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_remoteSyncGuard.Release();
|
_remoteSyncGuard.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a single document.
|
/// Deletes a single document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> when the operation succeeds.</returns>
|
/// <returns><see langword="true" /> when the operation succeeds.</returns>
|
||||||
public async Task<bool> DeleteDocumentAsync(string collection, string key, CancellationToken cancellationToken = default)
|
public async Task<bool> DeleteDocumentAsync(string collection, string key,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
@@ -470,6 +472,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_remoteSyncGuard.Release();
|
_remoteSyncGuard.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,25 +483,22 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deletes a batch of documents by composite keys.
|
/// Deletes a batch of documents by composite keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="documentKeys">The document keys in collection/key format.</param>
|
/// <param name="documentKeys">The document keys in collection/key format.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns><see langword="true"/> when the operation succeeds.</returns>
|
/// <returns><see langword="true" /> when the operation succeeds.</returns>
|
||||||
public async Task<bool> DeleteBatchDocumentsAsync(IEnumerable<string> documentKeys, CancellationToken cancellationToken = default)
|
public async Task<bool> DeleteBatchDocumentsAsync(IEnumerable<string> documentKeys,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var parsedKeys = new List<(string Collection, string Key)>();
|
var parsedKeys = new List<(string Collection, string Key)>();
|
||||||
foreach (var key in documentKeys)
|
foreach (string key in documentKeys)
|
||||||
{
|
{
|
||||||
var parts = key.Split('/');
|
string[] parts = key.Split('/');
|
||||||
if (parts.Length == 2)
|
if (parts.Length == 2)
|
||||||
{
|
|
||||||
parsedKeys.Add((parts[0], parts[1]));
|
parsedKeys.Add((parts[0], parts[1]));
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
_logger.LogWarning("Invalid document key format: {Key}", key);
|
_logger.LogWarning("Invalid document key format: {Key}", key);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedKeys.Count == 0) return true;
|
if (parsedKeys.Count == 0) return true;
|
||||||
@@ -506,10 +506,8 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var (collection, key) in parsedKeys)
|
foreach ((string collection, string key) in parsedKeys)
|
||||||
{
|
|
||||||
RegisterSuppressedCdcEvent(collection, key, OperationType.Delete);
|
RegisterSuppressedCdcEvent(collection, key, OperationType.Delete);
|
||||||
}
|
|
||||||
|
|
||||||
await RemoveEntitiesBatchAsync(parsedKeys, cancellationToken);
|
await RemoveEntitiesBatchAsync(parsedKeys, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -517,11 +515,12 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
_remoteSyncGuard.Release();
|
_remoteSyncGuard.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges an incoming document with the current stored document.
|
/// Merges an incoming document with the current stored document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="incoming">The incoming document.</param>
|
/// <param name="incoming">The incoming document.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -553,46 +552,44 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ISnapshotable Implementation
|
#region ISnapshotable Implementation
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes all tracked documents from registered collections.
|
/// Removes all tracked documents from registered collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
public async Task DropAsync(CancellationToken cancellationToken = default)
|
public async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var collection in InterestedCollection)
|
foreach (string collection in InterestedCollection)
|
||||||
{
|
{
|
||||||
var entities = await GetAllEntitiesAsJsonAsync(collection, cancellationToken);
|
var entities = await GetAllEntitiesAsJsonAsync(collection, cancellationToken);
|
||||||
foreach (var (key, _) in entities)
|
foreach ((string key, var _) in entities) await RemoveEntityAsync(collection, key, cancellationToken);
|
||||||
{
|
|
||||||
await RemoveEntityAsync(collection, key, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports all tracked documents from registered collections.
|
/// Exports all tracked documents from registered collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The exported documents.</returns>
|
/// <returns>The exported documents.</returns>
|
||||||
public async Task<IEnumerable<Document>> ExportAsync(CancellationToken cancellationToken = default)
|
public async Task<IEnumerable<Document>> ExportAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var documents = new List<Document>();
|
var documents = new List<Document>();
|
||||||
foreach (var collection in InterestedCollection)
|
foreach (string collection in InterestedCollection)
|
||||||
{
|
{
|
||||||
var collectionDocs = await GetDocumentsByCollectionAsync(collection, cancellationToken);
|
var collectionDocs = await GetDocumentsByCollectionAsync(collection, cancellationToken);
|
||||||
documents.AddRange(collectionDocs);
|
documents.AddRange(collectionDocs);
|
||||||
}
|
}
|
||||||
|
|
||||||
return documents;
|
return documents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Imports a batch of documents.
|
/// Imports a batch of documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">The documents to import.</param>
|
/// <param name="items">The documents to import.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -603,9 +600,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var document in documents)
|
foreach (var document in documents)
|
||||||
{
|
|
||||||
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
RegisterSuppressedCdcEvent(document.Collection, document.Key, OperationType.Put);
|
||||||
}
|
|
||||||
|
|
||||||
await ApplyContentToEntitiesBatchAsync(
|
await ApplyContentToEntitiesBatchAsync(
|
||||||
documents.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
documents.Select(d => (d.Collection, d.Key, d.Content)), cancellationToken);
|
||||||
@@ -617,7 +612,7 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges a batch of incoming documents.
|
/// Merges a batch of incoming documents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="items">The incoming documents.</param>
|
/// <param name="items">The incoming documents.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
@@ -625,32 +620,29 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
{
|
{
|
||||||
// Acquire guard to prevent Oplog creation during merge
|
// Acquire guard to prevent Oplog creation during merge
|
||||||
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
await _remoteSyncGuard.WaitAsync(cancellationToken);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var document in items)
|
foreach (var document in items) await MergeAsync(document, cancellationToken);
|
||||||
{
|
}
|
||||||
await MergeAsync(document, cancellationToken);
|
finally
|
||||||
}
|
{
|
||||||
}
|
_remoteSyncGuard.Release();
|
||||||
finally
|
}
|
||||||
{
|
}
|
||||||
_remoteSyncGuard.Release();
|
|
||||||
}
|
#endregion
|
||||||
}
|
|
||||||
|
#region Oplog Management
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Oplog Management
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns true if a remote sync operation is in progress (guard acquired).
|
|
||||||
/// CDC listeners should check this before creating OplogEntry.
|
|
||||||
/// </summary>
|
|
||||||
protected bool IsRemoteSyncInProgress => _remoteSyncGuard.CurrentCount == 0;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called by subclass CDC listeners when a local change is detected.
|
/// Returns true if a remote sync operation is in progress (guard acquired).
|
||||||
/// Creates OplogEntry + DocumentMetadata only if no remote sync is in progress.
|
/// CDC listeners should check this before creating OplogEntry.
|
||||||
|
/// </summary>
|
||||||
|
protected bool IsRemoteSyncInProgress => _remoteSyncGuard.CurrentCount == 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called by subclass CDC listeners when a local change is detected.
|
||||||
|
/// Creates OplogEntry + DocumentMetadata only if no remote sync is in progress.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collection">The logical collection name.</param>
|
/// <param name="collection">The logical collection name.</param>
|
||||||
/// <param name="key">The document key.</param>
|
/// <param name="key">The document key.</param>
|
||||||
@@ -661,90 +653,90 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
OperationType operationType,
|
OperationType operationType,
|
||||||
JsonElement? content,
|
JsonElement? content,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (IsRemoteSyncInProgress) return;
|
if (IsRemoteSyncInProgress) return;
|
||||||
|
|
||||||
await CreateOplogEntryAsync(collection, key, operationType, content, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private HlcTimestamp GenerateTimestamp(string nodeId)
|
|
||||||
{
|
|
||||||
lock (_clockLock)
|
|
||||||
{
|
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
||||||
|
|
||||||
if (now > _lastPhysicalTime)
|
await CreateOplogEntryAsync(collection, key, operationType, content, cancellationToken);
|
||||||
{
|
}
|
||||||
_lastPhysicalTime = now;
|
|
||||||
_logicalCounter = 0;
|
private HlcTimestamp GenerateTimestamp(string nodeId)
|
||||||
}
|
{
|
||||||
else
|
lock (_clockLock)
|
||||||
{
|
{
|
||||||
_logicalCounter++;
|
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||||
|
|
||||||
|
if (now > _lastPhysicalTime)
|
||||||
|
{
|
||||||
|
_lastPhysicalTime = now;
|
||||||
|
_logicalCounter = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logicalCounter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new HlcTimestamp(_lastPhysicalTime, _logicalCounter, nodeId);
|
return new HlcTimestamp(_lastPhysicalTime, _logicalCounter, nodeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CreateOplogEntryAsync(
|
private async Task CreateOplogEntryAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
OperationType operationType,
|
OperationType operationType,
|
||||||
JsonElement? content,
|
JsonElement? content,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var config = await _configProvider.GetConfiguration();
|
var config = await _configProvider.GetConfiguration();
|
||||||
var nodeId = config.NodeId;
|
string nodeId = config.NodeId;
|
||||||
|
|
||||||
// Get last hash from OplogEntries collection directly
|
// Get last hash from OplogEntries collection directly
|
||||||
var lastEntry = _context.OplogEntries
|
var lastEntry = _context.OplogEntries
|
||||||
.Find(e => e.TimestampNodeId == nodeId)
|
.Find(e => e.TimestampNodeId == nodeId)
|
||||||
.OrderByDescending(e => e.TimestampPhysicalTime)
|
.OrderByDescending(e => e.TimestampPhysicalTime)
|
||||||
.ThenByDescending(e => e.TimestampLogicalCounter)
|
.ThenByDescending(e => e.TimestampLogicalCounter)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
var previousHash = lastEntry?.Hash ?? string.Empty;
|
string previousHash = lastEntry?.Hash ?? string.Empty;
|
||||||
var timestamp = GenerateTimestamp(nodeId);
|
var timestamp = GenerateTimestamp(nodeId);
|
||||||
|
|
||||||
var oplogEntry = new OplogEntry(
|
var oplogEntry = new OplogEntry(
|
||||||
collection,
|
collection,
|
||||||
key,
|
key,
|
||||||
operationType,
|
operationType,
|
||||||
content,
|
content,
|
||||||
timestamp,
|
timestamp,
|
||||||
previousHash);
|
previousHash);
|
||||||
|
|
||||||
// Write directly to OplogEntries collection
|
// Write directly to OplogEntries collection
|
||||||
await _context.OplogEntries.InsertAsync(oplogEntry.ToEntity());
|
await _context.OplogEntries.InsertAsync(oplogEntry.ToEntity());
|
||||||
|
|
||||||
// Write DocumentMetadata for sync tracking
|
// Write DocumentMetadata for sync tracking
|
||||||
var docMetadata = EntityMappers.CreateDocumentMetadata(
|
var docMetadata = EntityMappers.CreateDocumentMetadata(
|
||||||
collection,
|
collection,
|
||||||
key,
|
key,
|
||||||
timestamp,
|
timestamp,
|
||||||
isDeleted: operationType == OperationType.Delete);
|
operationType == OperationType.Delete);
|
||||||
|
|
||||||
var existingMetadata = _context.DocumentMetadatas
|
var existingMetadata = _context.DocumentMetadatas
|
||||||
.Find(m => m.Collection == collection && m.Key == key)
|
.Find(m => m.Collection == collection && m.Key == key)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (existingMetadata != null)
|
if (existingMetadata != null)
|
||||||
{
|
{
|
||||||
// Update existing metadata
|
// Update existing metadata
|
||||||
existingMetadata.HlcPhysicalTime = timestamp.PhysicalTime;
|
existingMetadata.HlcPhysicalTime = timestamp.PhysicalTime;
|
||||||
existingMetadata.HlcLogicalCounter = timestamp.LogicalCounter;
|
existingMetadata.HlcLogicalCounter = timestamp.LogicalCounter;
|
||||||
existingMetadata.HlcNodeId = timestamp.NodeId;
|
existingMetadata.HlcNodeId = timestamp.NodeId;
|
||||||
existingMetadata.IsDeleted = operationType == OperationType.Delete;
|
existingMetadata.IsDeleted = operationType == OperationType.Delete;
|
||||||
await _context.DocumentMetadatas.UpdateAsync(existingMetadata);
|
await _context.DocumentMetadatas.UpdateAsync(existingMetadata);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await _context.DocumentMetadatas.InsertAsync(docMetadata);
|
await _context.DocumentMetadatas.InsertAsync(docMetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Notify VectorClockService so sync sees local changes
|
// Notify VectorClockService so sync sees local changes
|
||||||
@@ -753,24 +745,24 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
_logger.LogDebug(
|
_logger.LogDebug(
|
||||||
"Created Oplog entry: {Operation} {Collection}/{Key} at {Timestamp} (hash: {Hash})",
|
"Created Oplog entry: {Operation} {Collection}/{Key} at {Timestamp} (hash: {Hash})",
|
||||||
operationType, collection, key, timestamp, oplogEntry.Hash);
|
operationType, collection, key, timestamp, oplogEntry.Hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the start of remote sync operations (suppresses CDC-triggered Oplog creation).
|
/// Marks the start of remote sync operations (suppresses CDC-triggered Oplog creation).
|
||||||
/// Use in using statement: using (store.BeginRemoteSync()) { ... }
|
/// Use in using statement: using (store.BeginRemoteSync()) { ... }
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IDisposable BeginRemoteSync()
|
public IDisposable BeginRemoteSync()
|
||||||
{
|
{
|
||||||
_remoteSyncGuard.Wait();
|
_remoteSyncGuard.Wait();
|
||||||
return new RemoteSyncScope(_remoteSyncGuard);
|
return new RemoteSyncScope(_remoteSyncGuard);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RemoteSyncScope : IDisposable
|
private class RemoteSyncScope : IDisposable
|
||||||
{
|
{
|
||||||
private readonly SemaphoreSlim _guard;
|
private readonly SemaphoreSlim _guard;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RemoteSyncScope"/> class.
|
/// Initializes a new instance of the <see cref="RemoteSyncScope" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="guard">The semaphore guarding remote sync operations.</param>
|
/// <param name="guard">The semaphore guarding remote sync operations.</param>
|
||||||
public RemoteSyncScope(SemaphoreSlim guard)
|
public RemoteSyncScope(SemaphoreSlim guard)
|
||||||
@@ -779,26 +771,13 @@ public abstract class BLiteDocumentStore<TDbContext> : IDocumentStore, IDisposab
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Releases the remote sync guard.
|
/// Releases the remote sync guard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_guard.Release();
|
_guard.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
}
|
||||||
/// <summary>
|
|
||||||
/// Releases managed resources used by this document store.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var watcher in _cdcWatchers)
|
|
||||||
{
|
|
||||||
try { watcher.Dispose(); } catch { }
|
|
||||||
}
|
|
||||||
_cdcWatchers.Clear();
|
|
||||||
_remoteSyncGuard.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user