131 lines
5.4 KiB
C#
131 lines
5.4 KiB
C#
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;
|
|
}
|
|
}
|