From bace56ccb30cefa2210eb39753dc8d05133892a7 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Mon, 1 Jun 2026 18:04:57 +0200 Subject: [PATCH] Updated packages --- .../EonaCat.LogStack.LogClient.csproj | 2 +- ...aCat.LogStack.Flows.WindowsEventLog.csproj | 4 +- EonaCat.LogStack/EonaCat.LogStack.csproj | 14 +-- .../EonaCatLoggerCore/Flows/FailoverFlow.cs | 119 +++++++++++++++++- EonaCat.LogStack/LogBuilder.cs | 4 +- .../EonaCat.LogStack.Test.Web.csproj | 4 +- 6 files changed, 130 insertions(+), 17 deletions(-) diff --git a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj index 1393c9d..7a40f61 100644 --- a/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj +++ b/EonaCat.LogStack.LogClient/EonaCat.LogStack.LogClient.csproj @@ -25,7 +25,7 @@ - + diff --git a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj index 5aad612..9c46043 100644 --- a/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj +++ b/EonaCat.LogStack.WindowsEventLogFlow/EonaCat.LogStack.Flows.WindowsEventLog.csproj @@ -35,8 +35,8 @@ - - + + diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index f2d0491..43a9261 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -66,18 +66,18 @@ It features a rich fluent API for routing log events to dozens of destinations f - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + - + diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs index 92837c0..9b4ef45 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FailoverFlow.cs @@ -15,24 +15,65 @@ namespace EonaCat.LogStack.Flows { private readonly IFlow _primary; private readonly IFlow _secondary; + private readonly TimeSpan _recoveryCheckInterval; + private readonly int _failureThreshold; - public FailoverFlow(IFlow primary, IFlow secondary) + private IFlow _activeFlow; + private long _consecutiveFailures; + private DateTime _lastRecoveryCheck; + private CancellationTokenSource _recoveryCancellation; + private Task _recoveryTask; + private readonly object _syncLock = new object(); + + public FailoverFlow( + IFlow primary, + IFlow secondary, + TimeSpan? recoveryCheckInterval = null, + int failureThreshold = 5) : base($"Failover({primary.Name})", primary.MinimumLevel) { _primary = primary ?? throw new ArgumentNullException(nameof(primary)); _secondary = secondary ?? throw new ArgumentNullException(nameof(secondary)); + _recoveryCheckInterval = recoveryCheckInterval ?? TimeSpan.FromSeconds(10); + _failureThreshold = failureThreshold; + _activeFlow = _primary; + _consecutiveFailures = 0; + _lastRecoveryCheck = DateTime.UtcNow; + _recoveryCancellation = new CancellationTokenSource(); + _recoveryTask = StartRecoveryDetectionAsync(); } public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) { - var result = await _primary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + var result = await _activeFlow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); if (result == WriteResult.Success) { + // Reset failure counter on success + Interlocked.Exchange(ref _consecutiveFailures, 0); return result; } - return await _secondary.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + // Only failover to secondary if using primary + if (_activeFlow == _primary) + { + var failures = Interlocked.Increment(ref _consecutiveFailures); + + if (failures >= _failureThreshold) + { + lock (_syncLock) + { + // Double-check inside lock + if (_activeFlow == _primary && Interlocked.Read(ref _consecutiveFailures) >= _failureThreshold) + { + _activeFlow = _secondary; + Interlocked.Exchange(ref _consecutiveFailures, 0); + } + } + } + } + + return result; } public override async Task FlushAsync(CancellationToken cancellationToken = default) @@ -43,8 +84,80 @@ namespace EonaCat.LogStack.Flows public override async ValueTask DisposeAsync() { + _recoveryCancellation?.Cancel(); + + if (_recoveryTask != null) + { + try + { + await _recoveryTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected when cancelling the recovery task + } + } + + _recoveryCancellation?.Dispose(); await _primary.DisposeAsync(); await _secondary.DisposeAsync(); } + + private async Task StartRecoveryDetectionAsync() + { + try + { + while (!_recoveryCancellation.Token.IsCancellationRequested) + { + await Task.Delay(_recoveryCheckInterval, _recoveryCancellation.Token).ConfigureAwait(false); + + // Only check recovery if we've switched to secondary + if (_activeFlow == _secondary) + { + await CheckPrimaryRecoveryAsync().ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + // Expected when shutting down + } + } + + private async Task CheckPrimaryRecoveryAsync() + { + try + { + // Try to write a minimal test log to check if primary is healthy + var testEvent = new LogEvent + { + Level = LogLevel.Trace, + Message = new ReadOnlyMemory("recovery_check".ToCharArray()), + Timestamp = DateTime.UtcNow.Ticks, + Category = "RecoveryDetection", + ThreadId = Thread.CurrentThread.ManagedThreadId + }; + + var result = await _primary.BlastAsync(testEvent, _recoveryCancellation.Token).ConfigureAwait(false); + + if (result == WriteResult.Success) + { + lock (_syncLock) + { + if (_activeFlow == _secondary) + { + _activeFlow = _primary; + Interlocked.Exchange(ref _consecutiveFailures, 0); + _lastRecoveryCheck = DateTime.UtcNow; + } + } + } + } + catch + { + // Silently catch exceptions during recovery check + // The primary is still unhealthy + } + } } } diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs index d0d6d4f..51b3c4a 100644 --- a/EonaCat.LogStack/LogBuilder.cs +++ b/EonaCat.LogStack/LogBuilder.cs @@ -433,7 +433,7 @@ public sealed class LogBuilder /// /// Adds a Failover flow that switches to a secondary flow if the primary fails. /// - public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow) + public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5) { if (primaryFlow == null) { @@ -445,7 +445,7 @@ public sealed class LogBuilder throw new ArgumentNullException(nameof(secondaryFlow)); } - _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow)); + _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold)); return this; } diff --git a/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj index 95cedb5..68e0551 100644 --- a/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj +++ b/Testers/EonaCat.LogStack.Test.Web/EonaCat.LogStack.Test.Web.csproj @@ -7,11 +7,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - +