using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; using System.Collections.Frozen; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb; /// /// for the QuestDB historian. Supports every /// and HistoryUpdate (soft delete). All seams share ONE connection helper, ONE /// schema bootstrapper, ONE tag catalog (R7: the only "known tag" authority) and ONE WAL visibility wait, so the /// schema is ensured once and known tags are cached once. /// /// /// The shared state is built on the first Create* call, not in the constructor: the constructor does no /// validation and no I/O, so resolving the provider for a backend nobody consumes never fails on its options /// (they are validated, consumer-gated, by the host's ServerHistorianOptionsValidator). The first /// Create* call's service provider supplies the logger factory the shared pieces log through. /// The provider owns the shared connection pool and disposes it (IAsyncDisposable/IDisposable); the host /// registers the provider through a DI factory so the container disposes it at shutdown. /// public sealed class QuestDbHistorianProvider : IHistorianBackendProvider, IAsyncDisposable, IDisposable { private static readonly FrozenSet Kinds = Enum.GetValues().ToFrozenSet(); private readonly QuestDbHistorianOptions _options; private readonly object _initLock = new(); private Shared? _shared; private bool _disposed; /// Creates the provider over the ServerHistorian:QuestDb options (validated elsewhere). /// The ServerHistorian:QuestDb options. /// The shared ServerHistorian:MaxTimeWeightedRawRows cap (<= 0 = unbounded). public QuestDbHistorianProvider(QuestDbHistorianOptions options, int maxTimeWeightedRawRows = TimeWeightedAggregator.DefaultMaxWindowRows) { _options = options ?? throw new ArgumentNullException(nameof(options)); _maxTimeWeightedRawRows = maxTimeWeightedRawRows; } private readonly int _maxTimeWeightedRawRows; /// public string Name => "QuestDb"; /// public IReadOnlySet SupportedKinds => Kinds; private static ILoggerFactory Loggers(IServiceProvider sp) => (sp.GetService(typeof(ILoggerFactory)) as ILoggerFactory) ?? NullLoggerFactory.Instance; private Shared Get(IServiceProvider sp) { if (Volatile.Read(ref _shared) is { } built) return built; lock (_initLock) { if (_shared is { } raced) return raced; var loggers = Loggers(sp); ObjectDisposedException.ThrowIf(_disposed, this); var conn = new QuestDbConnection(_options, loggers.CreateLogger()); var bootstrap = new QuestDbSchemaBootstrapper(conn, _options, loggers.CreateLogger()); var wal = new QuestDbWalVisibility(conn, _options.VisibilityTimeout, loggers.CreateLogger()); var shared = new Shared(conn, bootstrap, new QuestDbTagCatalog(conn, bootstrap, wal), wal); Volatile.Write(ref _shared, shared); return shared; } } /// public IHistorianDataSource CreateDataSource(IServiceProvider sp) { var s = Get(sp); return new QuestDbHistorianDataSource(s.Conn, s.Bootstrap, s.Catalog, Loggers(sp).CreateLogger(), maxTimeWeightedRawRows: _maxTimeWeightedRawRows); } /// public IHistorianValueWriter CreateValueWriter(IServiceProvider sp) { var s = Get(sp); return new QuestDbHistorianValueWriter(s.Conn, s.Catalog, s.Wal, Loggers(sp).CreateLogger()); } /// public IAlarmHistorianWriter CreateAlarmWriter(IServiceProvider sp) { var s = Get(sp); return new QuestDbAlarmHistorianWriter(s.Conn, s.Bootstrap, s.Wal, Loggers(sp).CreateLogger()); } /// public IHistorianProvisioning CreateProvisioning(IServiceProvider sp) { var s = Get(sp); return new QuestDbTagProvisioner(s.Conn, s.Bootstrap, s.Catalog, s.Wal, Loggers(sp).CreateLogger()); } /// /// The soft-delete updater: deletes write tombstones (INSERT only, never QuestDB UPDATE/DELETE). public IHistorianDataUpdater? CreateUpdater(IServiceProvider sp) { var s = Get(sp); return new QuestDbHistorianDataUpdater(s.Conn, s.Catalog, s.Wal, Loggers(sp).CreateLogger()); } /// Disposes the shared connection pool, when one was built. Idempotent. /// A task completing when the pool is disposed. public async ValueTask DisposeAsync() { if (TakeForDispose() is { } conn) await conn.DisposeAsync().ConfigureAwait(false); } /// Disposes the shared connection pool, when one was built. Idempotent. public void Dispose() => TakeForDispose()?.Dispose(); private QuestDbConnection? TakeForDispose() { lock (_initLock) { if (_disposed) return null; _disposed = true; var conn = _shared?.Conn; _shared = null; return conn; } } private sealed record Shared( QuestDbConnection Conn, QuestDbSchemaBootstrapper Bootstrap, QuestDbTagCatalog Catalog, QuestDbWalVisibility Wal); }