using MBN_STOCK_WEBVIEW.Infrastructure; using MBN_STOCK_WEBVIEW.Infrastructure.Diagnostics; using MMoneyCoderSharp.Data; namespace MBN_STOCK_WEBVIEW.LegacyParityApp; public sealed partial class MainWindow { private static readonly TimeSpan DatabaseHealthPollingInterval = TimeSpan.FromSeconds(10); private readonly SemaphoreSlim _databaseActivityGate = new(1, 1); private readonly INativeOperatorLogWriter _databaseHealthLogWriter = new NativeOperatorLogWriter(); private readonly NativeOperatorLogTransitionTracker _databaseHealthTransitions = new(); private int _databaseActivityPriorityWaiterCount; private CancellationTokenSource? _databaseHealthCheckCancellation; private Task? _databaseHealthMonitorTask; private void StartDatabaseHealthMonitor() { _databaseHealthMonitorTask ??= MonitorDatabaseHealthAsync(_lifetimeCancellation.Token); } private async Task MonitorDatabaseHealthAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { // WinForms timerAliveChecker first fired after its 10-second interval. await Task.Delay(DatabaseHealthPollingInterval, cancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return; } try { await CheckAndLogDatabaseHealthAsync(cancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return; } } } private async Task CheckAndLogDatabaseHealthAsync(CancellationToken cancellationToken) { if (Volatile.Read(ref _playoutBusy) != 0 || Volatile.Read(ref _databaseActivityPriorityWaiterCount) != 0 || !await _databaseActivityGate.WaitAsync(0, cancellationToken) .ConfigureAwait(false)) { return; } CancellationTokenSource? healthCancellation = null; try { if (Volatile.Read(ref _playoutBusy) != 0) { return; } if (_databaseRuntime is null) { ObserveDatabaseHealthState( DataSourceKind.Oracle, DatabaseHealthState.NotConfigured); ObserveDatabaseHealthState( DataSourceKind.MariaDb, DatabaseHealthState.NotConfigured); return; } healthCancellation = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken); Interlocked.Exchange( ref _databaseHealthCheckCancellation, healthCancellation); // Close the race where an operator command starts after this monitor // acquired the activity gate but before the provider probes begin. if (Volatile.Read(ref _playoutBusy) != 0 || Volatile.Read(ref _databaseActivityPriorityWaiterCount) != 0) { healthCancellation.Cancel(); return; } var statuses = await _databaseRuntime.HealthService .CheckAllAsync(healthCancellation.Token) .ConfigureAwait(false); foreach (var status in statuses) { ObserveDatabaseHealthState(status.Source, status.State); } } catch (OperationCanceledException) when ( cancellationToken.IsCancellationRequested || healthCancellation?.IsCancellationRequested == true) { // Shutdown and operator work both have priority over diagnostics. } catch { ObserveDatabaseHealthState( DataSourceKind.Oracle, DatabaseHealthState.Unhealthy); ObserveDatabaseHealthState( DataSourceKind.MariaDb, DatabaseHealthState.Unhealthy); } finally { if (healthCancellation is not null) { Interlocked.CompareExchange( ref _databaseHealthCheckCancellation, null, healthCancellation); healthCancellation.Dispose(); } _databaseActivityGate.Release(); } } private async Task AcquirePriorityDatabaseActivityAsync( CancellationToken cancellationToken) { // Signal priority before inspecting the active probe. This closes both // sides of the acquisition/publish race: // * a probe that has not acquired the gate sees the waiter and yields; // * a probe that owns the gate either sees the waiter after publishing // its CTS or is cancelled through that already-published CTS. Interlocked.Increment(ref _databaseActivityPriorityWaiterCount); try { CancelActiveDatabaseHealthCheck(); await _databaseActivityGate.WaitAsync(cancellationToken) .ConfigureAwait(false); } finally { Interlocked.Decrement(ref _databaseActivityPriorityWaiterCount); } } private void CancelActiveDatabaseHealthCheck() { var healthCancellation = Volatile.Read(ref _databaseHealthCheckCancellation); try { healthCancellation?.Cancel(); } catch (ObjectDisposedException) { // Probe completion won the race after the reference was read. } } private void ObserveDatabaseHealthState( DataSourceKind source, DatabaseHealthState state) { if (!_databaseHealthTransitions.TryObserveDatabaseState( source, state, out var record)) { return; } try { _ = _databaseHealthLogWriter.TryWrite(record); } catch { // Logging is best effort and cannot affect DB or playout work. } } }