Harden Surreal migration with retry/coverage fixes and XML docs cleanup
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m17s

This commit is contained in:
Joseph Doherty
2026-02-22 05:39:00 -05:00
parent 9c2a77dc3c
commit bd10914828
27 changed files with 1402 additions and 19 deletions

View File

@@ -55,14 +55,16 @@ public class RetryPolicy : IRetryPolicy
return await operation();
}
catch (Exception ex) when (attempt < config.RetryAttempts && IsTransient(ex))
{
lastException = ex;
int delay = config.RetryDelayMs * attempt; // Exponential backoff
_logger.LogWarning(ex,
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
operationName, attempt, config.RetryAttempts, delay);
catch (Exception ex) when (IsTransient(ex))
{
lastException = ex;
if (attempt >= config.RetryAttempts) break;
int delay = config.RetryDelayMs * attempt; // Exponential backoff
_logger.LogWarning(ex,
"Operation {Operation} failed (attempt {Attempt}/{Max}). Retrying in {Delay}ms...",
operationName, attempt, config.RetryAttempts, delay);
await Task.Delay(delay, cancellationToken);
}
@@ -111,4 +113,4 @@ public class RetryPolicy : IRetryPolicy
return false;
}
}
}

View File

@@ -16,11 +16,15 @@ public interface ICBDDCSurrealEmbeddedClient : IAsyncDisposable, IDisposable
/// <summary>
/// Connects and selects namespace/database exactly once.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task InitializeAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Executes a raw SurrealQL statement.
/// </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,
IReadOnlyDictionary<string, object?>? parameters = null,
CancellationToken cancellationToken = default);
@@ -28,5 +32,6 @@ public interface ICBDDCSurrealEmbeddedClient : IAsyncDisposable, IDisposable
/// <summary>
/// Checks whether the embedded client responds to health probes.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task<bool> HealthAsync(CancellationToken cancellationToken = default);
}

View File

@@ -8,5 +8,6 @@ public interface ICBDDCSurrealReadinessProbe
/// <summary>
/// Returns true when client initialization, schema initialization, and health checks pass.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task<bool> IsReadyAsync(CancellationToken cancellationToken = default);
}

View File

@@ -8,5 +8,6 @@ public interface ICBDDCSurrealSchemaInitializer
/// <summary>
/// Creates required tables/indexes/checkpoint schema for CBDDC stores.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
Task EnsureInitializedAsync(CancellationToken cancellationToken = default);
}

View File

@@ -13,15 +13,18 @@ public interface ISurrealCdcWorkerLifecycle
/// <summary>
/// Starts the CDC worker.
/// </summary>
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
Task StartCdcWorkerAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Executes one CDC polling pass across all watched collections.
/// </summary>
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
Task PollCdcOnceAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Stops the CDC worker.
/// </summary>
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
Task StopCdcWorkerAsync(CancellationToken cancellationToken = default);
}

View File

