Files
CBDDC/src/ZB.MOM.WW.CBDDC.Persistence/Surreal/CBDDCSurrealSchemaInitializer.cs
Joseph Doherty 8e97061ab8
All checks were successful
NuGet Package Publish / nuget (push) Successful in 1m14s
Implement in-process multi-dataset sync isolation across core, network, persistence, and tests
2026-02-22 11:58:34 -05:00

132 lines
7.7 KiB
C#

using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace ZB.MOM.WW.CBDDC.Persistence.Surreal;
/// <summary>
/// Initializes Surreal schema objects required by CBDDC persistence stores.
/// </summary>
public sealed class CBDDCSurrealSchemaInitializer : ICBDDCSurrealSchemaInitializer
{
private static readonly Regex IdentifierRegex = new("^[A-Za-z_][A-Za-z0-9_]*$", RegexOptions.Compiled);
private readonly SemaphoreSlim _initializeGate = new(1, 1);
private readonly ICBDDCSurrealEmbeddedClient _surrealClient;
private readonly ILogger<CBDDCSurrealSchemaInitializer> _logger;
private readonly string _checkpointTable;
private readonly string _changefeedRetentionLiteral;
private bool _initialized;
/// <summary>
/// Initializes a new instance of the <see cref="CBDDCSurrealSchemaInitializer" /> class.
/// </summary>
/// <param name="surrealClient">Surreal client abstraction.</param>
/// <param name="options">Embedded options.</param>
/// <param name="logger">Optional logger.</param>
public CBDDCSurrealSchemaInitializer(
ICBDDCSurrealEmbeddedClient surrealClient,
CBDDCSurrealEmbeddedOptions options,
ILogger<CBDDCSurrealSchemaInitializer>? logger = null)
{
_surrealClient = surrealClient ?? throw new ArgumentNullException(nameof(surrealClient));
_logger = logger ?? NullLogger<CBDDCSurrealSchemaInitializer>.Instance;
if (options == null) throw new ArgumentNullException(nameof(options));
if (options.Cdc == null) throw new ArgumentException("CDC options are required.", nameof(options));
_checkpointTable = EnsureValidIdentifier(options.Cdc.CheckpointTable, nameof(options.Cdc.CheckpointTable));
_changefeedRetentionLiteral = ToSurrealDurationLiteral(
options.Cdc.RetentionDuration,
nameof(options.Cdc.RetentionDuration));
}
/// <inheritdoc />
public async Task EnsureInitializedAsync(CancellationToken cancellationToken = default)
{
if (_initialized) return;
await _initializeGate.WaitAsync(cancellationToken);
try
{
if (_initialized) return;
string schemaSql = BuildSchemaSql();
await _surrealClient.RawQueryAsync(schemaSql, cancellationToken: cancellationToken);
_initialized = true;
_logger.LogInformation(
"Surreal schema initialized with checkpoint table '{CheckpointTable}'.",
_checkpointTable);
}
finally
{
_initializeGate.Release();
}
}
private string BuildSchemaSql()
{
return $"""
DEFINE TABLE OVERWRITE {CBDDCSurrealSchemaNames.OplogEntriesTable} SCHEMAFULL CHANGEFEED {_changefeedRetentionLiteral};
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.OplogHashIndex} ON TABLE {CBDDCSurrealSchemaNames.OplogEntriesTable} COLUMNS datasetId, hash UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.OplogHlcIndex} ON TABLE {CBDDCSurrealSchemaNames.OplogEntriesTable} COLUMNS datasetId, timestampPhysicalTime, timestampLogicalCounter, timestampNodeId;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.OplogCollectionIndex} ON TABLE {CBDDCSurrealSchemaNames.OplogEntriesTable} COLUMNS datasetId, collection;
DEFINE TABLE OVERWRITE {CBDDCSurrealSchemaNames.SnapshotMetadataTable} SCHEMAFULL CHANGEFEED {_changefeedRetentionLiteral};
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.SnapshotNodeIdIndex} ON TABLE {CBDDCSurrealSchemaNames.SnapshotMetadataTable} COLUMNS datasetId, nodeId UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.SnapshotHlcIndex} ON TABLE {CBDDCSurrealSchemaNames.SnapshotMetadataTable} COLUMNS datasetId, timestampPhysicalTime, timestampLogicalCounter;
DEFINE TABLE OVERWRITE {CBDDCSurrealSchemaNames.RemotePeerConfigurationsTable} SCHEMAFULL CHANGEFEED {_changefeedRetentionLiteral};
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.PeerNodeIdIndex} ON TABLE {CBDDCSurrealSchemaNames.RemotePeerConfigurationsTable} COLUMNS nodeId UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.PeerEnabledIndex} ON TABLE {CBDDCSurrealSchemaNames.RemotePeerConfigurationsTable} COLUMNS isEnabled;
DEFINE TABLE OVERWRITE {CBDDCSurrealSchemaNames.DocumentMetadataTable} SCHEMAFULL CHANGEFEED {_changefeedRetentionLiteral};
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.DocumentMetadataCollectionKeyIndex} ON TABLE {CBDDCSurrealSchemaNames.DocumentMetadataTable} COLUMNS datasetId, collection, key UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.DocumentMetadataHlcIndex} ON TABLE {CBDDCSurrealSchemaNames.DocumentMetadataTable} COLUMNS datasetId, hlcPhysicalTime, hlcLogicalCounter, hlcNodeId;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.DocumentMetadataCollectionIndex} ON TABLE {CBDDCSurrealSchemaNames.DocumentMetadataTable} COLUMNS datasetId, collection;
DEFINE TABLE OVERWRITE {CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable} SCHEMAFULL CHANGEFEED {_changefeedRetentionLiteral};
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.PeerConfirmationPairIndex} ON TABLE {CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable} COLUMNS datasetId, peerNodeId, sourceNodeId UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.PeerConfirmationActiveIndex} ON TABLE {CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable} COLUMNS datasetId, isActive;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.PeerConfirmationSourceHlcIndex} ON TABLE {CBDDCSurrealSchemaNames.PeerOplogConfirmationsTable} COLUMNS datasetId, sourceNodeId, confirmedWall, confirmedLogic;
DEFINE TABLE OVERWRITE {_checkpointTable} SCHEMAFULL;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.CdcCheckpointConsumerIndex} ON TABLE {_checkpointTable} COLUMNS datasetId, consumerId UNIQUE;
DEFINE INDEX OVERWRITE {CBDDCSurrealSchemaNames.CdcCheckpointVersionstampIndex} ON TABLE {_checkpointTable} COLUMNS datasetId, versionstampCursor;
""";
}
private static string EnsureValidIdentifier(string? identifier, string argumentName)
{
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException("Surreal identifier is required.", argumentName);
if (!IdentifierRegex.IsMatch(identifier))
throw new ArgumentException(
$"Invalid Surreal identifier '{identifier}'. Use letters, numbers, and underscores only.",
argumentName);
return identifier;
}
private static string ToSurrealDurationLiteral(TimeSpan duration, string argumentName)
{
if (duration <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(argumentName, "Surreal changefeed retention duration must be positive.");
if (duration.TotalDays >= 1 && duration.TotalDays == Math.Truncate(duration.TotalDays))
return $"{(long)duration.TotalDays}d";
if (duration.TotalHours >= 1 && duration.TotalHours == Math.Truncate(duration.TotalHours))
return $"{(long)duration.TotalHours}h";
if (duration.TotalMinutes >= 1 && duration.TotalMinutes == Math.Truncate(duration.TotalMinutes))
return $"{(long)duration.TotalMinutes}m";
if (duration.TotalSeconds >= 1 && duration.TotalSeconds == Math.Truncate(duration.TotalSeconds))
return $"{(long)duration.TotalSeconds}s";
long totalMs = checked((long)Math.Ceiling(duration.TotalMilliseconds));
return $"{totalMs}ms";
}
}