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

@@ -13,6 +13,9 @@ namespace ZB.MOM.WW.CBDDC.Sample.Console.Tests;
[Collection("SurrealCdcDurability")]
public class SurrealCdcDurabilityTests
{
/// <summary>
/// Verifies checkpoints persist latest local changes per consumer across restarts.
/// </summary>
[Fact]
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]
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]
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]
public async Task LocalDelete_ShouldPersistTombstoneMetadata_AndAdvanceCheckpoint()
{
@@ -358,21 +370,46 @@ internal sealed class CdcTestHarness : IAsyncDisposable
NullLogger<SurrealDocumentMetadataStore>.Instance);
}
/// <summary>
/// Gets the sample database context.
/// </summary>
public SampleDbContext Context { get; }
/// <summary>
/// Gets the checkpointed sample document store.
/// </summary>
public CheckpointedSampleDocumentStore DocumentStore { get; }
/// <summary>
/// Gets the oplog store used by the harness.
/// </summary>
public SurrealOplogStore OplogStore { get; }
/// <summary>
/// Gets the document metadata store.
/// </summary>
public SurrealDocumentMetadataStore MetadataStore { get; }
/// <summary>
/// Gets checkpoint persistence used for CDC progress tracking.
/// </summary>
public ISurrealCdcCheckpointPersistence CheckpointPersistence { get; }
/// <summary>
/// Polls CDC once through the document store.
/// </summary>
public async Task PollAsync()
{
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(
string databasePath,
string nodeId,
@@ -391,6 +428,11 @@ internal sealed class CdcTestHarness : IAsyncDisposable
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)
{
return (await OplogStore.ExportAsync())
@@ -400,6 +442,12 @@ internal sealed class CdcTestHarness : IAsyncDisposable
.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)
{
return (await OplogStore.ExportAsync())
@@ -410,6 +458,7 @@ internal sealed class CdcTestHarness : IAsyncDisposable
.ToList();
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
DocumentStore.Dispose();
@@ -428,6 +477,15 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
private const string UsersCollection = "Users";
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(
SampleDbContext context,
IPeerNodeConfigurationProvider configProvider,
@@ -450,6 +508,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
WatchCollection(TodoListsCollection, context.TodoLists, t => t.Id, subscribeForInMemoryEvents: false);
}
/// <inheritdoc />
protected override async Task ApplyContentToEntityAsync(
string collection,
string key,
@@ -459,6 +518,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
await UpsertEntityAsync(collection, key, content, cancellationToken);
}
/// <inheritdoc />
protected override async Task ApplyContentToEntitiesBatchAsync(
IEnumerable<(string Collection, string Key, JsonElement Content)> documents,
CancellationToken cancellationToken)
@@ -467,6 +527,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
await UpsertEntityAsync(collection, key, content, cancellationToken);
}
/// <inheritdoc />
protected override async Task<JsonElement?> GetEntityAsJsonAsync(
string collection,
string key,
@@ -480,6 +541,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
};
}
/// <inheritdoc />
protected override async Task RemoveEntityAsync(
string collection,
string key,
@@ -488,6 +550,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
await DeleteEntityAsync(collection, key, cancellationToken);
}
/// <inheritdoc />
protected override async Task RemoveEntitiesBatchAsync(
IEnumerable<(string Collection, string Key)> documents,
CancellationToken cancellationToken)
@@ -496,6 +559,7 @@ internal sealed class CheckpointedSampleDocumentStore : SurrealDocumentStore<Sam
await DeleteEntityAsync(collection, key, cancellationToken);
}
/// <inheritdoc />
protected override async Task<IEnumerable<(string Key, JsonElement Content)>> GetAllEntitiesAsJsonAsync(
string collection,
CancellationToken cancellationToken)