Harden Surreal migration with retry/coverage fixes and XML docs cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s
This commit is contained in:
@@ -124,7 +124,9 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
var user = new User
|
var user = new User
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90),
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Name = $"User-{ts}",
|
||||||
|
Age = new Random().Next(18, 90),
|
||||||
Address = new Address { City = "AutoCity" }
|
Address = new Address { City = "AutoCity" }
|
||||||
};
|
};
|
||||||
await _db.Users.InsertAsync(user);
|
await _db.Users.InsertAsync(user);
|
||||||
@@ -138,7 +140,9 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
var ts = DateTime.Now.ToString("HH:mm:ss.fff");
|
||||||
var user = new User
|
var user = new User
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid().ToString(), Name = $"User-{ts}", Age = new Random().Next(18, 90),
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
Name = $"User-{ts}",
|
||||||
|
Age = new Random().Next(18, 90),
|
||||||
Address = new Address { City = "SpamCity" }
|
Address = new Address { City = "SpamCity" }
|
||||||
};
|
};
|
||||||
await _db.Users.InsertAsync(user);
|
await _db.Users.InsertAsync(user);
|
||||||
@@ -158,9 +162,9 @@ public class ConsoleInteractiveService : BackgroundService
|
|||||||
else if (input.StartsWith("p"))
|
else if (input.StartsWith("p"))
|
||||||
{
|
{
|
||||||
var alice = new User
|
var alice = new User
|
||||||
{ Id = Guid.NewGuid().ToString(), Name = "Alice", Age = 30, Address = new Address { City = "Paris" } };
|
{ Id = Guid.NewGuid().ToString(), Name = "Alice", Age = 30, Address = new Address { City = "Paris" } };
|
||||||
var bob = new User
|
var bob = new User
|
||||||
{ Id = Guid.NewGuid().ToString(), Name = "Bob", Age = 25, Address = new Address { City = "Rome" } };
|
{ 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();
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ public class SampleDbContext : IDisposable
|
|||||||
|
|
||||||
private readonly bool _ownsClient;
|
private readonly bool _ownsClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleDbContext"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
public SampleDbContext(
|
public SampleDbContext(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer)
|
ICBDDCSurrealSchemaInitializer schemaInitializer)
|
||||||
@@ -29,6 +34,10 @@ public class SampleDbContext : IDisposable
|
|||||||
SchemaInitializer);
|
SchemaInitializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleDbContext"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="databasePath">The database path used for the embedded store.</param>
|
||||||
public SampleDbContext(string databasePath)
|
public SampleDbContext(string databasePath)
|
||||||
{
|
{
|
||||||
string normalizedPath = NormalizeDatabasePath(databasePath);
|
string normalizedPath = NormalizeDatabasePath(databasePath);
|
||||||
@@ -54,21 +63,41 @@ public class SampleDbContext : IDisposable
|
|||||||
SchemaInitializer);
|
SchemaInitializer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the embedded SurrealDB client.
|
||||||
|
/// </summary>
|
||||||
public ICBDDCSurrealEmbeddedClient SurrealEmbeddedClient { get; }
|
public ICBDDCSurrealEmbeddedClient SurrealEmbeddedClient { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the schema initializer.
|
||||||
|
/// </summary>
|
||||||
public ICBDDCSurrealSchemaInitializer SchemaInitializer { get; private set; }
|
public ICBDDCSurrealSchemaInitializer SchemaInitializer { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the users collection.
|
||||||
|
/// </summary>
|
||||||
public SampleSurrealCollection<User> Users { get; private set; }
|
public SampleSurrealCollection<User> Users { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the todo lists collection.
|
||||||
|
/// </summary>
|
||||||
public SampleSurrealCollection<TodoList> TodoLists { get; private set; }
|
public SampleSurrealCollection<TodoList> TodoLists { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the operation log entries collection.
|
||||||
|
/// </summary>
|
||||||
public SampleSurrealReadOnlyCollection<SampleOplogEntry> OplogEntries { get; private set; }
|
public SampleSurrealReadOnlyCollection<SampleOplogEntry> OplogEntries { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures schema changes are applied before persisting updates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
public async Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await SchemaInitializer.EnsureInitializedAsync(cancellationToken);
|
await SchemaInitializer.EnsureInitializedAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Users.Dispose();
|
Users.Dispose();
|
||||||
@@ -101,11 +130,16 @@ public sealed class SampleSurrealSchemaInitializer : ICBDDCSurrealSchemaInitiali
|
|||||||
private readonly ICBDDCSurrealEmbeddedClient _client;
|
private readonly ICBDDCSurrealEmbeddedClient _client;
|
||||||
private int _initialized;
|
private int _initialized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleSurrealSchemaInitializer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The embedded SurrealDB client.</param>
|
||||||
public SampleSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
|
public SampleSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
|
||||||
{
|
{
|
||||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
_client = client ?? throw new ArgumentNullException(nameof(client));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
|
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (Volatile.Read(ref _initialized) == 1) return;
|
if (Volatile.Read(ref _initialized) == 1) return;
|
||||||
@@ -124,6 +158,13 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly string _tableName;
|
private readonly string _tableName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleSurrealCollection{TEntity}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">The backing table name.</param>
|
||||||
|
/// <param name="keySelector">The key selector for entities.</param>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
public SampleSurrealCollection(
|
public SampleSurrealCollection(
|
||||||
string tableName,
|
string tableName,
|
||||||
Func<TEntity, string> keySelector,
|
Func<TEntity, string> keySelector,
|
||||||
@@ -139,21 +180,25 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
|
|||||||
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
|
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IDisposable Subscribe(IObserver<SurrealCollectionChange<TEntity>> observer)
|
public IDisposable Subscribe(IObserver<SurrealCollectionChange<TEntity>> observer)
|
||||||
{
|
{
|
||||||
return _changeFeed.Subscribe(observer);
|
return _changeFeed.Subscribe(observer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
|
public async Task InsertAsync(TEntity entity, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await UpsertAsync(entity, cancellationToken);
|
await UpsertAsync(entity, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
|
public async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await UpsertAsync(entity, cancellationToken);
|
await UpsertAsync(entity, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
|
public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
@@ -164,11 +209,22 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
|
|||||||
_changeFeed.PublishDelete(id);
|
_changeFeed.PublishDelete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an entity by identifier.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The entity identifier.</param>
|
||||||
|
/// <returns>The matching entity when found; otherwise <see langword="null"/>.</returns>
|
||||||
public TEntity? FindById(string id)
|
public TEntity? FindById(string id)
|
||||||
{
|
{
|
||||||
return FindByIdAsync(id).GetAwaiter().GetResult();
|
return FindByIdAsync(id).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds an entity by identifier asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The entity identifier.</param>
|
||||||
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
|
/// <returns>The matching entity when found; otherwise <see langword="null"/>.</returns>
|
||||||
public async Task<TEntity?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
|
public async Task<TEntity?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
@@ -179,11 +235,13 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
|
|||||||
return record?.Entity;
|
return record?.Entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IEnumerable<TEntity> FindAll()
|
public IEnumerable<TEntity> FindAll()
|
||||||
{
|
{
|
||||||
return FindAllAsync().GetAwaiter().GetResult();
|
return FindAllAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
@@ -195,12 +253,14 @@ public sealed class SampleSurrealCollection<TEntity> : ISurrealWatchableCollecti
|
|||||||
?? [];
|
?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
|
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(predicate);
|
ArgumentNullException.ThrowIfNull(predicate);
|
||||||
return FindAll().Where(predicate);
|
return FindAll().Where(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_changeFeed.Dispose();
|
_changeFeed.Dispose();
|
||||||
@@ -235,6 +295,12 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly string _tableName;
|
private readonly string _tableName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleSurrealReadOnlyCollection{TEntity}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">The backing table name.</param>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
public SampleSurrealReadOnlyCollection(
|
public SampleSurrealReadOnlyCollection(
|
||||||
string tableName,
|
string tableName,
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
@@ -248,11 +314,20 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
|
|||||||
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
|
_schemaInitializer = schemaInitializer ?? throw new ArgumentNullException(nameof(schemaInitializer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities from the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The entities in the collection.</returns>
|
||||||
public IEnumerable<TEntity> FindAll()
|
public IEnumerable<TEntity> FindAll()
|
||||||
{
|
{
|
||||||
return FindAllAsync().GetAwaiter().GetResult();
|
return FindAllAsync().GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all entities from the collection asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">A cancellation token.</param>
|
||||||
|
/// <returns>The entities in the collection.</returns>
|
||||||
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<TEntity>> FindAllAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _schemaInitializer.EnsureInitializedAsync(cancellationToken);
|
await _schemaInitializer.EnsureInitializedAsync(cancellationToken);
|
||||||
@@ -260,6 +335,11 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
|
|||||||
return rows?.ToList() ?? [];
|
return rows?.ToList() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns entities that match the provided predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="predicate">The predicate used to filter entities.</param>
|
||||||
|
/// <returns>The entities that satisfy the predicate.</returns>
|
||||||
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
|
public IEnumerable<TEntity> Find(Func<TEntity, bool> predicate)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(predicate);
|
ArgumentNullException.ThrowIfNull(predicate);
|
||||||
@@ -270,30 +350,54 @@ public sealed class SampleSurrealReadOnlyCollection<TEntity>
|
|||||||
public sealed class SampleEntityRecord<TEntity> : Record
|
public sealed class SampleEntityRecord<TEntity> : Record
|
||||||
where TEntity : class
|
where TEntity : class
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the stored entity payload.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("entity")]
|
[JsonPropertyName("entity")]
|
||||||
public TEntity? Entity { get; set; }
|
public TEntity? Entity { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SampleOplogEntry : Record
|
public sealed class SampleOplogEntry : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection name.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("collection")]
|
[JsonPropertyName("collection")]
|
||||||
public string Collection { get; set; } = "";
|
public string Collection { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the entity key.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("key")]
|
[JsonPropertyName("key")]
|
||||||
public string Key { get; set; } = "";
|
public string Key { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the operation code.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("operation")]
|
[JsonPropertyName("operation")]
|
||||||
public int Operation { get; set; }
|
public int Operation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the node identifier portion of the timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampNodeId")]
|
[JsonPropertyName("timestampNodeId")]
|
||||||
public string TimestampNodeId { get; set; } = "";
|
public string TimestampNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the physical time portion of the timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampPhysicalTime")]
|
[JsonPropertyName("timestampPhysicalTime")]
|
||||||
public long TimestampPhysicalTime { get; set; }
|
public long TimestampPhysicalTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the logical counter portion of the timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampLogicalCounter")]
|
[JsonPropertyName("timestampLogicalCounter")]
|
||||||
public int TimestampLogicalCounter { get; set; }
|
public int TimestampLogicalCounter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the hash for the operation entry.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hash")]
|
[JsonPropertyName("hash")]
|
||||||
public string Hash { get; set; } = "";
|
public string Hash { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
private const string UsersCollection = "Users";
|
private const string UsersCollection = "Users";
|
||||||
private const string TodoListsCollection = "TodoLists";
|
private const string TodoListsCollection = "TodoLists";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SampleDocumentStore"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Sample database context.</param>
|
||||||
|
/// <param name="configProvider">Peer configuration provider.</param>
|
||||||
|
/// <param name="vectorClockService">Vector clock service.</param>
|
||||||
|
/// <param name="logger">Optional logger.</param>
|
||||||
public SampleDocumentStore(
|
public SampleDocumentStore(
|
||||||
SampleDbContext context,
|
SampleDbContext context,
|
||||||
IPeerNodeConfigurationProvider configProvider,
|
IPeerNodeConfigurationProvider configProvider,
|
||||||
@@ -35,6 +42,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntityAsync(
|
protected override async Task ApplyContentToEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -44,6 +52,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntitiesBatchAsync(
|
protected override async Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -52,6 +61,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -65,6 +75,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntityAsync(
|
protected override async Task RemoveEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -73,6 +84,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntitiesBatchAsync(
|
protected override async Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents,
|
IEnumerable<(string Collection, string Key)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -81,6 +93,7 @@ public class SampleDocumentStore : SurrealDocumentStore<SampleDbContext>
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -55,14 +55,16 @@ public class RetryPolicy : IRetryPolicy
|
|||||||
|
|
||||||
return await operation();
|
return await operation();
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (attempt < config.RetryAttempts && IsTransient(ex))
|
catch (Exception ex) when (IsTransient(ex))
|
||||||
{
|
{
|
||||||
lastException = ex;
|
lastException = ex;
|
||||||
int delay = config.RetryDelayMs * attempt; // Exponential backoff
|
if (attempt >= config.RetryAttempts) break;
|
||||||
|
|
||||||
_logger.LogWarning(ex,
|
int delay = config.RetryDelayMs * attempt; // Exponential backoff
|
||||||
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
|
|
||||||
operationName, attempt, config.RetryAttempts, delay);
|
_logger.LogWarning(ex,
|
||||||
|
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
|
||||||
|
operationName, attempt, config.RetryAttempts, delay);
|
||||||
|
|
||||||
await Task.Delay(delay, cancellationToken);
|
await Task.Delay(delay, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -111,4 +113,4 @@ public class RetryPolicy : IRetryPolicy
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,15 @@ public interface ICBDDCSurrealEmbeddedClient : IAsyncDisposable, IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connects and selects namespace/database exactly once.
|
/// Connects and selects namespace/database exactly once.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task InitializeAsync(CancellationToken cancellationToken = default);
|
Task InitializeAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes a raw SurrealQL statement.
|
/// Executes a raw SurrealQL statement.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="query">The SurrealQL query to execute.</param>
|
||||||
|
/// <param name="parameters">Optional named parameters for the query.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<SurrealDbResponse> RawQueryAsync(string query,
|
Task<SurrealDbResponse> RawQueryAsync(string query,
|
||||||
IReadOnlyDictionary<string, object?>? parameters = null,
|
IReadOnlyDictionary<string, object?>? parameters = null,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
@@ -28,5 +32,6 @@ public interface ICBDDCSurrealEmbeddedClient : IAsyncDisposable, IDisposable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks whether the embedded client responds to health probes.
|
/// Checks whether the embedded client responds to health probes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task<bool> HealthAsync(CancellationToken cancellationToken = default);
|
Task<bool> HealthAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ public interface ICBDDCSurrealReadinessProbe
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns true when client initialization, schema initialization, and health checks pass.
|
/// Returns true when client initialization, schema initialization, and health checks pass.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
Task<bool> IsReadyAsync(CancellationToken cancellationToken = default);
|
Task<bool> IsReadyAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ public interface ICBDDCSurrealSchemaInitializer
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates required tables/indexes/checkpoint schema for CBDDC stores.
|
/// Creates required tables/indexes/checkpoint schema for CBDDC stores.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
Task EnsureInitializedAsync(CancellationToken cancellationToken = default);
|
Task EnsureInitializedAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,18 @@ public interface ISurrealCdcWorkerLifecycle
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts the CDC worker.
|
/// Starts the CDC worker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
|
||||||
Task StartCdcWorkerAsync(CancellationToken cancellationToken = default);
|
Task StartCdcWorkerAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes one CDC polling pass across all watched collections.
|
/// Executes one CDC polling pass across all watched collections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
|
||||||
Task PollCdcOnceAsync(CancellationToken cancellationToken = default);
|
Task PollCdcOnceAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops the CDC worker.
|
/// Stops the CDC worker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
|
||||||
Task StopCdcWorkerAsync(CancellationToken cancellationToken = default);
|
Task StopCdcWorkerAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,30 +150,56 @@ public sealed class SurrealCdcCheckpointPersistence : ISurrealCdcCheckpointPersi
|
|||||||
|
|
||||||
internal sealed class SurrealCdcCheckpointRecord : Record
|
internal sealed class SurrealCdcCheckpointRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the CDC consumer identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("consumerId")]
|
[JsonPropertyName("consumerId")]
|
||||||
public string ConsumerId { get; set; } = "";
|
public string ConsumerId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the physical time component of the checkpoint timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampPhysicalTime")]
|
[JsonPropertyName("timestampPhysicalTime")]
|
||||||
public long TimestampPhysicalTime { get; set; }
|
public long TimestampPhysicalTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the logical counter component of the checkpoint timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampLogicalCounter")]
|
[JsonPropertyName("timestampLogicalCounter")]
|
||||||
public int TimestampLogicalCounter { get; set; }
|
public int TimestampLogicalCounter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the node identifier component of the checkpoint timestamp.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampNodeId")]
|
[JsonPropertyName("timestampNodeId")]
|
||||||
public string TimestampNodeId { get; set; } = "";
|
public string TimestampNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the hash associated with the checkpoint.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("lastHash")]
|
[JsonPropertyName("lastHash")]
|
||||||
public string LastHash { get; set; } = "";
|
public string LastHash { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the last update time in Unix milliseconds.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("updatedUtcMs")]
|
[JsonPropertyName("updatedUtcMs")]
|
||||||
public long UpdatedUtcMs { get; set; }
|
public long UpdatedUtcMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the optional encoded versionstamp cursor.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("versionstampCursor")]
|
[JsonPropertyName("versionstampCursor")]
|
||||||
public long? VersionstampCursor { get; set; }
|
public long? VersionstampCursor { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class SurrealCdcCheckpointRecordMappers
|
internal static class SurrealCdcCheckpointRecordMappers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a checkpoint record into the domain checkpoint model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The Surreal checkpoint record.</param>
|
||||||
|
/// <returns>The mapped domain checkpoint instance.</returns>
|
||||||
public static SurrealCdcCheckpoint ToDomain(this SurrealCdcCheckpointRecord record)
|
public static SurrealCdcCheckpoint ToDomain(this SurrealCdcCheckpointRecord record)
|
||||||
{
|
{
|
||||||
return new SurrealCdcCheckpoint
|
return new SurrealCdcCheckpoint
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly ISurrealDbClient _surrealClient;
|
private readonly ISurrealDbClient _surrealClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SurrealDocumentMetadataStore" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded Surreal client provider.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
|
/// <param name="logger">Optional logger.</param>
|
||||||
public SurrealDocumentMetadataStore(
|
public SurrealDocumentMetadataStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -24,6 +30,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
_logger = logger ?? NullLogger<SurrealDocumentMetadataStore>.Instance;
|
_logger = logger ?? NullLogger<SurrealDocumentMetadataStore>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<DocumentMetadata?> GetMetadataAsync(string collection, string key,
|
public override async Task<DocumentMetadata?> GetMetadataAsync(string collection, string key,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -31,6 +38,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
return existing?.ToDomain();
|
return existing?.ToDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection,
|
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -41,6 +49,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task UpsertMetadataAsync(DocumentMetadata metadata,
|
public override async Task UpsertMetadataAsync(DocumentMetadata metadata,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -55,6 +64,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas,
|
public override async Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -62,6 +72,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
await UpsertMetadataAsync(metadata, cancellationToken);
|
await UpsertMetadataAsync(metadata, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp,
|
public override async Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -69,6 +80,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
await UpsertMetadataAsync(metadata, cancellationToken);
|
await UpsertMetadataAsync(metadata, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since,
|
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since,
|
||||||
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -86,24 +98,28 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
await _surrealClient.Delete(CBDDCSurrealSchemaNames.DocumentMetadataTable, cancellationToken);
|
await _surrealClient.Delete(CBDDCSurrealSchemaNames.DocumentMetadataTable, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<DocumentMetadata>> ExportAsync(CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<DocumentMetadata>> ExportAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var all = await SelectAllAsync(cancellationToken);
|
var all = await SelectAllAsync(cancellationToken);
|
||||||
return all.Select(m => m.ToDomain()).ToList();
|
return all.Select(m => m.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task ImportAsync(IEnumerable<DocumentMetadata> items,
|
public override async Task ImportAsync(IEnumerable<DocumentMetadata> items,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var item in items) await UpsertMetadataAsync(item, cancellationToken);
|
foreach (var item in items) await UpsertMetadataAsync(item, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task MergeAsync(IEnumerable<DocumentMetadata> items,
|
public override async Task MergeAsync(IEnumerable<DocumentMetadata> items,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SurrealDocumentStore{TContext}" /> class.
|
/// Initializes a new instance of the <see cref="SurrealDocumentStore{TContext}" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="context">The application context used by the concrete store.</param>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded Surreal client provider.</param>
|
||||||
|
/// <param name="schemaInitializer">The Surreal schema initializer.</param>
|
||||||
|
/// <param name="configProvider">The peer node configuration provider.</param>
|
||||||
|
/// <param name="vectorClockService">The vector clock service used for local oplog state.</param>
|
||||||
|
/// <param name="conflictResolver">Optional conflict resolver; defaults to last-write-wins.</param>
|
||||||
|
/// <param name="checkpointPersistence">Optional CDC checkpoint persistence component.</param>
|
||||||
|
/// <param name="cdcPollingOptions">Optional CDC polling options.</param>
|
||||||
|
/// <param name="logger">Optional logger instance.</param>
|
||||||
protected SurrealDocumentStore(
|
protected SurrealDocumentStore(
|
||||||
TContext context,
|
TContext context,
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
@@ -128,21 +137,28 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
{
|
{
|
||||||
private readonly ILogger _inner;
|
private readonly ILogger _inner;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ForwardingLogger" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inner">The logger instance to forward calls to.</param>
|
||||||
public ForwardingLogger(ILogger inner)
|
public ForwardingLogger(ILogger inner)
|
||||||
{
|
{
|
||||||
_inner = inner;
|
_inner = inner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
|
||||||
{
|
{
|
||||||
return _inner.BeginScope(state);
|
return _inner.BeginScope(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
{
|
{
|
||||||
return _inner.IsEnabled(logLevel);
|
return _inner.IsEnabled(logLevel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Log<TState>(
|
public void Log<TState>(
|
||||||
LogLevel logLevel,
|
LogLevel logLevel,
|
||||||
EventId eventId,
|
EventId eventId,
|
||||||
@@ -191,6 +207,7 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
/// <param name="collectionName">Logical collection name used by oplog and metadata records.</param>
|
/// <param name="collectionName">Logical collection name used by oplog and metadata records.</param>
|
||||||
/// <param name="collection">Watchable change source.</param>
|
/// <param name="collection">Watchable change source.</param>
|
||||||
/// <param name="keySelector">Function used to resolve the entity key.</param>
|
/// <param name="keySelector">Function used to resolve the entity key.</param>
|
||||||
|
/// <param name="subscribeForInMemoryEvents">Whether to subscribe to in-memory collection events.</param>
|
||||||
protected void WatchCollection<TEntity>(
|
protected void WatchCollection<TEntity>(
|
||||||
string collectionName,
|
string collectionName,
|
||||||
ISurrealWatchableCollection<TEntity> collection,
|
ISurrealWatchableCollection<TEntity> collection,
|
||||||
@@ -220,6 +237,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
private readonly Func<TEntity, string> _keySelector;
|
private readonly Func<TEntity, string> _keySelector;
|
||||||
private readonly SurrealDocumentStore<TContext> _store;
|
private readonly SurrealDocumentStore<TContext> _store;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CdcObserver{TEntity}" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collectionName">The logical collection name.</param>
|
||||||
|
/// <param name="keySelector">The key selector for observed entities.</param>
|
||||||
|
/// <param name="store">The owning document store.</param>
|
||||||
public CdcObserver(
|
public CdcObserver(
|
||||||
string collectionName,
|
string collectionName,
|
||||||
Func<TEntity, string> keySelector,
|
Func<TEntity, string> keySelector,
|
||||||
@@ -230,6 +253,7 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
_store = store;
|
_store = store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnNext(SurrealCollectionChange<TEntity> changeEvent)
|
public void OnNext(SurrealCollectionChange<TEntity> changeEvent)
|
||||||
{
|
{
|
||||||
if (_store.IsCdcPollingWorkerActiveForCollection(_collectionName)) return;
|
if (_store.IsCdcPollingWorkerActiveForCollection(_collectionName)) return;
|
||||||
@@ -267,10 +291,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
.GetAwaiter().GetResult();
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnError(Exception error)
|
public void OnError(Exception error)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void OnCompleted()
|
public void OnCompleted()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -760,22 +786,58 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
|
|
||||||
#region Abstract Methods - Implemented by subclass
|
#region Abstract Methods - Implemented by subclass
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies JSON content to a single entity in the backing store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <param name="content">The JSON payload to persist.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task ApplyContentToEntityAsync(
|
protected abstract Task ApplyContentToEntityAsync(
|
||||||
string collection, string key, JsonElement content, CancellationToken cancellationToken);
|
string collection, string key, JsonElement content, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies JSON content to multiple entities in the backing store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="documents">The documents to persist.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task ApplyContentToEntitiesBatchAsync(
|
protected abstract Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a single entity as JSON content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>The JSON content when found; otherwise <see langword="null" />.</returns>
|
||||||
protected abstract Task<JsonElement?> GetEntityAsJsonAsync(
|
protected abstract Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection, string key, CancellationToken cancellationToken);
|
string collection, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a single entity from the backing store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task RemoveEntityAsync(
|
protected abstract Task RemoveEntityAsync(
|
||||||
string collection, string key, CancellationToken cancellationToken);
|
string collection, string key, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes multiple entities from the backing store.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="documents">The documents to remove.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected abstract Task RemoveEntitiesBatchAsync(
|
protected abstract Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken);
|
IEnumerable<(string Collection, string Key)> documents, CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all entities from a collection as JSON content.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>A sequence of key/content pairs.</returns>
|
||||||
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);
|
||||||
|
|
||||||
@@ -1055,6 +1117,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles a local collection change and records oplog/metadata when not suppressed.
|
/// Handles a local collection change and records oplog/metadata when not suppressed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <param name="operationType">The detected operation type.</param>
|
||||||
|
/// <param name="content">Optional JSON content for non-delete operations.</param>
|
||||||
|
/// <param name="pendingCursorCheckpoint">Optional pending cursor checkpoint to persist.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
protected async Task OnLocalChangeDetectedAsync(
|
protected async Task OnLocalChangeDetectedAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -1315,11 +1383,16 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
|
|||||||
private readonly SemaphoreSlim _guard;
|
private readonly SemaphoreSlim _guard;
|
||||||
private int _disposed;
|
private int _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RemoteSyncScope" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guard">The guard semaphore to release on dispose.</param>
|
||||||
public RemoteSyncScope(SemaphoreSlim guard)
|
public RemoteSyncScope(SemaphoreSlim guard)
|
||||||
{
|
{
|
||||||
_guard = guard;
|
_guard = guard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
|
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ public sealed class SurrealCollectionChangeFeed<TEntity> : ISurrealWatchableColl
|
|||||||
private readonly IObserver<SurrealCollectionChange<TEntity>> _observer;
|
private readonly IObserver<SurrealCollectionChange<TEntity>> _observer;
|
||||||
private int _disposed;
|
private int _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="Subscription" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="owner">The owning change feed.</param>
|
||||||
|
/// <param name="observer">The observer to unsubscribe on disposal.</param>
|
||||||
public Subscription(
|
public Subscription(
|
||||||
SurrealCollectionChangeFeed<TEntity> owner,
|
SurrealCollectionChangeFeed<TEntity> owner,
|
||||||
IObserver<SurrealCollectionChange<TEntity>> observer)
|
IObserver<SurrealCollectionChange<TEntity>> observer)
|
||||||
@@ -135,6 +140,7 @@ public sealed class SurrealCollectionChangeFeed<TEntity> : ISurrealWatchableColl
|
|||||||
_observer = observer;
|
_observer = observer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
|
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ public class SurrealOplogStore : OplogStore
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer? _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer? _schemaInitializer;
|
||||||
private readonly ISurrealDbClient? _surrealClient;
|
private readonly ISurrealDbClient? _surrealClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SurrealOplogStore" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The Surreal embedded client provider.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer used to prepare storage.</param>
|
||||||
|
/// <param name="documentStore">The document store used for entity operations.</param>
|
||||||
|
/// <param name="conflictResolver">The conflict resolver for replicated mutations.</param>
|
||||||
|
/// <param name="vectorClockService">The vector clock service for causal ordering.</param>
|
||||||
|
/// <param name="snapshotMetadataStore">The optional snapshot metadata store.</param>
|
||||||
|
/// <param name="logger">The optional logger instance.</param>
|
||||||
public SurrealOplogStore(
|
public SurrealOplogStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -36,6 +46,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
InitializeVectorClock();
|
InitializeVectorClock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<OplogEntry>> GetChainRangeAsync(string startHash, string endHash,
|
public override async Task<IEnumerable<OplogEntry>> GetChainRangeAsync(string startHash, string endHash,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -61,12 +72,14 @@ public class SurrealOplogStore : OplogStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<OplogEntry?> GetEntryByHashAsync(string hash, CancellationToken cancellationToken = default)
|
public override async Task<OplogEntry?> GetEntryByHashAsync(string hash, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var existing = await FindByHashAsync(hash, cancellationToken);
|
var existing = await FindByHashAsync(hash, cancellationToken);
|
||||||
return existing?.ToDomain();
|
return existing?.ToDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<OplogEntry>> GetOplogAfterAsync(HlcTimestamp timestamp,
|
public override async Task<IEnumerable<OplogEntry>> GetOplogAfterAsync(HlcTimestamp timestamp,
|
||||||
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -85,6 +98,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<OplogEntry>> GetOplogForNodeAfterAsync(string nodeId, HlcTimestamp since,
|
public override async Task<IEnumerable<OplogEntry>> GetOplogForNodeAfterAsync(string nodeId, HlcTimestamp since,
|
||||||
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -104,6 +118,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task PruneOplogAsync(HlcTimestamp cutoff, CancellationToken cancellationToken = default)
|
public override async Task PruneOplogAsync(HlcTimestamp cutoff, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var all = await SelectAllAsync(cancellationToken);
|
var all = await SelectAllAsync(cancellationToken);
|
||||||
@@ -121,6 +136,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
@@ -128,12 +144,14 @@ public class SurrealOplogStore : OplogStore
|
|||||||
_vectorClock.Invalidate();
|
_vectorClock.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<OplogEntry>> ExportAsync(CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<OplogEntry>> ExportAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var all = await SelectAllAsync(cancellationToken);
|
var all = await SelectAllAsync(cancellationToken);
|
||||||
return all.Select(o => o.ToDomain()).ToList();
|
return all.Select(o => o.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task ImportAsync(IEnumerable<OplogEntry> items, CancellationToken cancellationToken = default)
|
public override async Task ImportAsync(IEnumerable<OplogEntry> items, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
@@ -144,6 +162,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task MergeAsync(IEnumerable<OplogEntry> items, CancellationToken cancellationToken = default)
|
public override async Task MergeAsync(IEnumerable<OplogEntry> items, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
@@ -155,6 +174,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void InitializeVectorClock()
|
protected override void InitializeVectorClock()
|
||||||
{
|
{
|
||||||
if (_vectorClock.IsInitialized) return;
|
if (_vectorClock.IsInitialized) return;
|
||||||
@@ -206,6 +226,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
_vectorClock.IsInitialized = true;
|
_vectorClock.IsInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task InsertOplogEntryAsync(OplogEntry entry, CancellationToken cancellationToken = default)
|
protected override async Task InsertOplogEntryAsync(OplogEntry entry, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var existing = await FindByHashAsync(entry.Hash, cancellationToken);
|
var existing = await FindByHashAsync(entry.Hash, cancellationToken);
|
||||||
@@ -214,6 +235,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
await UpsertAsync(entry, SurrealStoreRecordIds.Oplog(entry.Hash), cancellationToken);
|
await UpsertAsync(entry, SurrealStoreRecordIds.Oplog(entry.Hash), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<string?> QueryLastHashForNodeAsync(string nodeId,
|
protected override async Task<string?> QueryLastHashForNodeAsync(string nodeId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -226,6 +248,7 @@ public class SurrealOplogStore : OplogStore
|
|||||||
return lastEntry?.Hash;
|
return lastEntry?.Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<(long Wall, int Logic)?> QueryLastHashTimestampFromOplogAsync(string hash,
|
protected override async Task<(long Wall, int Logic)?> QueryLastHashTimestampFromOplogAsync(string hash,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly ISurrealDbClient _surrealClient;
|
private readonly ISurrealDbClient _surrealClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SurrealPeerConfigurationStore"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded SurrealDB client.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
|
/// <param name="logger">The logger instance.</param>
|
||||||
public SurrealPeerConfigurationStore(
|
public SurrealPeerConfigurationStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -23,6 +29,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
_logger = logger ?? NullLogger<SurrealPeerConfigurationStore>.Instance;
|
_logger = logger ?? NullLogger<SurrealPeerConfigurationStore>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<RemotePeerConfiguration>> GetRemotePeersAsync(
|
public override async Task<IEnumerable<RemotePeerConfiguration>> GetRemotePeersAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -30,6 +37,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
return all.Select(p => p.ToDomain()).ToList();
|
return all.Select(p => p.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<RemotePeerConfiguration?> GetRemotePeerAsync(string nodeId,
|
public override async Task<RemotePeerConfiguration?> GetRemotePeerAsync(string nodeId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -37,6 +45,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
return existing?.ToDomain();
|
return existing?.ToDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default)
|
public override async Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
@@ -52,6 +61,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
_logger.LogInformation("Removed remote peer configuration: {NodeId}", nodeId);
|
_logger.LogInformation("Removed remote peer configuration: {NodeId}", nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task SaveRemotePeerAsync(RemotePeerConfiguration peer,
|
public override async Task SaveRemotePeerAsync(RemotePeerConfiguration peer,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -67,6 +77,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
_logger.LogInformation("Saved remote peer configuration: {NodeId} ({Type})", peer.NodeId, peer.Type);
|
_logger.LogInformation("Saved remote peer configuration: {NodeId} ({Type})", peer.NodeId, peer.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
@@ -76,6 +87,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
|
|||||||
_logger.LogInformation("Peer configuration store dropped successfully.");
|
_logger.LogInformation("Peer configuration store dropped successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<RemotePeerConfiguration>> ExportAsync(
|
public override async Task<IEnumerable<RemotePeerConfiguration>> ExportAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly ISurrealDbClient _surrealClient;
|
private readonly ISurrealDbClient _surrealClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SurrealPeerOplogConfirmationStore"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">Embedded Surreal client wrapper.</param>
|
||||||
|
/// <param name="schemaInitializer">Schema initializer.</param>
|
||||||
|
/// <param name="logger">Optional logger.</param>
|
||||||
public SurrealPeerOplogConfirmationStore(
|
public SurrealPeerOplogConfirmationStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -26,6 +32,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
_logger = logger ?? NullLogger<SurrealPeerOplogConfirmationStore>.Instance;
|
_logger = logger ?? NullLogger<SurrealPeerOplogConfirmationStore>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task EnsurePeerRegisteredAsync(
|
public override async Task EnsurePeerRegisteredAsync(
|
||||||
string peerNodeId,
|
string peerNodeId,
|
||||||
string address,
|
string address,
|
||||||
@@ -68,6 +75,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
await UpsertAsync(existing, recordId, cancellationToken);
|
await UpsertAsync(existing, recordId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task UpdateConfirmationAsync(
|
public override async Task UpdateConfirmationAsync(
|
||||||
string peerNodeId,
|
string peerNodeId,
|
||||||
string sourceNodeId,
|
string sourceNodeId,
|
||||||
@@ -118,6 +126,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
await UpsertAsync(existing, recordId, cancellationToken);
|
await UpsertAsync(existing, recordId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsAsync(
|
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -128,6 +137,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsForPeerAsync(
|
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsForPeerAsync(
|
||||||
string peerNodeId,
|
string peerNodeId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -143,6 +153,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task RemovePeerTrackingAsync(string peerNodeId, CancellationToken cancellationToken = default)
|
public override async Task RemovePeerTrackingAsync(string peerNodeId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(peerNodeId))
|
if (string.IsNullOrWhiteSpace(peerNodeId))
|
||||||
@@ -167,6 +178,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<string>> GetActiveTrackedPeersAsync(
|
public override async Task<IEnumerable<string>> GetActiveTrackedPeersAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -178,18 +190,21 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
await _surrealClient.Delete(CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable, cancellationToken);
|
await _surrealClient.Delete(CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<PeerOplogConfirmation>> ExportAsync(CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<PeerOplogConfirmation>> ExportAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var all = await SelectAllAsync(cancellationToken);
|
var all = await SelectAllAsync(cancellationToken);
|
||||||
return all.Select(c => c.ToDomain()).ToList();
|
return all.Select(c => c.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task ImportAsync(IEnumerable<PeerOplogConfirmation> items,
|
public override async Task ImportAsync(IEnumerable<PeerOplogConfirmation> items,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -202,6 +217,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task MergeAsync(IEnumerable<PeerOplogConfirmation> items,
|
public override async Task MergeAsync(IEnumerable<PeerOplogConfirmation> items,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ internal static class SurrealShowChangesCborDecoder
|
|||||||
{
|
{
|
||||||
private static readonly string[] PutChangeKinds = ["create", "update", "upsert", "insert", "set", "replace"];
|
private static readonly string[] PutChangeKinds = ["create", "update", "upsert", "insert", "set", "replace"];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodes change rows returned by a SurrealDB show changes query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rows">The CBOR rows to decode.</param>
|
||||||
|
/// <param name="expectedTableName">The expected table name used to validate row identifiers.</param>
|
||||||
|
/// <returns>The decoded set of change rows.</returns>
|
||||||
public static IReadOnlyList<SurrealPolledChangeRow> DecodeRows(
|
public static IReadOnlyList<SurrealPolledChangeRow> DecodeRows(
|
||||||
IEnumerable<CborObject> rows,
|
IEnumerable<CborObject> rows,
|
||||||
string expectedTableName)
|
string expectedTableName)
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
private readonly ISurrealDbClient _surrealClient;
|
private readonly ISurrealDbClient _surrealClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SurrealSnapshotMetadataStore" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The Surreal embedded client provider.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer used to prepare storage.</param>
|
||||||
|
/// <param name="logger">The optional logger instance.</param>
|
||||||
public SurrealSnapshotMetadataStore(
|
public SurrealSnapshotMetadataStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -23,18 +29,21 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
_logger = logger ?? NullLogger<SurrealSnapshotMetadataStore>.Instance;
|
_logger = logger ?? NullLogger<SurrealSnapshotMetadataStore>.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
public override async Task DropAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureReadyAsync(cancellationToken);
|
await EnsureReadyAsync(cancellationToken);
|
||||||
await _surrealClient.Delete(CBDDCSurrealSchemaNames.SnapshotMetadataTable, cancellationToken);
|
await _surrealClient.Delete(CBDDCSurrealSchemaNames.SnapshotMetadataTable, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<SnapshotMetadata>> ExportAsync(CancellationToken cancellationToken = default)
|
public override async Task<IEnumerable<SnapshotMetadata>> ExportAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var all = await SelectAllAsync(cancellationToken);
|
var all = await SelectAllAsync(cancellationToken);
|
||||||
return all.Select(m => m.ToDomain()).ToList();
|
return all.Select(m => m.ToDomain()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<SnapshotMetadata?> GetSnapshotMetadataAsync(string nodeId,
|
public override async Task<SnapshotMetadata?> GetSnapshotMetadataAsync(string nodeId,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -42,12 +51,14 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
return existing?.ToDomain();
|
return existing?.ToDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<string?> GetSnapshotHashAsync(string nodeId, CancellationToken cancellationToken = default)
|
public override async Task<string?> GetSnapshotHashAsync(string nodeId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var existing = await FindByNodeIdAsync(nodeId, cancellationToken);
|
var existing = await FindByNodeIdAsync(nodeId, cancellationToken);
|
||||||
return existing?.Hash;
|
return existing?.Hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task ImportAsync(IEnumerable<SnapshotMetadata> items,
|
public override async Task ImportAsync(IEnumerable<SnapshotMetadata> items,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -59,6 +70,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task InsertSnapshotMetadataAsync(SnapshotMetadata metadata,
|
public override async Task InsertSnapshotMetadataAsync(SnapshotMetadata metadata,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -67,6 +79,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
await UpsertAsync(metadata, recordId, cancellationToken);
|
await UpsertAsync(metadata, recordId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task MergeAsync(IEnumerable<SnapshotMetadata> items, CancellationToken cancellationToken = default)
|
public override async Task MergeAsync(IEnumerable<SnapshotMetadata> items, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
foreach (var metadata in items)
|
foreach (var metadata in items)
|
||||||
@@ -88,6 +101,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task UpdateSnapshotMetadataAsync(SnapshotMetadata existingMeta,
|
public override async Task UpdateSnapshotMetadataAsync(SnapshotMetadata existingMeta,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -98,6 +112,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
|
|||||||
await UpsertAsync(existingMeta, recordId, cancellationToken);
|
await UpsertAsync(existingMeta, recordId, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<SnapshotMetadata>> GetAllSnapshotMetadataAsync(
|
public override async Task<IEnumerable<SnapshotMetadata>> GetAllSnapshotMetadataAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,11 +11,22 @@ namespace ZB.MOM.WW.CBDDC.Persistence.Surreal;
|
|||||||
|
|
||||||
internal static class SurrealStoreRecordIds
|
internal static class SurrealStoreRecordIds
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the record identifier for an oplog entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hash">The oplog entry hash.</param>
|
||||||
|
/// <returns>The SurrealDB record identifier.</returns>
|
||||||
public static RecordId Oplog(string hash)
|
public static RecordId Oplog(string hash)
|
||||||
{
|
{
|
||||||
return RecordId.From(CBDDCSurrealSchemaNames.OplogEntriesTable, hash);
|
return RecordId.From(CBDDCSurrealSchemaNames.OplogEntriesTable, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the record identifier for document metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The document collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <returns>The SurrealDB record identifier.</returns>
|
||||||
public static RecordId DocumentMetadata(string collection, string key)
|
public static RecordId DocumentMetadata(string collection, string key)
|
||||||
{
|
{
|
||||||
return RecordId.From(
|
return RecordId.From(
|
||||||
@@ -23,16 +34,32 @@ internal static class SurrealStoreRecordIds
|
|||||||
CompositeKey("docmeta", collection, key));
|
CompositeKey("docmeta", collection, key));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the record identifier for snapshot metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">The node identifier.</param>
|
||||||
|
/// <returns>The SurrealDB record identifier.</returns>
|
||||||
public static RecordId SnapshotMetadata(string nodeId)
|
public static RecordId SnapshotMetadata(string nodeId)
|
||||||
{
|
{
|
||||||
return RecordId.From(CBDDCSurrealSchemaNames.SnapshotMetadataTable, nodeId);
|
return RecordId.From(CBDDCSurrealSchemaNames.SnapshotMetadataTable, nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the record identifier for a remote peer configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeId">The peer node identifier.</param>
|
||||||
|
/// <returns>The SurrealDB record identifier.</returns>
|
||||||
public static RecordId RemotePeer(string nodeId)
|
public static RecordId RemotePeer(string nodeId)
|
||||||
{
|
{
|
||||||
return RecordId.From(CBDDCSurrealSchemaNames.RemotePeerConfigurationsTable, nodeId);
|
return RecordId.From(CBDDCSurrealSchemaNames.RemotePeerConfigurationsTable, nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the record identifier for a peer oplog confirmation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="peerNodeId">The peer node identifier.</param>
|
||||||
|
/// <param name="sourceNodeId">The source node identifier.</param>
|
||||||
|
/// <returns>The SurrealDB record identifier.</returns>
|
||||||
public static RecordId PeerOplogConfirmation(string peerNodeId, string sourceNodeId)
|
public static RecordId PeerOplogConfirmation(string peerNodeId, string sourceNodeId)
|
||||||
{
|
{
|
||||||
return RecordId.From(
|
return RecordId.From(
|
||||||
@@ -49,114 +76,212 @@ internal static class SurrealStoreRecordIds
|
|||||||
|
|
||||||
internal sealed class SurrealOplogRecord : Record
|
internal sealed class SurrealOplogRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection name.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("collection")]
|
[JsonPropertyName("collection")]
|
||||||
public string Collection { get; set; } = "";
|
public string Collection { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the document key.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("key")]
|
[JsonPropertyName("key")]
|
||||||
public string Key { get; set; } = "";
|
public string Key { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the operation type value.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("operation")]
|
[JsonPropertyName("operation")]
|
||||||
public int Operation { get; set; }
|
public int Operation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the serialized payload JSON.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("payloadJson")]
|
[JsonPropertyName("payloadJson")]
|
||||||
public string PayloadJson { get; set; } = "";
|
public string PayloadJson { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timestamp physical time.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampPhysicalTime")]
|
[JsonPropertyName("timestampPhysicalTime")]
|
||||||
public long TimestampPhysicalTime { get; set; }
|
public long TimestampPhysicalTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timestamp logical counter.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampLogicalCounter")]
|
[JsonPropertyName("timestampLogicalCounter")]
|
||||||
public int TimestampLogicalCounter { get; set; }
|
public int TimestampLogicalCounter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timestamp node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampNodeId")]
|
[JsonPropertyName("timestampNodeId")]
|
||||||
public string TimestampNodeId { get; set; } = "";
|
public string TimestampNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the entry hash.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hash")]
|
[JsonPropertyName("hash")]
|
||||||
public string Hash { get; set; } = "";
|
public string Hash { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the previous entry hash.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("previousHash")]
|
[JsonPropertyName("previousHash")]
|
||||||
public string PreviousHash { get; set; } = "";
|
public string PreviousHash { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class SurrealDocumentMetadataRecord : Record
|
internal sealed class SurrealDocumentMetadataRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the collection name.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("collection")]
|
[JsonPropertyName("collection")]
|
||||||
public string Collection { get; set; } = "";
|
public string Collection { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the document key.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("key")]
|
[JsonPropertyName("key")]
|
||||||
public string Key { get; set; } = "";
|
public string Key { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HLC physical time.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hlcPhysicalTime")]
|
[JsonPropertyName("hlcPhysicalTime")]
|
||||||
public long HlcPhysicalTime { get; set; }
|
public long HlcPhysicalTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HLC logical counter.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hlcLogicalCounter")]
|
[JsonPropertyName("hlcLogicalCounter")]
|
||||||
public int HlcLogicalCounter { get; set; }
|
public int HlcLogicalCounter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the HLC node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hlcNodeId")]
|
[JsonPropertyName("hlcNodeId")]
|
||||||
public string HlcNodeId { get; set; } = "";
|
public string HlcNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the document is deleted.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("isDeleted")]
|
[JsonPropertyName("isDeleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class SurrealRemotePeerRecord : Record
|
internal sealed class SurrealRemotePeerRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the peer node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("nodeId")]
|
[JsonPropertyName("nodeId")]
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the peer network address.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("address")]
|
[JsonPropertyName("address")]
|
||||||
public string Address { get; set; } = "";
|
public string Address { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the peer type value.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("type")]
|
[JsonPropertyName("type")]
|
||||||
public int Type { get; set; }
|
public int Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the peer is enabled.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("isEnabled")]
|
[JsonPropertyName("isEnabled")]
|
||||||
public bool IsEnabled { get; set; }
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the serialized list of collection interests.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("interestsJson")]
|
[JsonPropertyName("interestsJson")]
|
||||||
public string InterestsJson { get; set; } = "";
|
public string InterestsJson { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class SurrealPeerOplogConfirmationRecord : Record
|
internal sealed class SurrealPeerOplogConfirmationRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the peer node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("peerNodeId")]
|
[JsonPropertyName("peerNodeId")]
|
||||||
public string PeerNodeId { get; set; } = "";
|
public string PeerNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the source node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("sourceNodeId")]
|
[JsonPropertyName("sourceNodeId")]
|
||||||
public string SourceNodeId { get; set; } = "";
|
public string SourceNodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the confirmed wall clock component.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("confirmedWall")]
|
[JsonPropertyName("confirmedWall")]
|
||||||
public long ConfirmedWall { get; set; }
|
public long ConfirmedWall { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the confirmed logical component.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("confirmedLogic")]
|
[JsonPropertyName("confirmedLogic")]
|
||||||
public int ConfirmedLogic { get; set; }
|
public int ConfirmedLogic { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the confirmed hash.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("confirmedHash")]
|
[JsonPropertyName("confirmedHash")]
|
||||||
public string ConfirmedHash { get; set; } = "";
|
public string ConfirmedHash { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the last confirmation time in Unix milliseconds.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("lastConfirmedUtcMs")]
|
[JsonPropertyName("lastConfirmedUtcMs")]
|
||||||
public long LastConfirmedUtcMs { get; set; }
|
public long LastConfirmedUtcMs { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the confirmation is active.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("isActive")]
|
[JsonPropertyName("isActive")]
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class SurrealSnapshotMetadataRecord : Record
|
internal sealed class SurrealSnapshotMetadataRecord : Record
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the node identifier.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("nodeId")]
|
[JsonPropertyName("nodeId")]
|
||||||
public string NodeId { get; set; } = "";
|
public string NodeId { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timestamp physical time.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampPhysicalTime")]
|
[JsonPropertyName("timestampPhysicalTime")]
|
||||||
public long TimestampPhysicalTime { get; set; }
|
public long TimestampPhysicalTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timestamp logical counter.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("timestampLogicalCounter")]
|
[JsonPropertyName("timestampLogicalCounter")]
|
||||||
public int TimestampLogicalCounter { get; set; }
|
public int TimestampLogicalCounter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the snapshot hash.
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("hash")]
|
[JsonPropertyName("hash")]
|
||||||
public string Hash { get; set; } = "";
|
public string Hash { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class SurrealStoreRecordMappers
|
internal static class SurrealStoreRecordMappers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a domain oplog entry to a SurrealDB record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">The domain oplog entry.</param>
|
||||||
|
/// <returns>The SurrealDB oplog record.</returns>
|
||||||
public static SurrealOplogRecord ToSurrealRecord(this OplogEntry entry)
|
public static SurrealOplogRecord ToSurrealRecord(this OplogEntry entry)
|
||||||
{
|
{
|
||||||
return new SurrealOplogRecord
|
return new SurrealOplogRecord
|
||||||
@@ -173,6 +298,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a SurrealDB oplog record to a domain oplog entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The SurrealDB oplog record.</param>
|
||||||
|
/// <returns>The domain oplog entry.</returns>
|
||||||
public static OplogEntry ToDomain(this SurrealOplogRecord record)
|
public static OplogEntry ToDomain(this SurrealOplogRecord record)
|
||||||
{
|
{
|
||||||
JsonElement? payload = null;
|
JsonElement? payload = null;
|
||||||
@@ -189,6 +319,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
record.Hash);
|
record.Hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps domain document metadata to a SurrealDB record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">The domain document metadata.</param>
|
||||||
|
/// <returns>The SurrealDB document metadata record.</returns>
|
||||||
public static SurrealDocumentMetadataRecord ToSurrealRecord(this DocumentMetadata metadata)
|
public static SurrealDocumentMetadataRecord ToSurrealRecord(this DocumentMetadata metadata)
|
||||||
{
|
{
|
||||||
return new SurrealDocumentMetadataRecord
|
return new SurrealDocumentMetadataRecord
|
||||||
@@ -202,6 +337,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a SurrealDB document metadata record to domain document metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The SurrealDB document metadata record.</param>
|
||||||
|
/// <returns>The domain document metadata.</returns>
|
||||||
public static DocumentMetadata ToDomain(this SurrealDocumentMetadataRecord record)
|
public static DocumentMetadata ToDomain(this SurrealDocumentMetadataRecord record)
|
||||||
{
|
{
|
||||||
return new DocumentMetadata(
|
return new DocumentMetadata(
|
||||||
@@ -211,6 +351,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
record.IsDeleted);
|
record.IsDeleted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a domain remote peer configuration to a SurrealDB record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="peer">The domain remote peer configuration.</param>
|
||||||
|
/// <returns>The SurrealDB remote peer record.</returns>
|
||||||
public static SurrealRemotePeerRecord ToSurrealRecord(this RemotePeerConfiguration peer)
|
public static SurrealRemotePeerRecord ToSurrealRecord(this RemotePeerConfiguration peer)
|
||||||
{
|
{
|
||||||
return new SurrealRemotePeerRecord
|
return new SurrealRemotePeerRecord
|
||||||
@@ -225,6 +370,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a SurrealDB remote peer record to a domain remote peer configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The SurrealDB remote peer record.</param>
|
||||||
|
/// <returns>The domain remote peer configuration.</returns>
|
||||||
public static RemotePeerConfiguration ToDomain(this SurrealRemotePeerRecord record)
|
public static RemotePeerConfiguration ToDomain(this SurrealRemotePeerRecord record)
|
||||||
{
|
{
|
||||||
var result = new RemotePeerConfiguration
|
var result = new RemotePeerConfiguration
|
||||||
@@ -242,6 +392,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a domain peer oplog confirmation to a SurrealDB record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="confirmation">The domain peer oplog confirmation.</param>
|
||||||
|
/// <returns>The SurrealDB peer oplog confirmation record.</returns>
|
||||||
public static SurrealPeerOplogConfirmationRecord ToSurrealRecord(this PeerOplogConfirmation confirmation)
|
public static SurrealPeerOplogConfirmationRecord ToSurrealRecord(this PeerOplogConfirmation confirmation)
|
||||||
{
|
{
|
||||||
return new SurrealPeerOplogConfirmationRecord
|
return new SurrealPeerOplogConfirmationRecord
|
||||||
@@ -256,6 +411,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a SurrealDB peer oplog confirmation record to a domain model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The SurrealDB peer oplog confirmation record.</param>
|
||||||
|
/// <returns>The domain peer oplog confirmation.</returns>
|
||||||
public static PeerOplogConfirmation ToDomain(this SurrealPeerOplogConfirmationRecord record)
|
public static PeerOplogConfirmation ToDomain(this SurrealPeerOplogConfirmationRecord record)
|
||||||
{
|
{
|
||||||
return new PeerOplogConfirmation
|
return new PeerOplogConfirmation
|
||||||
@@ -270,6 +430,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps domain snapshot metadata to a SurrealDB record.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">The domain snapshot metadata.</param>
|
||||||
|
/// <returns>The SurrealDB snapshot metadata record.</returns>
|
||||||
public static SurrealSnapshotMetadataRecord ToSurrealRecord(this SnapshotMetadata metadata)
|
public static SurrealSnapshotMetadataRecord ToSurrealRecord(this SnapshotMetadata metadata)
|
||||||
{
|
{
|
||||||
return new SurrealSnapshotMetadataRecord
|
return new SurrealSnapshotMetadataRecord
|
||||||
@@ -281,6 +446,11 @@ internal static class SurrealStoreRecordMappers
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a SurrealDB snapshot metadata record to a domain model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="record">The SurrealDB snapshot metadata record.</param>
|
||||||
|
/// <returns>The domain snapshot metadata.</returns>
|
||||||
public static SnapshotMetadata ToDomain(this SurrealSnapshotMetadataRecord record)
|
public static SnapshotMetadata ToDomain(this SurrealSnapshotMetadataRecord record)
|
||||||
{
|
{
|
||||||
return new SnapshotMetadata
|
return new SnapshotMetadata
|
||||||
|
|||||||
360
surreal.md
Normal file
360
surreal.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# BLite -> SurrealDB (Embedded + RocksDB) Migration Plan
|
||||||
|
|
||||||
|
## 1) Goal and Scope
|
||||||
|
|
||||||
|
Replace all BLite-backed persistence in this repository with SurrealDB embedded using RocksDB persistence, while preserving current CBDDC behavior:
|
||||||
|
|
||||||
|
1. Automatic CDC-driven oplog generation for local writes.
|
||||||
|
2. Reliable sync across peers (including reconnect and snapshot flows).
|
||||||
|
3. Existing storage contracts (`IDocumentStore`, `IOplogStore`, `IPeerConfigurationStore`, `IDocumentMetadataStore`, `ISnapshotMetadataStore`, `IPeerOplogConfirmationStore`) and test semantics.
|
||||||
|
4. Full removal of BLite dependencies, APIs, and documentation references.
|
||||||
|
|
||||||
|
## 2) Current-State Inventory (Repository-Specific)
|
||||||
|
|
||||||
|
Primary BLite implementation and integration points currently live in:
|
||||||
|
|
||||||
|
1. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/CBDDCBLiteExtensions.cs`
|
||||||
|
2. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/CBDDCDocumentDbContext.cs`
|
||||||
|
3. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLiteDocumentStore.cs`
|
||||||
|
4. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLiteOplogStore.cs`
|
||||||
|
5. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLiteDocumentMetadataStore.cs`
|
||||||
|
6. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLitePeerConfigurationStore.cs`
|
||||||
|
7. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLitePeerOplogConfirmationStore.cs`
|
||||||
|
8. `src/ZB.MOM.WW.CBDDC.Persistence/BLite/BLiteSnapshotMetadataStore.cs`
|
||||||
|
9. `samples/ZB.MOM.WW.CBDDC.Sample.Console/SampleDbContext.cs`
|
||||||
|
10. `samples/ZB.MOM.WW.CBDDC.Sample.Console/SampleDocumentStore.cs`
|
||||||
|
11. `samples/ZB.MOM.WW.CBDDC.Sample.Console/Program.cs`
|
||||||
|
12. `tests/ZB.MOM.WW.CBDDC.Sample.Console.Tests/*.cs` (BLite-focused tests)
|
||||||
|
13. `tests/ZB.MOM.WW.CBDDC.E2E.Tests/ClusterCrudSyncE2ETests.cs`
|
||||||
|
14. `src/ZB.MOM.WW.CBDDC.Persistence/ZB.MOM.WW.CBDDC.Persistence.csproj` and sample/test package references
|
||||||
|
15. `README.md` and related docs that currently describe BLite as the embedded provider.
|
||||||
|
|
||||||
|
## 3) Target Architecture
|
||||||
|
|
||||||
|
### 3.1 Provider Surface
|
||||||
|
|
||||||
|
Create a Surreal provider namespace and extension entrypoint that mirrors current integration shape:
|
||||||
|
|
||||||
|
1. Add `AddCBDDCSurrealEmbedded<...>()` in a new file (e.g., `src/ZB.MOM.WW.CBDDC.Persistence/Surreal/CBDDCSurrealExtensions.cs`).
|
||||||
|
2. Register Surreal-backed implementations for all existing persistence interfaces.
|
||||||
|
3. Keep singleton lifetime for store services and Surreal client factory (equivalent to current BLite singleton model).
|
||||||
|
4. Expose options object including:
|
||||||
|
- RocksDB endpoint/path (`rocksdb://...`)
|
||||||
|
- Namespace
|
||||||
|
- Database
|
||||||
|
- CDC polling interval
|
||||||
|
- CDC batch size
|
||||||
|
- CDC retention duration
|
||||||
|
|
||||||
|
### 3.2 Surreal Connection and Embedded Startup
|
||||||
|
|
||||||
|
Use official embedded .NET guidance:
|
||||||
|
|
||||||
|
1. Add Surreal embedded packages.
|
||||||
|
2. Use `SurrealDbEmbeddedClient`/RocksDB embedded client with `rocksdb://` endpoint.
|
||||||
|
3. Run `USE NS <ns> DB <db>` at startup.
|
||||||
|
4. Dispose/close client on host shutdown.
|
||||||
|
|
||||||
|
### 3.3 Table Design (Schema + Indexing)
|
||||||
|
|
||||||
|
Define internal tables as `SCHEMAFULL` and strongly typed fields to reduce runtime drift.
|
||||||
|
|
||||||
|
Proposed tables:
|
||||||
|
|
||||||
|
1. `oplog_entries`
|
||||||
|
2. `snapshot_metadatas`
|
||||||
|
3. `remote_peer_configurations`
|
||||||
|
4. `document_metadatas`
|
||||||
|
5. `peer_oplog_confirmations`
|
||||||
|
6. `cdc_checkpoints` (new: durable cursor per watched table)
|
||||||
|
7. Optional: `cdc_dedup` (new: idempotency window for duplicate/overlapping reads)
|
||||||
|
|
||||||
|
Indexes and IDs:
|
||||||
|
|
||||||
|
1. Prefer deterministic record IDs for point lookups (`table:id`) where possible.
|
||||||
|
2. Add unique indexes for business keys currently enforced in BLite:
|
||||||
|
- `oplog_entries.hash`
|
||||||
|
- `snapshot_metadatas.node_id`
|
||||||
|
- `(document_metadatas.collection, document_metadatas.key)`
|
||||||
|
- `(peer_oplog_confirmations.peer_node_id, peer_oplog_confirmations.source_node_id)`
|
||||||
|
3. Add composite indexes for hot sync queries:
|
||||||
|
- Oplog by `(timestamp_physical, timestamp_logical)`
|
||||||
|
- Oplog by `(timestamp_node_id, timestamp_physical, timestamp_logical)`
|
||||||
|
- Metadata by `(hlc_physical, hlc_logical)`
|
||||||
|
4. Use `EXPLAIN FULL` during test/benchmark phase to verify index usage.
|
||||||
|
|
||||||
|
### 3.4 CDC Strategy (Durable + Low Latency)
|
||||||
|
|
||||||
|
Implement CDC with Surreal Change Feeds as source of truth and Live Queries as optional accelerators.
|
||||||
|
|
||||||
|
1. Enable `CHANGEFEED <duration>` per watched table (`INCLUDE ORIGINAL` when old values are required for conflict handling/debug).
|
||||||
|
2. Persist checkpoint cursor (`versionstamp` preferred) in `cdc_checkpoints`.
|
||||||
|
3. Poll with `SHOW CHANGES FOR TABLE <table> SINCE <cursor> LIMIT <N>`.
|
||||||
|
4. Process changes idempotently; tolerate duplicate windows when timestamp cursors overlap.
|
||||||
|
5. Commit checkpoint only after oplog + metadata writes commit successfully.
|
||||||
|
6. Optionally run `LIVE SELECT` subscribers for lower-latency wakeups, but never rely on live events alone for durability.
|
||||||
|
7. On startup/reconnect, always catch up via `SHOW CHANGES` from last persisted cursor.
|
||||||
|
|
||||||
|
### 3.5 Transaction Boundaries
|
||||||
|
|
||||||
|
Use explicit SurrealQL transactions for atomic state transitions:
|
||||||
|
|
||||||
|
1. Local CDC event -> write oplog entry + document metadata + vector clock backing data in one transaction.
|
||||||
|
2. Remote apply batch -> apply documents + merge oplog + metadata updates atomically in bounded batches.
|
||||||
|
3. Snapshot replace/merge -> table-level clear/import or merge in deterministic order with rollback on failure.
|
||||||
|
|
||||||
|
## 4) Execution Plan (Phased)
|
||||||
|
|
||||||
|
## Phase 0: Design Freeze and Safety Rails
|
||||||
|
|
||||||
|
1. Finalize data model and table schema DDL.
|
||||||
|
2. Finalize CDC cursor semantics (`versionstamp` vs timestamp fallback).
|
||||||
|
3. Freeze shared contracts in `ZB.MOM.WW.CBDDC.Core` (no signature churn during provider port).
|
||||||
|
4. Add migration feature flag for temporary cutover control (`UseSurrealPersistence`), removed in final cleanup.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Design doc approved.
|
||||||
|
2. DDL + index plan reviewed.
|
||||||
|
3. CDC retention value chosen (must exceed maximum offline peer window).
|
||||||
|
|
||||||
|
## Phase 1: Surreal Infrastructure Layer
|
||||||
|
|
||||||
|
1. Add Surreal packages and connection factory.
|
||||||
|
2. Implement startup initialization: NS/DB selection, table/index creation, capability checks.
|
||||||
|
3. Introduce provider options and DI extension (`AddCBDDCSurrealEmbedded`).
|
||||||
|
4. Add health probe for embedded connection and schema readiness.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. `dotnet build` succeeds.
|
||||||
|
2. Basic smoke test can connect, create, read, and delete records in RocksDB-backed embedded Surreal.
|
||||||
|
|
||||||
|
## Phase 2: Port Store Implementations
|
||||||
|
|
||||||
|
Port each BLite store to Surreal while preserving interface behavior:
|
||||||
|
|
||||||
|
1. `BLiteOplogStore` -> `SurrealOplogStore`
|
||||||
|
2. `BLiteDocumentMetadataStore` -> `SurrealDocumentMetadataStore`
|
||||||
|
3. `BLitePeerConfigurationStore` -> `SurrealPeerConfigurationStore`
|
||||||
|
4. `BLitePeerOplogConfirmationStore` -> `SurrealPeerOplogConfirmationStore`
|
||||||
|
5. `BLiteSnapshotMetadataStore` -> `SurrealSnapshotMetadataStore`
|
||||||
|
|
||||||
|
Implementation requirements:
|
||||||
|
|
||||||
|
1. Keep existing merge/drop/export/import semantics.
|
||||||
|
2. Preserve ordering guarantees for hash-chain methods.
|
||||||
|
3. Preserve vector clock bootstrap behavior (snapshot metadata first, oplog second).
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Store-level unit tests pass with Surreal backend.
|
||||||
|
2. No BLite store classes used in DI path.
|
||||||
|
|
||||||
|
## Phase 3: Document Store + CDC Engine
|
||||||
|
|
||||||
|
1. Replace `BLiteDocumentStore<TDbContext>` with Surreal-aware document store base.
|
||||||
|
2. Implement collection registration + watched table catalog.
|
||||||
|
3. Implement CDC worker:
|
||||||
|
- Poll `SHOW CHANGES`
|
||||||
|
- Map CDC events to `OperationType`
|
||||||
|
- Generate oplog + metadata
|
||||||
|
- Enforce remote-sync suppression/idempotency
|
||||||
|
4. Keep equivalent remote apply guard semantics to prevent CDC loopback during sync replay.
|
||||||
|
5. Add graceful start/stop lifecycle hooks for CDC worker.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Local direct writes produce expected oplog entries.
|
||||||
|
2. Remote replay does not create duplicate local oplog entries.
|
||||||
|
3. Restart resumes CDC from persisted checkpoint without missing changes.
|
||||||
|
|
||||||
|
## Phase 4: Sample App and E2E Harness Migration
|
||||||
|
|
||||||
|
1. Replace sample BLite context usage with Surreal-backed sample persistence.
|
||||||
|
2. Replace `AddCBDDCBLite` usage in sample and tests.
|
||||||
|
3. Update `ClusterCrudSyncE2ETests` internals that currently access BLite collections directly.
|
||||||
|
4. Refactor fallback CDC assertion logic to Surreal-based observability hooks.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Sample runs two-node sync with Surreal embedded RocksDB.
|
||||||
|
2. E2E CRUD bidirectional test passes unchanged in behavior.
|
||||||
|
|
||||||
|
## Phase 5: Data Migration Tooling and Cutover
|
||||||
|
|
||||||
|
1. Build one-time migration utility:
|
||||||
|
- Read BLite data via existing stores
|
||||||
|
- Write to Surreal tables
|
||||||
|
- Preserve hashes/timestamps exactly
|
||||||
|
2. Add verification routine comparing counts, hashes, and key spot checks.
|
||||||
|
3. Document migration command and rollback artifacts.
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Dry-run migration succeeds on fixture DB.
|
||||||
|
2. Post-migration parity checks are clean.
|
||||||
|
|
||||||
|
## Phase 6: Remove BLite Completely
|
||||||
|
|
||||||
|
1. Delete `src/ZB.MOM.WW.CBDDC.Persistence/BLite/*` after Surreal parity is proven.
|
||||||
|
2. Remove BLite package references and BLite source generators from project files.
|
||||||
|
3. Remove `.blite` path assumptions from sample/tests/docs.
|
||||||
|
4. Update docs and READMEs to SurrealDB terminology.
|
||||||
|
5. Ensure `rg -n "BLite|blite|AddCBDDCBLite|CBDDCDocumentDbContext"` returns no functional references (except historical notes if intentionally retained).
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
1. Solution builds/tests pass with zero BLite runtime dependency.
|
||||||
|
2. Docs reflect Surreal-only provider path.
|
||||||
|
|
||||||
|
## 5) Safe Parallel Subagent Plan
|
||||||
|
|
||||||
|
Use parallel subagents only with strict ownership boundaries and integration gates.
|
||||||
|
|
||||||
|
## 5.1 Subagent Work Split
|
||||||
|
|
||||||
|
1. Subagent A (Infrastructure/DI)
|
||||||
|
- Owns: new Surreal options, connection factory, DI extension, startup schema init.
|
||||||
|
- Files: new `src/.../Surreal/*` infra files, `*.csproj` package refs.
|
||||||
|
|
||||||
|
2. Subagent B (Core Stores)
|
||||||
|
- Owns: oplog/document metadata/snapshot metadata/peer config/peer confirmation Surreal stores.
|
||||||
|
- Files: `src/ZB.MOM.WW.CBDDC.Persistence/Surreal/*Store.cs`.
|
||||||
|
|
||||||
|
3. Subagent C (CDC + DocumentStore)
|
||||||
|
- Owns: Surreal document store base, CDC poller, checkpoint persistence, suppression loop prevention.
|
||||||
|
- Files: `src/ZB.MOM.WW.CBDDC.Persistence/Surreal/*DocumentStore*`, CDC worker files.
|
||||||
|
|
||||||
|
4. Subagent D (Tests)
|
||||||
|
- Owns: unit/integration/E2E tests migrated to Surreal.
|
||||||
|
- Files: `tests/*` touched by provider swap.
|
||||||
|
|
||||||
|
5. Subagent E (Sample + Docs)
|
||||||
|
- Owns: sample console migration and doc rewrites.
|
||||||
|
- Files: `samples/*`, `README.md`, `docs/*` provider docs.
|
||||||
|
|
||||||
|
## 5.2 Parallel Safety Rules
|
||||||
|
|
||||||
|
1. No overlapping file ownership between active subagents.
|
||||||
|
2. Shared contract files are locked unless explicitly assigned to one subagent.
|
||||||
|
3. Each subagent must submit:
|
||||||
|
- changed file list
|
||||||
|
- rationale
|
||||||
|
- commands run
|
||||||
|
- test evidence
|
||||||
|
4. Integrator rebases/merges sequentially, never blindly squashing conflicting edits.
|
||||||
|
5. If a subagent encounters unrelated dirty changes, it must stop and escalate before editing.
|
||||||
|
|
||||||
|
## 5.3 Integration Order
|
||||||
|
|
||||||
|
1. Merge A -> B -> C -> D -> E.
|
||||||
|
2. Run full verification after each merge step, not only at the end.
|
||||||
|
|
||||||
|
## 6) Required Unit/Integration Test Matrix
|
||||||
|
|
||||||
|
## 6.1 Store Contract Tests
|
||||||
|
|
||||||
|
1. Oplog append/export/import/merge/drop parity.
|
||||||
|
2. `GetChainRangeAsync` correctness by hash chain ordering.
|
||||||
|
3. `GetLastEntryHashAsync` behavior with oplog hit and snapshot fallback.
|
||||||
|
4. Pruning respects cutoff and confirmations.
|
||||||
|
5. Document metadata upsert/mark-deleted/get-after ordering.
|
||||||
|
6. Peer config save/get/remove/merge semantics.
|
||||||
|
7. Peer confirmation registration/update/deactivate/merge semantics.
|
||||||
|
8. Snapshot metadata insert/update/merge and hash lookup.
|
||||||
|
|
||||||
|
## 6.2 CDC Tests
|
||||||
|
|
||||||
|
1. Local write on watched table emits exactly one oplog entry.
|
||||||
|
2. Delete mutation emits delete oplog + metadata tombstone.
|
||||||
|
3. Remote apply path does not re-emit local CDC oplog entries.
|
||||||
|
4. CDC checkpoint persists only after atomic write success.
|
||||||
|
5. Restart from checkpoint catches missed changes.
|
||||||
|
6. Duplicate window replay is idempotent.
|
||||||
|
7. Changefeed retention boundary behavior is explicit and logged.
|
||||||
|
|
||||||
|
## 6.3 Snapshot and Recovery Tests
|
||||||
|
|
||||||
|
1. `CreateSnapshotAsync` includes docs/oplog/peers/confirmations.
|
||||||
|
2. `ReplaceDatabaseAsync` restores full state.
|
||||||
|
3. `MergeSnapshotAsync` conflict behavior unchanged.
|
||||||
|
4. Recovery after process restart retains Surreal RocksDB data.
|
||||||
|
|
||||||
|
## 6.4 E2E Sync Tests
|
||||||
|
|
||||||
|
1. Two peers replicate create/update/delete bidirectionally.
|
||||||
|
2. Peer reconnect performs incremental catch-up from CDC cursor.
|
||||||
|
3. Multi-change burst preserves deterministic final state.
|
||||||
|
4. Optional fault-injection test: crash between oplog write and checkpoint update should replay safely on restart.
|
||||||
|
|
||||||
|
## 7) Verification After Each Subagent Completion
|
||||||
|
|
||||||
|
Run this checklist after each merged subagent contribution:
|
||||||
|
|
||||||
|
1. `dotnet restore`
|
||||||
|
2. `dotnet build CBDDC.slnx -c Release`
|
||||||
|
3. Targeted tests for modified projects (fast gate)
|
||||||
|
4. Full test suite before moving to next major phase:
|
||||||
|
- `dotnet test CBDDC.slnx -c Release`
|
||||||
|
5. Regression grep checks:
|
||||||
|
- `rg -n "BLite|AddCBDDCBLite|\.blite|CBDDCDocumentDbContext" src samples tests README.md docs`
|
||||||
|
6. Surreal smoke test:
|
||||||
|
- create temp RocksDB path
|
||||||
|
- start sample node
|
||||||
|
- perform write/update/delete
|
||||||
|
- restart process and verify persisted state
|
||||||
|
7. CDC durability test:
|
||||||
|
- stop node
|
||||||
|
- mutate source
|
||||||
|
- restart node
|
||||||
|
- confirm catch-up via `SHOW CHANGES` cursor
|
||||||
|
|
||||||
|
## 8) Rollout and Rollback
|
||||||
|
|
||||||
|
## Rollout
|
||||||
|
|
||||||
|
1. Internal canary branch with Surreal-only provider.
|
||||||
|
2. Run full CI + extended E2E soak (long-running sync/reconnect).
|
||||||
|
3. Migrate one test dataset from BLite to Surreal and validate parity.
|
||||||
|
4. Promote after acceptance criteria are met.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
1. Keep BLite export snapshots until Surreal cutover is accepted.
|
||||||
|
2. If severe defect appears, restore from pre-cutover snapshot and redeploy previous BLite-tagged build.
|
||||||
|
3. Preserve migration logs and parity reports for audit.
|
||||||
|
|
||||||
|
## 9) Definition of Done
|
||||||
|
|
||||||
|
1. No runtime BLite dependency remains.
|
||||||
|
2. All store contracts pass with Surreal backend.
|
||||||
|
3. CDC is durable (checkpointed), idempotent, and restart-safe.
|
||||||
|
4. Sample + E2E prove sync parity.
|
||||||
|
5. Documentation and onboarding instructions updated to Surreal embedded RocksDB.
|
||||||
|
6. Migration utility + validation report available for production cutover.
|
||||||
|
|
||||||
|
## 10) SurrealDB Best-Practice Notes Applied in This Plan
|
||||||
|
|
||||||
|
This plan explicitly applies official Surreal guidance:
|
||||||
|
|
||||||
|
1. Embedded .NET with RocksDB endpoint (`rocksdb://`) and explicit NS/DB usage.
|
||||||
|
2. Schema-first design with strict table/field definitions and typed record references.
|
||||||
|
3. Query/index discipline (`EXPLAIN FULL`, indexed lookups, avoid broad scans).
|
||||||
|
4. CDC durability with changefeeds and checkpointed `SHOW CHANGES` replay.
|
||||||
|
5. Live queries used as low-latency signals, not as sole durable CDC transport.
|
||||||
|
6. Security hardening (authentication, encryption/backups, restricted capabilities) for any non-embedded server deployments used in tooling/CI.
|
||||||
|
|
||||||
|
## References (Primary Sources)
|
||||||
|
|
||||||
|
1. SurrealDB .NET embedded engine docs: [https://surrealdb.com/docs/surrealdb/embedding/dotnet](https://surrealdb.com/docs/surrealdb/embedding/dotnet)
|
||||||
|
2. SurrealDB .NET SDK embedding guide: [https://surrealdb.com/docs/sdk/dotnet/embedding](https://surrealdb.com/docs/sdk/dotnet/embedding)
|
||||||
|
3. SurrealDB connection strings (protocol formats incl. RocksDB): [https://surrealdb.com/docs/surrealdb/reference-guide/connection-strings](https://surrealdb.com/docs/surrealdb/reference-guide/connection-strings)
|
||||||
|
4. SurrealDB schema best practices: [https://surrealdb.com/docs/surrealdb/reference-guide/schema-creation-best-practices](https://surrealdb.com/docs/surrealdb/reference-guide/schema-creation-best-practices)
|
||||||
|
5. SurrealDB performance best practices: [https://surrealdb.com/docs/surrealdb/reference-guide/performance-best-practices](https://surrealdb.com/docs/surrealdb/reference-guide/performance-best-practices)
|
||||||
|
6. SurrealDB real-time/events best practices: [https://surrealdb.com/docs/surrealdb/reference-guide/realtime-best-practices](https://surrealdb.com/docs/surrealdb/reference-guide/realtime-best-practices)
|
||||||
|
7. SurrealQL `DEFINE TABLE` (changefeed options): [https://surrealdb.com/docs/surrealql/statements/define/table](https://surrealdb.com/docs/surrealql/statements/define/table)
|
||||||
|
8. SurrealQL `SHOW CHANGES` (durable CDC read): [https://surrealdb.com/docs/surrealql/statements/show](https://surrealdb.com/docs/surrealql/statements/show)
|
||||||
|
9. SurrealQL `LIVE SELECT` behavior and caveats: [https://surrealdb.com/docs/surrealql/statements/live](https://surrealdb.com/docs/surrealql/statements/live)
|
||||||
|
10. SurrealDB security best practices: [https://surrealdb.com/docs/surrealdb/security/security-best-practices](https://surrealdb.com/docs/surrealdb/security/security-best-practices)
|
||||||
|
11. SurrealQL transactions (`BEGIN`/`COMMIT`): [https://surrealdb.com/docs/surrealql/statements/begin](https://surrealdb.com/docs/surrealql/statements/begin), [https://surrealdb.com/docs/surrealql/statements/commit](https://surrealdb.com/docs/surrealql/statements/commit)
|
||||||
87
tests/ZB.MOM.WW.CBDDC.Core.Tests/DocumentCacheTests.cs
Normal file
87
tests/ZB.MOM.WW.CBDDC.Core.Tests/DocumentCacheTests.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Cache;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||||
|
|
||||||
|
public class DocumentCacheTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies cache hit/miss statistics after get and set operations.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAndSet_ShouldTrackCacheHitsAndMisses()
|
||||||
|
{
|
||||||
|
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 2));
|
||||||
|
|
||||||
|
Document? missing = await cache.Get("users", "1");
|
||||||
|
missing.ShouldBeNull();
|
||||||
|
|
||||||
|
var document = CreateDocument("users", "1");
|
||||||
|
await cache.Set("users", "1", document);
|
||||||
|
Document? hit = await cache.Get("users", "1");
|
||||||
|
|
||||||
|
hit.ShouldNotBeNull();
|
||||||
|
hit.Key.ShouldBe("1");
|
||||||
|
var stats = cache.GetStatistics();
|
||||||
|
stats.Hits.ShouldBe(1);
|
||||||
|
stats.Misses.ShouldBe(1);
|
||||||
|
stats.Size.ShouldBe(1);
|
||||||
|
stats.HitRate.ShouldBe(0.5d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies least-recently-used eviction when cache capacity is reached.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Set_WhenCacheIsFull_EvictsLeastRecentlyUsedEntry()
|
||||||
|
{
|
||||||
|
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 2));
|
||||||
|
await cache.Set("users", "1", CreateDocument("users", "1"));
|
||||||
|
await cache.Set("users", "2", CreateDocument("users", "2"));
|
||||||
|
|
||||||
|
// Touch key 1 so key 2 becomes the LRU entry.
|
||||||
|
(await cache.Get("users", "1")).ShouldNotBeNull();
|
||||||
|
|
||||||
|
await cache.Set("users", "3", CreateDocument("users", "3"));
|
||||||
|
|
||||||
|
(await cache.Get("users", "2")).ShouldBeNull();
|
||||||
|
(await cache.Get("users", "1")).ShouldNotBeNull();
|
||||||
|
(await cache.Get("users", "3")).ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies remove and clear operations delete entries from the cache.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task RemoveAndClear_ShouldDeleteEntriesFromCache()
|
||||||
|
{
|
||||||
|
var cache = new DocumentCache(CreateConfigProvider(maxDocumentCacheSize: 3));
|
||||||
|
await cache.Set("users", "1", CreateDocument("users", "1"));
|
||||||
|
await cache.Set("users", "2", CreateDocument("users", "2"));
|
||||||
|
cache.GetStatistics().Size.ShouldBe(2);
|
||||||
|
|
||||||
|
cache.Remove("users", "1");
|
||||||
|
(await cache.Get("users", "1")).ShouldBeNull();
|
||||||
|
cache.GetStatistics().Size.ShouldBe(1);
|
||||||
|
|
||||||
|
cache.Clear();
|
||||||
|
cache.GetStatistics().Size.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Document CreateDocument(string collection, string key)
|
||||||
|
{
|
||||||
|
using var json = JsonDocument.Parse("""{"name":"test"}""");
|
||||||
|
return new Document(collection, key, json.RootElement.Clone(), new HlcTimestamp(1, 0, "node-a"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPeerNodeConfigurationProvider CreateConfigProvider(int maxDocumentCacheSize)
|
||||||
|
{
|
||||||
|
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||||
|
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||||
|
{
|
||||||
|
MaxDocumentCacheSize = maxDocumentCacheSize
|
||||||
|
});
|
||||||
|
return configProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
tests/ZB.MOM.WW.CBDDC.Core.Tests/OfflineQueueTests.cs
Normal file
92
tests/ZB.MOM.WW.CBDDC.Core.Tests/OfflineQueueTests.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Sync;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||||
|
|
||||||
|
public class OfflineQueueTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that enqueuing beyond capacity drops the oldest operation.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Enqueue_WhenQueueIsFull_DropsOldestOperation()
|
||||||
|
{
|
||||||
|
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 2));
|
||||||
|
await queue.Enqueue(CreateOperation("1"));
|
||||||
|
await queue.Enqueue(CreateOperation("2"));
|
||||||
|
await queue.Enqueue(CreateOperation("3"));
|
||||||
|
|
||||||
|
var flushed = new List<string>();
|
||||||
|
(int successful, int failed) = await queue.FlushAsync(op =>
|
||||||
|
{
|
||||||
|
flushed.Add(op.Key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
successful.ShouldBe(2);
|
||||||
|
failed.ShouldBe(0);
|
||||||
|
flushed.ShouldBe(["2", "3"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that flush continues when an executor throws and returns the failure count.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task FlushAsync_WhenExecutorThrows_ContinuesAndReturnsFailureCount()
|
||||||
|
{
|
||||||
|
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 10));
|
||||||
|
await queue.Enqueue(CreateOperation("1"));
|
||||||
|
await queue.Enqueue(CreateOperation("2"));
|
||||||
|
|
||||||
|
(int successful, int failed) = await queue.FlushAsync(op =>
|
||||||
|
{
|
||||||
|
if (op.Key == "1") throw new InvalidOperationException("boom");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
successful.ShouldBe(1);
|
||||||
|
failed.ShouldBe(1);
|
||||||
|
queue.Count.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that clear removes all queued operations.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Clear_RemovesAllQueuedOperations()
|
||||||
|
{
|
||||||
|
var queue = new OfflineQueue(CreateConfigProvider(maxQueueSize: 10));
|
||||||
|
await queue.Enqueue(CreateOperation("1"));
|
||||||
|
await queue.Enqueue(CreateOperation("2"));
|
||||||
|
queue.Count.ShouldBe(2);
|
||||||
|
|
||||||
|
await queue.Clear();
|
||||||
|
|
||||||
|
queue.Count.ShouldBe(0);
|
||||||
|
(int successful, int failed) = await queue.FlushAsync(_ => Task.CompletedTask);
|
||||||
|
successful.ShouldBe(0);
|
||||||
|
failed.ShouldBe(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingOperation CreateOperation(string key)
|
||||||
|
{
|
||||||
|
return new PendingOperation
|
||||||
|
{
|
||||||
|
Type = "upsert",
|
||||||
|
Collection = "users",
|
||||||
|
Key = key,
|
||||||
|
Data = new { Value = key },
|
||||||
|
QueuedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPeerNodeConfigurationProvider CreateConfigProvider(int maxQueueSize)
|
||||||
|
{
|
||||||
|
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||||
|
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||||
|
{
|
||||||
|
MaxQueueSize = maxQueueSize
|
||||||
|
});
|
||||||
|
return configProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
78
tests/ZB.MOM.WW.CBDDC.Core.Tests/RetryPolicyTests.cs
Normal file
78
tests/ZB.MOM.WW.CBDDC.Core.Tests/RetryPolicyTests.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using ZB.MOM.WW.CBDDC.Core.Exceptions;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Network;
|
||||||
|
using ZB.MOM.WW.CBDDC.Core.Resilience;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Core.Tests;
|
||||||
|
|
||||||
|
public class RetryPolicyTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies transient failures are retried until a successful result is returned.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_WhenTransientFailureEventuallySucceeds_RetriesAndReturnsResult()
|
||||||
|
{
|
||||||
|
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1));
|
||||||
|
var attempts = 0;
|
||||||
|
|
||||||
|
int result = await policy.ExecuteAsync(async () =>
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
if (attempts < 3) throw new NetworkException("transient");
|
||||||
|
await Task.CompletedTask;
|
||||||
|
return 42;
|
||||||
|
}, "test-op");
|
||||||
|
|
||||||
|
result.ShouldBe(42);
|
||||||
|
attempts.ShouldBe(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies transient failures throw retry exhausted when all retries are consumed.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_WhenTransientFailureExhausted_ThrowsRetryExhaustedException()
|
||||||
|
{
|
||||||
|
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 2, retryDelayMs: 1));
|
||||||
|
var attempts = 0;
|
||||||
|
|
||||||
|
var ex = await Should.ThrowAsync<CBDDCException>(() => policy.ExecuteAsync<int>(() =>
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
throw new NetworkException("still transient");
|
||||||
|
}, "test-op"));
|
||||||
|
|
||||||
|
ex.ErrorCode.ShouldBe("RETRY_EXHAUSTED");
|
||||||
|
ex.InnerException.ShouldBeOfType<NetworkException>();
|
||||||
|
attempts.ShouldBe(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies non-transient failures are not retried.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteAsync_WhenFailureIsNonTransient_DoesNotRetry()
|
||||||
|
{
|
||||||
|
var policy = new RetryPolicy(CreateConfigProvider(retryAttempts: 3, retryDelayMs: 1));
|
||||||
|
var attempts = 0;
|
||||||
|
|
||||||
|
await Should.ThrowAsync<InvalidOperationException>(() => policy.ExecuteAsync<int>(() =>
|
||||||
|
{
|
||||||
|
attempts++;
|
||||||
|
throw new InvalidOperationException("non-transient");
|
||||||
|
}, "test-op"));
|
||||||
|
|
||||||
|
attempts.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IPeerNodeConfigurationProvider CreateConfigProvider(int retryAttempts, int retryDelayMs)
|
||||||
|
{
|
||||||
|
var configProvider = Substitute.For<IPeerNodeConfigurationProvider>();
|
||||||
|
configProvider.GetConfiguration().Returns(new PeerNodeConfiguration
|
||||||
|
{
|
||||||
|
RetryAttempts = retryAttempts,
|
||||||
|
RetryDelayMs = retryDelayMs
|
||||||
|
});
|
||||||
|
return configProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -428,12 +428,12 @@ public class ClusterCrudSyncE2ETests
|
|||||||
&& replicated.Address?.City == payload.Address?.City;
|
&& replicated.Address?.City == payload.Address?.City;
|
||||||
}, 60, "Node B did not converge after crash-window recovery.", () => BuildDiagnostics(recoveredNodeA, nodeB));
|
}, 60, "Node B did not converge after crash-window recovery.", () => BuildDiagnostics(recoveredNodeA, nodeB));
|
||||||
|
|
||||||
await AssertEventuallyAsync(
|
await AssertEventuallyAsync(
|
||||||
() => recoveredNodeA.GetOplogCountForKey("Users", userId) == 1 &&
|
() => recoveredNodeA.GetOplogCountForKey("Users", userId) == 1 &&
|
||||||
nodeB.GetOplogCountForKey("Users", userId) == 1,
|
nodeB.GetOplogCountForKey("Users", userId) == 1,
|
||||||
60,
|
60,
|
||||||
"Crash-window recovery created duplicate oplog entries.",
|
"Crash-window recovery created duplicate oplog entries.",
|
||||||
() => BuildDiagnostics(recoveredNodeA, nodeB));
|
() => BuildDiagnostics(recoveredNodeA, nodeB));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -569,6 +569,9 @@ public class ClusterCrudSyncE2ETests
|
|||||||
/// <param name="tcpPort">The TCP port used by the node listener.</param>
|
/// <param name="tcpPort">The TCP port used by the node listener.</param>
|
||||||
/// <param name="authToken">The cluster authentication token.</param>
|
/// <param name="authToken">The cluster authentication token.</param>
|
||||||
/// <param name="knownPeers">The known peers this node can connect to.</param>
|
/// <param name="knownPeers">The known peers this node can connect to.</param>
|
||||||
|
/// <param name="workDirOverride">An optional working directory override for test artifacts.</param>
|
||||||
|
/// <param name="preserveWorkDirOnDispose">A value indicating whether to preserve the working directory on dispose.</param>
|
||||||
|
/// <param name="useFaultInjectedCheckpointStore">A value indicating whether to inject a checkpoint persistence that fails once.</param>
|
||||||
/// <returns>A configured <see cref="TestPeerNode" /> instance.</returns>
|
/// <returns>A configured <see cref="TestPeerNode" /> instance.</returns>
|
||||||
public static TestPeerNode Create(
|
public static TestPeerNode Create(
|
||||||
string nodeId,
|
string nodeId,
|
||||||
@@ -690,6 +693,12 @@ public class ClusterCrudSyncE2ETests
|
|||||||
return Context.Users.Find(u => u.Id == userId).FirstOrDefault();
|
return Context.Users.Find(u => u.Id == userId).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the local oplog entry count for a collection/key pair produced by this node.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <returns>The number of local oplog entries matching the key.</returns>
|
||||||
public int GetLocalOplogCountForKey(string collection, string key)
|
public int GetLocalOplogCountForKey(string collection, string key)
|
||||||
{
|
{
|
||||||
return Context.OplogEntries.FindAll()
|
return Context.OplogEntries.FindAll()
|
||||||
@@ -699,6 +708,12 @@ public class ClusterCrudSyncE2ETests
|
|||||||
string.Equals(e.TimestampNodeId, _nodeId, StringComparison.Ordinal));
|
string.Equals(e.TimestampNodeId, _nodeId, StringComparison.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total oplog entry count for a collection/key pair across nodes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <returns>The number of oplog entries matching the key.</returns>
|
||||||
public int GetOplogCountForKey(string collection, string key)
|
public int GetOplogCountForKey(string collection, string key)
|
||||||
{
|
{
|
||||||
return Context.OplogEntries.FindAll()
|
return Context.OplogEntries.FindAll()
|
||||||
@@ -824,6 +839,14 @@ public class ClusterCrudSyncE2ETests
|
|||||||
private const string UsersCollection = "Users";
|
private const string UsersCollection = "Users";
|
||||||
private const string TodoListsCollection = "TodoLists";
|
private const string TodoListsCollection = "TodoLists";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FaultInjectedSampleDocumentStore" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The sample database context.</param>
|
||||||
|
/// <param name="configProvider">The peer node configuration provider.</param>
|
||||||
|
/// <param name="vectorClockService">The vector clock service.</param>
|
||||||
|
/// <param name="checkpointPersistence">The checkpoint persistence implementation.</param>
|
||||||
|
/// <param name="logger">The optional logger instance.</param>
|
||||||
public FaultInjectedSampleDocumentStore(
|
public FaultInjectedSampleDocumentStore(
|
||||||
SampleDbContext context,
|
SampleDbContext context,
|
||||||
IPeerNodeConfigurationProvider configProvider,
|
IPeerNodeConfigurationProvider configProvider,
|
||||||
@@ -849,6 +872,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntityAsync(
|
protected override async Task ApplyContentToEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -858,6 +882,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntitiesBatchAsync(
|
protected override async Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -866,6 +891,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -879,6 +905,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntityAsync(
|
protected override async Task RemoveEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -887,6 +914,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntitiesBatchAsync(
|
protected override async Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents,
|
IEnumerable<(string Collection, string Key)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -895,6 +923,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -967,6 +996,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
{
|
{
|
||||||
private int _failOnNextAdvance = 1;
|
private int _failOnNextAdvance = 1;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<SurrealCdcCheckpoint?> GetCheckpointAsync(
|
public Task<SurrealCdcCheckpoint?> GetCheckpointAsync(
|
||||||
string? consumerId = null,
|
string? consumerId = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -974,6 +1004,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
return Task.FromResult<SurrealCdcCheckpoint?>(null);
|
return Task.FromResult<SurrealCdcCheckpoint?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task UpsertCheckpointAsync(
|
public Task UpsertCheckpointAsync(
|
||||||
HlcTimestamp timestamp,
|
HlcTimestamp timestamp,
|
||||||
string lastHash,
|
string lastHash,
|
||||||
@@ -984,6 +1015,7 @@ public class ClusterCrudSyncE2ETests
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task AdvanceCheckpointAsync(
|
public Task AdvanceCheckpointAsync(
|
||||||
OplogEntry entry,
|
OplogEntry entry,
|
||||||
string? consumerId = null,
|
string? consumerId = null,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
|
|||||||
[Collection("SurrealCdcDurability")]
|
[Collection("SurrealCdcDurability")]
|
||||||
public class SurrealCdcDurabilityTests
|
public class SurrealCdcDurabilityTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies checkpoints persist latest local changes per consumer across restarts.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CheckpointPersistence_ShouldTrackLatestLocalChange_AndPersistPerConsumer()
|
public async Task CheckpointPersistence_ShouldTrackLatestLocalChange_AndPersistPerConsumer()
|
||||||
{
|
{
|
||||||
@@ -85,6 +88,9 @@ public class SurrealCdcDurabilityTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies recovery resumes from a persisted checkpoint and advances after catch-up.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RestartRecovery_ShouldResumeCatchUpFromPersistedCheckpoint_InRocksDb()
|
public async Task RestartRecovery_ShouldResumeCatchUpFromPersistedCheckpoint_InRocksDb()
|
||||||
{
|
{
|
||||||
@@ -153,6 +159,9 @@ public class SurrealCdcDurabilityTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies duplicate remote apply windows are idempotent without loopback entries.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RemoteApply_ShouldBeIdempotentAcrossDuplicateWindow_WithoutLoopbackEntries()
|
public async Task RemoteApply_ShouldBeIdempotentAcrossDuplicateWindow_WithoutLoopbackEntries()
|
||||||
{
|
{
|
||||||
@@ -207,6 +216,9 @@ public class SurrealCdcDurabilityTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies local deletes persist tombstone metadata and advance checkpoints.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LocalDelete_ShouldPersistTombstoneMetadata_AndAdvanceCheckpoint()
|
public async Task LocalDelete_ShouldPersistTombstoneMetadata_AndAdvanceCheckpoint()
|
||||||
{
|
{
|
||||||
@@ -358,21 +370,46 @@ internal sealed class CdcTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealDocumentMetadataStore>.Instance);
|
NullLogger<SurrealDocumentMetadataStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the sample database context.
|
||||||
|
/// </summary>
|
||||||
public SampleDbContext Context { get; }
|
public SampleDbContext Context { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the checkpointed sample document store.
|
||||||
|
/// </summary>
|
||||||
public CheckpointedSampleDocumentStore DocumentStore { get; }
|
public CheckpointedSampleDocumentStore DocumentStore { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the oplog store used by the harness.
|
||||||
|
/// </summary>
|
||||||
public SurrealOplogStore OplogStore { get; }
|
public SurrealOplogStore OplogStore { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the document metadata store.
|
||||||
|
/// </summary>
|
||||||
public SurrealDocumentMetadataStore MetadataStore { get; }
|
public SurrealDocumentMetadataStore MetadataStore { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets checkpoint persistence used for CDC progress tracking.
|
||||||
|
/// </summary>
|
||||||
public ISurrealCdcCheckpointPersistence CheckpointPersistence { get; }
|
public ISurrealCdcCheckpointPersistence CheckpointPersistence { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Polls CDC once through the document store.
|
||||||
|
/// </summary>
|
||||||
public async Task PollAsync()
|
public async Task PollAsync()
|
||||||
{
|
{
|
||||||
await DocumentStore.PollCdcOnceAsync();
|
await DocumentStore.PollCdcOnceAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a harness instance with retries for transient RocksDB lock contention.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="databasePath">Database directory path.</param>
|
||||||
|
/// <param name="nodeId">Node identifier.</param>
|
||||||
|
/// <param name="consumerId">CDC consumer identifier.</param>
|
||||||
|
/// <returns>Initialized test harness.</returns>
|
||||||
public static async Task<CdcTestHarness> OpenWithRetriesAsync(
|
public static async Task<CdcTestHarness> OpenWithRetriesAsync(
|
||||||
string databasePath,
|
string databasePath,
|
||||||
string nodeId,
|
string nodeId,
|
||||||
@@ -391,6 +428,11 @@ internal sealed class CdcTestHarness : IAsyncDisposable
|
|||||||
throw new InvalidOperationException("Unable to acquire RocksDB lock for test harness.");
|
throw new InvalidOperationException("Unable to acquire RocksDB lock for test harness.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets oplog entries for a collection ordered by timestamp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">Collection name.</param>
|
||||||
|
/// <returns>Ordered oplog entries.</returns>
|
||||||
public async Task<List<OplogEntry>> GetEntriesByCollectionAsync(string collection)
|
public async Task<List<OplogEntry>> GetEntriesByCollectionAsync(string collection)
|
||||||
{
|
{
|
||||||
return (await OplogStore.ExportAsync())
|
return (await OplogStore.ExportAsync())
|
||||||
@@ -400,6 +442,12 @@ internal sealed class CdcTestHarness : IAsyncDisposable
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets oplog entries for a collection key ordered by timestamp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">Collection name.</param>
|
||||||
|
/// <param name="key">Document key.</param>
|
||||||
|
/// <returns>Ordered oplog entries.</returns>
|
||||||
public async Task<List<OplogEntry>> GetEntriesByKeyAsync(string collection, string key)
|
public async Task<List<OplogEntry>> GetEntriesByKeyAsync(string collection, string key)
|
||||||
{
|
{
|
||||||
return (await OplogStore.ExportAsync())
|
return (await OplogStore.ExportAsync())
|
||||||
@@ -410,6 +458,7 @@ internal sealed class CdcTestHarness : IAsyncDisposable
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
DocumentStore.Dispose();
|
DocumentStore.Dispose();
|
||||||
@@ -428,6 +477,15 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
private const string UsersCollection = "Users";
|
private const string UsersCollection = "Users";
|
||||||
private const string TodoListsCollection = "TodoLists";
|
private const string TodoListsCollection = "TodoLists";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CheckpointedSampleDocumentStore"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">Sample database context.</param>
|
||||||
|
/// <param name="configProvider">Peer configuration provider.</param>
|
||||||
|
/// <param name="vectorClockService">Vector clock service.</param>
|
||||||
|
/// <param name="checkpointPersistence">Checkpoint persistence implementation.</param>
|
||||||
|
/// <param name="surrealOptions">Optional Surreal embedded options.</param>
|
||||||
|
/// <param name="logger">Optional logger.</param>
|
||||||
public CheckpointedSampleDocumentStore(
|
public CheckpointedSampleDocumentStore(
|
||||||
SampleDbContext context,
|
SampleDbContext context,
|
||||||
IPeerNodeConfigurationProvider configProvider,
|
IPeerNodeConfigurationProvider configProvider,
|
||||||
@@ -450,6 +508,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id, subscribeForInMemoryEvents: false);
|
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id, subscribeForInMemoryEvents: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntityAsync(
|
protected override async Task ApplyContentToEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -459,6 +518,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ApplyContentToEntitiesBatchAsync(
|
protected override async Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -467,6 +527,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
await UpsertEntityAsync(collection, key, content, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -480,6 +541,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntityAsync(
|
protected override async Task RemoveEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -488,6 +550,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task RemoveEntitiesBatchAsync(
|
protected override async Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents,
|
IEnumerable<(string Collection, string Key)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -496,6 +559,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
|
|||||||
await DeleteEntityAsync(collection, key, cancellationToken);
|
await DeleteEntityAsync(collection, key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
|
|||||||
|
|
||||||
public class SurrealCdcMatrixCompletionTests
|
public class SurrealCdcMatrixCompletionTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies retention-boundary classifier behavior across expected exception message patterns.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The exception message sample.</param>
|
||||||
|
/// <param name="expected">Expected classifier outcome.</param>
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("versionstamp is outside the configured retention window", true)]
|
[InlineData("versionstamp is outside the configured retention window", true)]
|
||||||
[InlineData("change feed history since cursor is unavailable", true)]
|
[InlineData("change feed history since cursor is unavailable", true)]
|
||||||
@@ -29,6 +34,9 @@ public class SurrealCdcMatrixCompletionTests
|
|||||||
actual.ShouldBe(expected);
|
actual.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies a local write produces exactly one oplog entry.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LocalWrite_ShouldEmitExactlyOneOplogEntry()
|
public async Task LocalWrite_ShouldEmitExactlyOneOplogEntry()
|
||||||
{
|
{
|
||||||
@@ -63,6 +71,9 @@ public class SurrealCdcMatrixCompletionTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies checkpoint persistence does not advance when atomic write fails.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Checkpoint_ShouldNotAdvance_WhenAtomicWriteFails()
|
public async Task Checkpoint_ShouldNotAdvance_WhenAtomicWriteFails()
|
||||||
{
|
{
|
||||||
@@ -139,6 +150,14 @@ public class SurrealCdcMatrixCompletionTests
|
|||||||
|
|
||||||
internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object>
|
internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FailureInjectedDocumentStore" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="surrealEmbeddedClient">The embedded Surreal client provider.</param>
|
||||||
|
/// <param name="schemaInitializer">The schema initializer.</param>
|
||||||
|
/// <param name="configProvider">The node configuration provider.</param>
|
||||||
|
/// <param name="vectorClockService">The vector clock service.</param>
|
||||||
|
/// <param name="checkpointPersistence">The CDC checkpoint persistence dependency.</param>
|
||||||
public FailureInjectedDocumentStore(
|
public FailureInjectedDocumentStore(
|
||||||
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
|
||||||
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
ICBDDCSurrealSchemaInitializer schemaInitializer,
|
||||||
@@ -158,6 +177,15 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggers local change handling for testing failure scenarios.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="collection">The collection name.</param>
|
||||||
|
/// <param name="key">The document key.</param>
|
||||||
|
/// <param name="operationType">The operation type.</param>
|
||||||
|
/// <param name="content">Optional document content payload.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>A task that completes when processing is finished.</returns>
|
||||||
public Task TriggerLocalChangeAsync(
|
public Task TriggerLocalChangeAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -174,6 +202,7 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task ApplyContentToEntityAsync(
|
protected override Task ApplyContentToEntityAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -183,6 +212,7 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task ApplyContentToEntitiesBatchAsync(
|
protected override Task ApplyContentToEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -190,6 +220,7 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task<JsonElement?> GetEntityAsJsonAsync(
|
protected override Task<JsonElement?> GetEntityAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
string key,
|
string key,
|
||||||
@@ -198,11 +229,13 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
return Task.FromResult<JsonElement?>(null);
|
return Task.FromResult<JsonElement?>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task RemoveEntityAsync(string collection, string key, CancellationToken cancellationToken)
|
protected override Task RemoveEntityAsync(string collection, string key, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task RemoveEntitiesBatchAsync(
|
protected override Task RemoveEntitiesBatchAsync(
|
||||||
IEnumerable<(string Collection, string Key)> documents,
|
IEnumerable<(string Collection, string Key)> documents,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -210,6 +243,7 @@ internal sealed class FailureInjectedDocumentStore : SurrealDocumentStore<object
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
protected override Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
|
||||||
string collection,
|
string collection,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using SurrealDb.Net.Models.Response;
|
||||||
|
using ZB.MOM.WW.CBDDC.Persistence.Surreal;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
|
||||||
|
|
||||||
|
public class SurrealInfrastructureReadinessTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies schema initialization runs only once for repeated calls.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task SchemaInitializer_ShouldApplySchemaOnlyOnce()
|
||||||
|
{
|
||||||
|
var embeddedClient = Substitute.For<ICBDDCSurrealEmbeddedClient>();
|
||||||
|
embeddedClient.RawQueryAsync(
|
||||||
|
Arg.Any<string>(),
|
||||||
|
Arg.Any<IReadOnlyDictionary<string, object?>>(),
|
||||||
|
Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult(default(SurrealDbResponse)!));
|
||||||
|
|
||||||
|
var options = new CBDDCSurrealEmbeddedOptions
|
||||||
|
{
|
||||||
|
Cdc = new CBDDCSurrealCdcOptions
|
||||||
|
{
|
||||||
|
CheckpointTable = "custom_checkpoint",
|
||||||
|
RetentionDuration = TimeSpan.FromHours(12)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var initializer = new CBDDCSurrealSchemaInitializer(embeddedClient, options);
|
||||||
|
await initializer.EnsureInitializedAsync();
|
||||||
|
await initializer.EnsureInitializedAsync();
|
||||||
|
|
||||||
|
await embeddedClient.Received(1).RawQueryAsync(
|
||||||
|
Arg.Is<string>(sql =>
|
||||||
|
sql.Contains("DEFINE TABLE OVERWRITE cbddc_oplog_entries", StringComparison.Ordinal) &&
|
||||||
|
sql.Contains("CHANGEFEED 12h", StringComparison.Ordinal) &&
|
||||||
|
sql.Contains("DEFINE TABLE OVERWRITE custom_checkpoint", StringComparison.Ordinal)),
|
||||||
|
Arg.Any<IReadOnlyDictionary<string, object?>>(),
|
||||||
|
Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies invalid checkpoint table identifiers are rejected.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void SchemaInitializer_WhenCheckpointIdentifierIsInvalid_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var embeddedClient = Substitute.For<ICBDDCSurrealEmbeddedClient>();
|
||||||
|
var options = new CBDDCSurrealEmbeddedOptions
|
||||||
|
{
|
||||||
|
Cdc = new CBDDCSurrealCdcOptions
|
||||||
|
{
|
||||||
|
CheckpointTable = "invalid-checkpoint"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Should.Throw<ArgumentException>(() => new CBDDCSurrealSchemaInitializer(embeddedClient, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies readiness returns true when schema init and health checks succeed.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadinessProbe_WhenSchemaAndHealthSucceed_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var embeddedClient = Substitute.For<ICBDDCSurrealEmbeddedClient>();
|
||||||
|
embeddedClient.HealthAsync(Arg.Any<CancellationToken>()).Returns(true);
|
||||||
|
|
||||||
|
var schemaInitializer = Substitute.For<ICBDDCSurrealSchemaInitializer>();
|
||||||
|
schemaInitializer.EnsureInitializedAsync(Arg.Any<CancellationToken>()).Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var probe = new CBDDCSurrealReadinessProbe(embeddedClient, schemaInitializer);
|
||||||
|
bool isReady = await probe.IsReadyAsync();
|
||||||
|
|
||||||
|
isReady.ShouldBeTrue();
|
||||||
|
await schemaInitializer.Received(1).EnsureInitializedAsync(Arg.Any<CancellationToken>());
|
||||||
|
await embeddedClient.Received(1).HealthAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies readiness returns false when schema initialization fails.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ReadinessProbe_WhenSchemaInitializationThrows_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var embeddedClient = Substitute.For<ICBDDCSurrealEmbeddedClient>();
|
||||||
|
var schemaInitializer = Substitute.For<ICBDDCSurrealSchemaInitializer>();
|
||||||
|
schemaInitializer.EnsureInitializedAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromException(new InvalidOperationException("boom")));
|
||||||
|
|
||||||
|
var probe = new CBDDCSurrealReadinessProbe(embeddedClient, schemaInitializer);
|
||||||
|
bool isReady = await probe.IsReadyAsync();
|
||||||
|
|
||||||
|
isReady.ShouldBeFalse();
|
||||||
|
await embeddedClient.DidNotReceive().HealthAsync(Arg.Any<CancellationToken>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
|
|||||||
|
|
||||||
public class SurrealOplogStoreContractTests
|
public class SurrealOplogStoreContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies append, range query, merge, drop, and last-hash behavior for the oplog store.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OplogStore_AppendQueryMergeDrop_AndLastHash_Works()
|
public async Task OplogStore_AppendQueryMergeDrop_AndLastHash_Works()
|
||||||
{
|
{
|
||||||
@@ -71,6 +74,9 @@ public class SurrealOplogStoreContractTests
|
|||||||
|
|
||||||
public class SurrealDocumentMetadataStoreContractTests
|
public class SurrealDocumentMetadataStoreContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies upsert, deletion marking, incremental reads, and merge precedence for document metadata.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DocumentMetadataStore_UpsertMarkDeletedGetAfterAndMergeNewer_Works()
|
public async Task DocumentMetadataStore_UpsertMarkDeletedGetAfterAndMergeNewer_Works()
|
||||||
{
|
{
|
||||||
@@ -108,6 +114,9 @@ public class SurrealDocumentMetadataStoreContractTests
|
|||||||
|
|
||||||
public class SurrealPeerConfigurationStoreContractTests
|
public class SurrealPeerConfigurationStoreContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies save, read, remove, and merge behavior for remote peer configuration records.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PeerConfigurationStore_SaveGetRemoveAndMerge_Works()
|
public async Task PeerConfigurationStore_SaveGetRemoveAndMerge_Works()
|
||||||
{
|
{
|
||||||
@@ -163,6 +172,9 @@ public class SurrealPeerConfigurationStoreContractTests
|
|||||||
|
|
||||||
public class SurrealPeerOplogConfirmationStoreContractTests
|
public class SurrealPeerOplogConfirmationStoreContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies peer registration, confirmation updates, and peer deactivation semantics.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PeerOplogConfirmationStore_EnsureUpdateAndDeactivate_Works()
|
public async Task PeerOplogConfirmationStore_EnsureUpdateAndDeactivate_Works()
|
||||||
{
|
{
|
||||||
@@ -194,6 +206,9 @@ public class SurrealPeerOplogConfirmationStoreContractTests
|
|||||||
afterDeactivate.All(x => x.IsActive == false).ShouldBeTrue();
|
afterDeactivate.All(x => x.IsActive == false).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies merge semantics prefer newer confirmations and preserve active-state transitions.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PeerOplogConfirmationStore_Merge_UsesNewerAndActiveStateSemantics()
|
public async Task PeerOplogConfirmationStore_Merge_UsesNewerAndActiveStateSemantics()
|
||||||
{
|
{
|
||||||
@@ -262,6 +277,9 @@ public class SurrealPeerOplogConfirmationStoreContractTests
|
|||||||
|
|
||||||
public class SurrealSnapshotMetadataStoreContractTests
|
public class SurrealSnapshotMetadataStoreContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies insert, update, merge, and hash lookup behavior for snapshot metadata records.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SnapshotMetadataStore_InsertUpdateMergeAndHashLookup_Works()
|
public async Task SnapshotMetadataStore_InsertUpdateMergeAndHashLookup_Works()
|
||||||
{
|
{
|
||||||
@@ -333,6 +351,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
private readonly string _rootPath;
|
private readonly string _rootPath;
|
||||||
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a temporary embedded Surreal environment for contract tests.
|
||||||
|
/// </summary>
|
||||||
public SurrealTestHarness()
|
public SurrealTestHarness()
|
||||||
{
|
{
|
||||||
string suffix = Guid.NewGuid().ToString("N");
|
string suffix = Guid.NewGuid().ToString("N");
|
||||||
@@ -351,6 +372,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
_schemaInitializer = new TestSurrealSchemaInitializer(_client);
|
_schemaInitializer = new TestSurrealSchemaInitializer(_client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a document metadata store instance bound to the test harness database.
|
||||||
|
/// </summary>
|
||||||
public SurrealDocumentMetadataStore CreateDocumentMetadataStore()
|
public SurrealDocumentMetadataStore CreateDocumentMetadataStore()
|
||||||
{
|
{
|
||||||
return new SurrealDocumentMetadataStore(
|
return new SurrealDocumentMetadataStore(
|
||||||
@@ -359,6 +383,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealDocumentMetadataStore>.Instance);
|
NullLogger<SurrealDocumentMetadataStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an oplog store instance bound to the test harness database.
|
||||||
|
/// </summary>
|
||||||
public SurrealOplogStore CreateOplogStore()
|
public SurrealOplogStore CreateOplogStore()
|
||||||
{
|
{
|
||||||
return new SurrealOplogStore(
|
return new SurrealOplogStore(
|
||||||
@@ -371,6 +398,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealOplogStore>.Instance);
|
NullLogger<SurrealOplogStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a peer configuration store instance bound to the test harness database.
|
||||||
|
/// </summary>
|
||||||
public SurrealPeerConfigurationStore CreatePeerConfigurationStore()
|
public SurrealPeerConfigurationStore CreatePeerConfigurationStore()
|
||||||
{
|
{
|
||||||
return new SurrealPeerConfigurationStore(
|
return new SurrealPeerConfigurationStore(
|
||||||
@@ -379,6 +409,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealPeerConfigurationStore>.Instance);
|
NullLogger<SurrealPeerConfigurationStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a peer oplog confirmation store instance bound to the test harness database.
|
||||||
|
/// </summary>
|
||||||
public SurrealPeerOplogConfirmationStore CreatePeerOplogConfirmationStore()
|
public SurrealPeerOplogConfirmationStore CreatePeerOplogConfirmationStore()
|
||||||
{
|
{
|
||||||
return new SurrealPeerOplogConfirmationStore(
|
return new SurrealPeerOplogConfirmationStore(
|
||||||
@@ -387,6 +420,9 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealPeerOplogConfirmationStore>.Instance);
|
NullLogger<SurrealPeerOplogConfirmationStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a snapshot metadata store instance bound to the test harness database.
|
||||||
|
/// </summary>
|
||||||
public SurrealSnapshotMetadataStore CreateSnapshotMetadataStore()
|
public SurrealSnapshotMetadataStore CreateSnapshotMetadataStore()
|
||||||
{
|
{
|
||||||
return new SurrealSnapshotMetadataStore(
|
return new SurrealSnapshotMetadataStore(
|
||||||
@@ -395,6 +431,7 @@ internal sealed class SurrealTestHarness : IAsyncDisposable
|
|||||||
NullLogger<SurrealSnapshotMetadataStore>.Instance);
|
NullLogger<SurrealSnapshotMetadataStore>.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await _client.DisposeAsync();
|
await _client.DisposeAsync();
|
||||||
@@ -421,11 +458,16 @@ internal sealed class TestSurrealSchemaInitializer : ICBDDCSurrealSchemaInitiali
|
|||||||
private readonly ICBDDCSurrealEmbeddedClient _client;
|
private readonly ICBDDCSurrealEmbeddedClient _client;
|
||||||
private int _initialized;
|
private int _initialized;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TestSurrealSchemaInitializer" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The embedded client to initialize.</param>
|
||||||
public TestSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
|
public TestSurrealSchemaInitializer(ICBDDCSurrealEmbeddedClient client)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
|
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (Interlocked.Exchange(ref _initialized, 1) == 1) return;
|
if (Interlocked.Exchange(ref _initialized, 1) == 1) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user