132 lines
7.7 KiB
C#
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";
|
|
}
|
|
}
|