@@ -150,30 +150,56 @@ public sealed class SurrealCdcCheckpointPersistence : ISurrealCdcCheckpointPersi
internal sealed class SurrealCdcCheckpointRecord : Record
{
/// <summary>
/// Gets or sets the CDC consumer identifier.
/// </summary>
[JsonPropertyName("consumerId")]
public string ConsumerId { get; set; } = "";
/// <summary>
/// Gets or sets the physical time component of the checkpoint timestamp.
/// </summary>
[JsonPropertyName("timestampPhysicalTime")]
public long TimestampPhysicalTime { get; set; }
/// <summary>
/// Gets or sets the logical counter component of the checkpoint timestamp.
/// </summary>
[JsonPropertyName("timestampLogicalCounter")]
public int TimestampLogicalCounter { get; set; }
/// <summary>
/// Gets or sets the node identifier component of the checkpoint timestamp.
/// </summary>
[JsonPropertyName("timestampNodeId")]
public string TimestampNodeId { get; set; } = "";
/// <summary>
/// Gets or sets the hash associated with the checkpoint.
/// </summary>
[JsonPropertyName("lastHash")]
public string LastHash { get; set; } = "";
/// <summary>
/// Gets or sets the last update time in Unix milliseconds.
/// </summary>
[JsonPropertyName("updatedUtcMs")]
public long UpdatedUtcMs { get; set; }
/// <summary>
/// Gets or sets the optional encoded versionstamp cursor.
/// </summary>
[JsonPropertyName("versionstampCursor")]
public long? VersionstampCursor { get; set; }
}
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)
{
return new SurrealCdcCheckpoint

View File

@@ -13,6 +13,12 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
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(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer,
@@ -24,6 +30,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
_logger = logger ?? NullLogger<SurrealDocumentMetadataStore>.Instance;
}
/// <inheritdoc />
public override async Task<DocumentMetadata?> GetMetadataAsync(string collection, string key,
CancellationToken cancellationToken = default)
{
@@ -31,6 +38,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
return existing?.ToDomain();
}
/// <inheritdoc />
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataByCollectionAsync(string collection,
CancellationToken cancellationToken = default)
{
@@ -41,6 +49,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
.ToList();
}
/// <inheritdoc />
public override async Task UpsertMetadataAsync(DocumentMetadata metadata,
CancellationToken cancellationToken = default)
{
@@ -55,6 +64,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
cancellationToken);
}
/// <inheritdoc />
public override async Task UpsertMetadataBatchAsync(IEnumerable<DocumentMetadata> metadatas,
CancellationToken cancellationToken = default)
{
@@ -62,6 +72,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
await UpsertMetadataAsync(metadata, cancellationToken);
}
/// <inheritdoc />
public override async Task MarkDeletedAsync(string collection, string key, HlcTimestamp timestamp,
CancellationToken cancellationToken = default)
{
@@ -69,6 +80,7 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
await UpsertMetadataAsync(metadata, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<DocumentMetadata>> GetMetadataAfterAsync(HlcTimestamp since,
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
{
@@ -86,24 +98,28 @@ public class SurrealDocumentMetadataStore : DocumentMetadataStore
.ToList();
}
/// <inheritdoc />
public override async Task DropAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
await _surrealClient.Delete(CBDDCSurrealSchemaNames.DocumentMetadataTable, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<DocumentMetadata>> ExportAsync(CancellationToken cancellationToken = default)
{
var all = await SelectAllAsync(cancellationToken);
return all.Select(m => m.ToDomain()).ToList();
}
/// <inheritdoc />
public override async Task ImportAsync(IEnumerable<DocumentMetadata> items,
CancellationToken cancellationToken = default)
{
foreach (var item in items) await UpsertMetadataAsync(item, cancellationToken);
}
/// <inheritdoc />
public override async Task MergeAsync(IEnumerable<DocumentMetadata> items,
CancellationToken cancellationToken = default)
{

View File

@@ -61,6 +61,15 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
/// <summary>
/// Initializes a new instance of the <see cref="SurrealDocumentStore{TContext}" /> class.
/// </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(
TContext context,
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
@@ -128,21 +137,28 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
{
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)
{
_inner = inner;
}
/// <inheritdoc />
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return _inner.BeginScope(state);
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return _inner.IsEnabled(logLevel);
}
/// <inheritdoc />
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
@@ -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="collection">Watchable change source.</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>(
string collectionName,
ISurrealWatchableCollection<TEntity> collection,
@@ -220,6 +237,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
private readonly Func<TEntity, string> _keySelector;
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(
string collectionName,
Func<TEntity, string> keySelector,
@@ -230,6 +253,7 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
_store = store;
}
/// <inheritdoc />
public void OnNext(SurrealCollectionChange<TEntity> changeEvent)
{
if (_store.IsCdcPollingWorkerActiveForCollection(_collectionName)) return;
@@ -267,10 +291,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
.GetAwaiter().GetResult();
}
/// <inheritdoc />
public void OnError(Exception error)
{
}
/// <inheritdoc />
public void OnCompleted()
{
}
@@ -760,22 +786,58 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
#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(
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(
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
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(
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(
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(
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(
string collection, CancellationToken cancellationToken);
@@ -1055,6 +1117,12 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
/// <summary>
/// Handles a local collection change and records oplog/metadata when not suppressed.
/// </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(
string collection,
string key,
@@ -1315,11 +1383,16 @@ public abstract class SurrealDocumentStore<TContext> : IDocumentStore, ISurrealC
private readonly SemaphoreSlim _guard;
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)
{
_guard = guard;
}
/// <inheritdoc />
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;

View File

@@ -127,6 +127,11 @@ public sealed class SurrealCollectionChangeFeed<TEntity> : ISurrealWatchableColl
private readonly IObserver<SurrealCollectionChange<TEntity>> _observer;
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(
SurrealCollectionChangeFeed<TEntity> owner,
IObserver<SurrealCollectionChange<TEntity>> observer)
@@ -135,6 +140,7 @@ public sealed class SurrealCollectionChangeFeed<TEntity> : ISurrealWatchableColl
_observer = observer;
}
/// <inheritdoc />
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) == 1) return;

View File

@@ -14,6 +14,16 @@ public class SurrealOplogStore : OplogStore
private readonly ICBDDCSurrealSchemaInitializer? _schemaInitializer;
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(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer,
@@ -36,6 +46,7 @@ public class SurrealOplogStore : OplogStore
InitializeVectorClock();
}
/// <inheritdoc />
public override async Task<IEnumerable<OplogEntry>> GetChainRangeAsync(string startHash, string endHash,
CancellationToken cancellationToken = default)
{
@@ -61,12 +72,14 @@ public class SurrealOplogStore : OplogStore
.ToList();
}
/// <inheritdoc />
public override async Task<OplogEntry?> GetEntryByHashAsync(string hash, CancellationToken cancellationToken = default)
{
var existing = await FindByHashAsync(hash, cancellationToken);
return existing?.ToDomain();
}
/// <inheritdoc />
public override async Task<IEnumerable<OplogEntry>> GetOplogAfterAsync(HlcTimestamp timestamp,
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
{
@@ -85,6 +98,7 @@ public class SurrealOplogStore : OplogStore
.ToList();
}
/// <inheritdoc />
public override async Task<IEnumerable<OplogEntry>> GetOplogForNodeAfterAsync(string nodeId, HlcTimestamp since,
IEnumerable<string>? collections = null, CancellationToken cancellationToken = default)
{
@@ -104,6 +118,7 @@ public class SurrealOplogStore : OplogStore
.ToList();
}
/// <inheritdoc />
public override async Task PruneOplogAsync(HlcTimestamp cutoff, CancellationToken cancellationToken = default)
{
var all = await SelectAllAsync(cancellationToken);
@@ -121,6 +136,7 @@ public class SurrealOplogStore : OplogStore
}
}
/// <inheritdoc />
public override async Task DropAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
@@ -128,12 +144,14 @@ public class SurrealOplogStore : OplogStore
_vectorClock.Invalidate();
}
/// <inheritdoc />
public override async Task<IEnumerable<OplogEntry>> ExportAsync(CancellationToken cancellationToken = default)
{
var all = await SelectAllAsync(cancellationToken);
return all.Select(o => o.ToDomain()).ToList();
}
/// <inheritdoc />
public override async Task ImportAsync(IEnumerable<OplogEntry> items, CancellationToken cancellationToken = default)
{
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)
{
foreach (var item in items)
@@ -155,6 +174,7 @@ public class SurrealOplogStore : OplogStore
}
}
/// <inheritdoc />
protected override void InitializeVectorClock()
{
if (_vectorClock.IsInitialized) return;
@@ -206,6 +226,7 @@ public class SurrealOplogStore : OplogStore
_vectorClock.IsInitialized = true;
}
/// <inheritdoc />
protected override async Task InsertOplogEntryAsync(OplogEntry entry, CancellationToken cancellationToken = default)
{
var existing = await FindByHashAsync(entry.Hash, cancellationToken);
@@ -214,6 +235,7 @@ public class SurrealOplogStore : OplogStore
await UpsertAsync(entry, SurrealStoreRecordIds.Oplog(entry.Hash), cancellationToken);
}
/// <inheritdoc />
protected override async Task<string?> QueryLastHashForNodeAsync(string nodeId,
CancellationToken cancellationToken = default)
{
@@ -226,6 +248,7 @@ public class SurrealOplogStore : OplogStore
return lastEntry?.Hash;
}
/// <inheritdoc />
protected override async Task<(long Wall, int Logic)?> QueryLastHashTimestampFromOplogAsync(string hash,
CancellationToken cancellationToken = default)
{

View File

@@ -12,6 +12,12 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
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(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer,
@@ -23,6 +29,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
_logger = logger ?? NullLogger<SurrealPeerConfigurationStore>.Instance;
}
/// <inheritdoc />
public override async Task<IEnumerable<RemotePeerConfiguration>> GetRemotePeersAsync(
CancellationToken cancellationToken = default)
{
@@ -30,6 +37,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
return all.Select(p => p.ToDomain()).ToList();
}
/// <inheritdoc />
public override async Task<RemotePeerConfiguration?> GetRemotePeerAsync(string nodeId,
CancellationToken cancellationToken)
{
@@ -37,6 +45,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
return existing?.ToDomain();
}
/// <inheritdoc />
public override async Task RemoveRemotePeerAsync(string nodeId, CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
@@ -52,6 +61,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
_logger.LogInformation("Removed remote peer configuration: {NodeId}", nodeId);
}
/// <inheritdoc />
public override async Task SaveRemotePeerAsync(RemotePeerConfiguration peer,
CancellationToken cancellationToken = default)
{
@@ -67,6 +77,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
_logger.LogInformation("Saved remote peer configuration: {NodeId} ({Type})", peer.NodeId, peer.Type);
}
/// <inheritdoc />
public override async Task DropAsync(CancellationToken cancellationToken = default)
{
_logger.LogWarning(
@@ -76,6 +87,7 @@ public class SurrealPeerConfigurationStore : PeerConfigurationStore
_logger.LogInformation("Peer configuration store dropped successfully.");
}
/// <inheritdoc />
public override async Task<IEnumerable<RemotePeerConfiguration>> ExportAsync(
CancellationToken cancellationToken = default)
{

View File

@@ -15,6 +15,12 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
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(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer,
@@ -26,6 +32,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
_logger = logger ?? NullLogger<SurrealPeerOplogConfirmationStore>.Instance;
}
/// <inheritdoc />
public override async Task EnsurePeerRegisteredAsync(
string peerNodeId,
string address,
@@ -68,6 +75,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
await UpsertAsync(existing, recordId, cancellationToken);
}
/// <inheritdoc />
public override async Task UpdateConfirmationAsync(
string peerNodeId,
string sourceNodeId,
@@ -118,6 +126,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
await UpsertAsync(existing, recordId, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsAsync(
CancellationToken cancellationToken = default)
{
@@ -128,6 +137,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
.ToList();
}
/// <inheritdoc />
public override async Task<IEnumerable<PeerOplogConfirmation>> GetConfirmationsForPeerAsync(
string peerNodeId,
CancellationToken cancellationToken = default)
@@ -143,6 +153,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
.ToList();
}
/// <inheritdoc />
public override async Task RemovePeerTrackingAsync(string peerNodeId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(peerNodeId))
@@ -167,6 +178,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
}
}
/// <inheritdoc />
public override async Task<IEnumerable<string>> GetActiveTrackedPeersAsync(
CancellationToken cancellationToken = default)
{
@@ -178,18 +190,21 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
.ToList();
}
/// <inheritdoc />
public override async Task DropAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
await _surrealClient.Delete(CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<PeerOplogConfirmation>> ExportAsync(CancellationToken cancellationToken = default)
{
var all = await SelectAllAsync(cancellationToken);
return all.Select(c => c.ToDomain()).ToList();
}
/// <inheritdoc />
public override async Task ImportAsync(IEnumerable<PeerOplogConfirmation> items,
CancellationToken cancellationToken = default)
{
@@ -202,6 +217,7 @@ public class SurrealPeerOplogConfirmationStore : PeerOplogConfirmationStore
}
}
/// <inheritdoc />
public override async Task MergeAsync(IEnumerable<PeerOplogConfirmation> items,
CancellationToken cancellationToken = default)
{

View File

@@ -17,6 +17,12 @@ internal static class SurrealShowChangesCborDecoder
{
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(
IEnumerable<CborObject> rows,
string expectedTableName)

View File

@@ -12,6 +12,12 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
private readonly ICBDDCSurrealSchemaInitializer _schemaInitializer;
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(
ICBDDCSurrealEmbeddedClient surrealEmbeddedClient,
ICBDDCSurrealSchemaInitializer schemaInitializer,
@@ -23,18 +29,21 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
_logger = logger ?? NullLogger<SurrealSnapshotMetadataStore>.Instance;
}
/// <inheritdoc />
public override async Task DropAsync(CancellationToken cancellationToken = default)
{
await EnsureReadyAsync(cancellationToken);
await _surrealClient.Delete(CBDDCSurrealSchemaNames.SnapshotMetadataTable, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<SnapshotMetadata>> ExportAsync(CancellationToken cancellationToken = default)
{
var all = await SelectAllAsync(cancellationToken);
return all.Select(m => m.ToDomain()).ToList();
}
/// <inheritdoc />
public override async Task<SnapshotMetadata?> GetSnapshotMetadataAsync(string nodeId,
CancellationToken cancellationToken = default)
{
@@ -42,12 +51,14 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
return existing?.ToDomain();
}
/// <inheritdoc />
public override async Task<string?> GetSnapshotHashAsync(string nodeId, CancellationToken cancellationToken = default)
{
var existing = await FindByNodeIdAsync(nodeId, cancellationToken);
return existing?.Hash;
}
/// <inheritdoc />
public override async Task ImportAsync(IEnumerable<SnapshotMetadata> items,
CancellationToken cancellationToken = default)
{
@@ -59,6 +70,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
}
}
/// <inheritdoc />
public override async Task InsertSnapshotMetadataAsync(SnapshotMetadata metadata,
CancellationToken cancellationToken = default)
{
@@ -67,6 +79,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
await UpsertAsync(metadata, recordId, cancellationToken);
}
/// <inheritdoc />
public override async Task MergeAsync(IEnumerable<SnapshotMetadata> items, CancellationToken cancellationToken = default)
{
foreach (var metadata in items)
@@ -88,6 +101,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
}
}
/// <inheritdoc />
public override async Task UpdateSnapshotMetadataAsync(SnapshotMetadata existingMeta,
CancellationToken cancellationToken)
{
@@ -98,6 +112,7 @@ public class SurrealSnapshotMetadataStore : SnapshotMetadataStore
await UpsertAsync(existingMeta, recordId, cancellationToken);
}
/// <inheritdoc />
public override async Task<IEnumerable<SnapshotMetadata>> GetAllSnapshotMetadataAsync(
CancellationToken cancellationToken = default)
{

View File

@@ -11,11 +11,22 @@ namespace ZB.MOM.WW.CBDDC.Persistence.Surreal;
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)
{
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)
{
return RecordId.From(
@@ -23,16 +34,32 @@ internal static class SurrealStoreRecordIds
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)
{
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)
{
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)
{
return RecordId.From(
@@ -49,114 +76,212 @@ internal static class SurrealStoreRecordIds
internal sealed class SurrealOplogRecord : Record
{
/// <summary>
/// Gets or sets the collection name.
/// </summary>
[JsonPropertyName("collection")]
public string Collection { get; set; } = "";
/// <summary>
/// Gets or sets the document key.
/// </summary>
[JsonPropertyName("key")]
public string Key { get; set; } = "";
/// <summary>
/// Gets or sets the operation type value.
/// </summary>
[JsonPropertyName("operation")]
public int Operation { get; set; }
/// <summary>
/// Gets or sets the serialized payload JSON.
/// </summary>
[JsonPropertyName("payloadJson")]
public string PayloadJson { get; set; } = "";
/// <summary>
/// Gets or sets the timestamp physical time.
/// </summary>
[JsonPropertyName("timestampPhysicalTime")]
public long TimestampPhysicalTime { get; set; }
/// <summary>
/// Gets or sets the timestamp logical counter.
/// </summary>
[JsonPropertyName("timestampLogicalCounter")]
public int TimestampLogicalCounter { get; set; }
/// <summary>
/// Gets or sets the timestamp node identifier.
/// </summary>
[JsonPropertyName("timestampNodeId")]
public string TimestampNodeId { get; set; } = "";
/// <summary>
/// Gets or sets the entry hash.
/// </summary>
[JsonPropertyName("hash")]
public string Hash { get; set; } = "";
/// <summary>
/// Gets or sets the previous entry hash.
/// </summary>
[JsonPropertyName("previousHash")]
public string PreviousHash { get; set; } = "";
}
internal sealed class SurrealDocumentMetadataRecord : Record
{
/// <summary>
/// Gets or sets the collection name.
/// </summary>
[JsonPropertyName("collection")]
public string Collection { get; set; } = "";
/// <summary>
/// Gets or sets the document key.
/// </summary>
[JsonPropertyName("key")]
public string Key { get; set; } = "";
/// <summary>
/// Gets or sets the HLC physical time.
/// </summary>
[JsonPropertyName("hlcPhysicalTime")]
public long HlcPhysicalTime { get; set; }
/// <summary>
/// Gets or sets the HLC logical counter.
/// </summary>
[JsonPropertyName("hlcLogicalCounter")]
public int HlcLogicalCounter { get; set; }
/// <summary>
/// Gets or sets the HLC node identifier.
/// </summary>
[JsonPropertyName("hlcNodeId")]
public string HlcNodeId { get; set; } = "";
/// <summary>
/// Gets or sets a value indicating whether the document is deleted.
/// </summary>
[JsonPropertyName("isDeleted")]
public bool IsDeleted { get; set; }
}
internal sealed class SurrealRemotePeerRecord : Record
{
/// <summary>
/// Gets or sets the peer node identifier.
/// </summary>
[JsonPropertyName("nodeId")]
public string NodeId { get; set; } = "";
/// <summary>
/// Gets or sets the peer network address.
/// </summary>
[JsonPropertyName("address")]
public string Address { get; set; } = "";
/// <summary>
/// Gets or sets the peer type value.
/// </summary>
[JsonPropertyName("type")]
public int Type { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the peer is enabled.
/// </summary>
[JsonPropertyName("isEnabled")]
public bool IsEnabled { get; set; }
/// <summary>
/// Gets or sets the serialized list of collection interests.
/// </summary>
[JsonPropertyName("interestsJson")]
public string InterestsJson { get; set; } = "";
}
internal sealed class SurrealPeerOplogConfirmationRecord : Record
{
/// <summary>
/// Gets or sets the peer node identifier.
/// </summary>
[JsonPropertyName("peerNodeId")]
public string PeerNodeId { get; set; } = "";
/// <summary>
/// Gets or sets the source node identifier.
/// </summary>
[JsonPropertyName("sourceNodeId")]
public string SourceNodeId { get; set; } = "";
/// <summary>
/// Gets or sets the confirmed wall clock component.
/// </summary>
[JsonPropertyName("confirmedWall")]
public long ConfirmedWall { get; set; }
/// <summary>
/// Gets or sets the confirmed logical component.
/// </summary>
[JsonPropertyName("confirmedLogic")]
public int ConfirmedLogic { get; set; }
/// <summary>
/// Gets or sets the confirmed hash.
/// </summary>
[JsonPropertyName("confirmedHash")]
public string ConfirmedHash { get; set; } = "";
/// <summary>
/// Gets or sets the last confirmation time in Unix milliseconds.
/// </summary>
[JsonPropertyName("lastConfirmedUtcMs")]
public long LastConfirmedUtcMs { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the confirmation is active.
/// </summary>
[JsonPropertyName("isActive")]
public bool IsActive { get; set; }
}
internal sealed class SurrealSnapshotMetadataRecord : Record
{
/// <summary>
/// Gets or sets the node identifier.
/// </summary>
[JsonPropertyName("nodeId")]
public string NodeId { get; set; } = "";
/// <summary>
/// Gets or sets the timestamp physical time.
/// </summary>
[JsonPropertyName("timestampPhysicalTime")]
public long TimestampPhysicalTime { get; set; }
/// <summary>
/// Gets or sets the timestamp logical counter.
/// </summary>
[JsonPropertyName("timestampLogicalCounter")]
public int TimestampLogicalCounter { get; set; }
/// <summary>
/// Gets or sets the snapshot hash.
/// </summary>
[JsonPropertyName("hash")]
public string Hash { get; set; } = "";
}
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)
{
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)
{
JsonElement? payload = null;
@@ -189,6 +319,11 @@ internal static class SurrealStoreRecordMappers
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)
{
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)
{
return new DocumentMetadata(
@@ -211,6 +351,11 @@ internal static class SurrealStoreRecordMappers
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)
{
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)
{
var result = new RemotePeerConfiguration
@@ -242,6 +392,11 @@ internal static class SurrealStoreRecordMappers
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)
{
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)
{
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)
{
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)
{
return new SnapshotMetadata