192 lines
6.3 KiB
C#
192 lines
6.3 KiB
C#
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.
|
|
}
|
|
}
|
|
}
|