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

@@ -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;