feat: consolidate legacy parity migration baseline
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user