Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
b47641feaa
commit
4f488bae45
78 changed files with 3309 additions and 1570 deletions
131
dotnet/src/TwitchArchive.Core/Recovery/RecoveryPolicy.cs
Normal file
131
dotnet/src/TwitchArchive.Core/Recovery/RecoveryPolicy.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
using System;
|
||||
|
||||
namespace TwitchArchive.Core.Recovery
|
||||
{
|
||||
public enum RecoveryState
|
||||
{
|
||||
Monitoring,
|
||||
Recording,
|
||||
FastReconnect,
|
||||
SlowReconnect,
|
||||
NetworkFault
|
||||
}
|
||||
|
||||
public enum RecoveryAction
|
||||
{
|
||||
None,
|
||||
StartRecording,
|
||||
StartProcessing,
|
||||
SleepOnly
|
||||
}
|
||||
|
||||
public sealed class RecoveryDecision
|
||||
{
|
||||
public RecoveryAction Action { get; init; }
|
||||
public TimeSpan Sleep { get; init; }
|
||||
|
||||
public static RecoveryDecision SleepFor(TimeSpan t) => new RecoveryDecision { Action = RecoveryAction.SleepOnly, Sleep = t };
|
||||
public static RecoveryDecision StartRecordingNow() => new RecoveryDecision { Action = RecoveryAction.StartRecording, Sleep = TimeSpan.Zero };
|
||||
public static RecoveryDecision StartProcessingAndSleep(TimeSpan sleep) => new RecoveryDecision { Action = RecoveryAction.StartProcessing, Sleep = sleep };
|
||||
public static RecoveryDecision None() => new RecoveryDecision { Action = RecoveryAction.None, Sleep = TimeSpan.Zero };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure, testable recovery policy state machine.
|
||||
/// It decides whether to start recording immediately, wait a short period (fast reconnect),
|
||||
/// wait longer (slow reconnect), or enter a network fault backoff.
|
||||
///</summary>
|
||||
public class RecoveryPolicy
|
||||
{
|
||||
private RecoveryState _state = RecoveryState.Monitoring;
|
||||
private readonly TimeSpan _refreshInterval;
|
||||
private DateTime? _fastReconnectStartUtc;
|
||||
private int _networkFaultAttempts;
|
||||
|
||||
// constants matching requested behavior
|
||||
private static readonly TimeSpan FastReconnectInterval = TimeSpan.FromSeconds(10);
|
||||
private static readonly TimeSpan FastReconnectWindow = TimeSpan.FromMinutes(2);
|
||||
private static readonly TimeSpan SlowReconnectInterval = TimeSpan.FromSeconds(60);
|
||||
private static readonly TimeSpan NetworkFaultBase = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan NetworkFaultMax = TimeSpan.FromMinutes(10);
|
||||
|
||||
public RecoveryPolicy(TimeSpan? refreshInterval = null)
|
||||
{
|
||||
_refreshInterval = refreshInterval ?? TimeSpan.FromSeconds(60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the policy given current time, whether the streamer is live (null = unknown),
|
||||
/// and whether there was a network error reaching Twitch APIs.
|
||||
/// Returns a RecoveryDecision describing the next action and how long to wait if sleeping.
|
||||
/// </summary>
|
||||
public RecoveryDecision Tick(DateTime nowUtc, bool? isLive, bool networkError)
|
||||
{
|
||||
// Network error handling: enter NetworkFault state with exponential backoff
|
||||
if (networkError)
|
||||
{
|
||||
_networkFaultAttempts++;
|
||||
_state = RecoveryState.NetworkFault;
|
||||
var backoff = NetworkFaultBase * Math.Pow(2, Math.Max(0, _networkFaultAttempts - 1));
|
||||
if (backoff > NetworkFaultMax) backoff = NetworkFaultMax;
|
||||
return RecoveryDecision.SleepFor(backoff);
|
||||
}
|
||||
|
||||
// If we recover from network fault, reset attempts
|
||||
if (_state == RecoveryState.NetworkFault && !networkError)
|
||||
{
|
||||
_networkFaultAttempts = 0;
|
||||
_state = RecoveryState.Monitoring;
|
||||
}
|
||||
|
||||
// If live -> start recording immediately
|
||||
if (isLive == true)
|
||||
{
|
||||
_state = RecoveryState.Recording;
|
||||
_fastReconnectStartUtc = null;
|
||||
return RecoveryDecision.StartRecordingNow();
|
||||
}
|
||||
|
||||
// If unknown live status, treat conservatively: sleep a short amount (refresh)
|
||||
if (isLive == null)
|
||||
{
|
||||
return RecoveryDecision.SleepFor(_refreshInterval);
|
||||
}
|
||||
|
||||
// isLive == false here
|
||||
switch (_state)
|
||||
{
|
||||
case RecoveryState.Recording:
|
||||
// Just transitioned from recording to not-live: start fast reconnect window
|
||||
_state = RecoveryState.FastReconnect;
|
||||
_fastReconnectStartUtc = nowUtc;
|
||||
return RecoveryDecision.SleepFor(FastReconnectInterval);
|
||||
|
||||
case RecoveryState.FastReconnect:
|
||||
if (_fastReconnectStartUtc.HasValue && (nowUtc - _fastReconnectStartUtc.Value) < FastReconnectWindow)
|
||||
{
|
||||
// stay in fast reconnect, poll frequently
|
||||
return RecoveryDecision.SleepFor(FastReconnectInterval);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fast reconnect window expired -> move to slow reconnect and trigger processing
|
||||
_state = RecoveryState.SlowReconnect;
|
||||
_fastReconnectStartUtc = null;
|
||||
return RecoveryDecision.StartProcessingAndSleep(SlowReconnectInterval);
|
||||
}
|
||||
|
||||
case RecoveryState.SlowReconnect:
|
||||
// In slow reconnect, poll less frequently
|
||||
return RecoveryDecision.SleepFor(SlowReconnectInterval);
|
||||
|
||||
case RecoveryState.Monitoring:
|
||||
default:
|
||||
// Normal monitoring cadence
|
||||
return RecoveryDecision.SleepFor(_refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
public RecoveryState CurrentState => _state;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue