using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
[assembly: InternalsVisibleTo("NT8.Core.Tests")]
[assembly: InternalsVisibleTo("NT8.Integration.Tests")]
namespace NT8.Core.Execution
{
///
/// Circuit breaker implementation for execution systems to prevent cascading failures
///
public class ExecutionCircuitBreaker
{
private readonly ILogger _logger;
private readonly NT8.Core.Logging.ILogger _sdkLogger;
private readonly object _lock = new object();
private CircuitBreakerStatus _status;
private DateTime _lastFailureTime;
private int _failureCount;
private DateTime _nextRetryTime;
private readonly TimeSpan _timeout;
private readonly int _failureThreshold;
private readonly TimeSpan _retryTimeout;
private readonly Queue _executionTimes;
private readonly int _latencyWindowSize;
private readonly Queue _rejectionTimes;
private readonly int _rejectionWindowSize;
// Log helpers — route through whichever logger is available
private void LogDebug(string message) { if (_logger != null) _logger.LogDebug(message); else if (_sdkLogger != null) _sdkLogger.LogDebug(message); }
private void LogInfo(string message) { if (_logger != null) _logger.LogInformation(message); else if (_sdkLogger != null) _sdkLogger.LogInformation(message); }
private void LogWarn(string message) { if (_logger != null) _logger.LogWarning(message); else if (_sdkLogger != null) _sdkLogger.LogWarning(message); }
private void LogErr(string message) { if (_logger != null) _logger.LogError(message); else if (_sdkLogger != null) _sdkLogger.LogError(message); }
///
/// Constructor accepting NT8.Core.Logging.ILogger.
/// Use this overload from NinjaScript (.cs) files — no Microsoft.Extensions.Logging reference required.
///
public ExecutionCircuitBreaker(
NT8.Core.Logging.ILogger sdkLogger,
int failureThreshold = 3,
TimeSpan? timeout = null,
TimeSpan? retryTimeout = null,
int latencyWindowSize = 100,
int rejectionWindowSize = 10)
{
_sdkLogger = sdkLogger;
_logger = null;
_status = CircuitBreakerStatus.Closed;
_failureCount = 0;
_lastFailureTime = DateTime.MinValue;
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_retryTimeout = retryTimeout ?? TimeSpan.FromSeconds(5);
_failureThreshold = failureThreshold;
_latencyWindowSize = latencyWindowSize;
_rejectionWindowSize = rejectionWindowSize;
_executionTimes = new Queue();
_rejectionTimes = new Queue();
}
///
/// Constructor accepting Microsoft.Extensions.Logging.ILogger.
/// Use this overload from DLL projects and unit tests.
///
internal ExecutionCircuitBreaker(
ILogger logger,
int failureThreshold = 3,
TimeSpan? timeout = null,
TimeSpan? retryTimeout = null,
int latencyWindowSize = 100,
int rejectionWindowSize = 10)
{
if (logger == null)
throw new ArgumentNullException("logger");
_logger = logger;
_sdkLogger = null;
_status = CircuitBreakerStatus.Closed;
_failureCount = 0;
_lastFailureTime = DateTime.MinValue;
_timeout = timeout ?? TimeSpan.FromSeconds(30);
_retryTimeout = retryTimeout ?? TimeSpan.FromSeconds(5);
_failureThreshold = failureThreshold;
_latencyWindowSize = latencyWindowSize;
_rejectionWindowSize = rejectionWindowSize;
_executionTimes = new Queue();
_rejectionTimes = new Queue();
}
/// Records execution time for latency monitoring.
public void RecordExecutionTime(TimeSpan latency)
{
try
{
lock (_lock)
{
_executionTimes.Enqueue(latency);
while (_executionTimes.Count > _latencyWindowSize)
_executionTimes.Dequeue();
if (_status == CircuitBreakerStatus.Closed && HasExcessiveLatency())
TripCircuitBreaker("Excessive execution latency detected");
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to record execution time: {0}", ex.Message));
throw;
}
}
/// Records an order rejection.
public void RecordOrderRejection(string reason)
{
if (string.IsNullOrEmpty(reason))
reason = "Unknown";
try
{
lock (_lock)
{
_rejectionTimes.Enqueue(DateTime.UtcNow);
while (_rejectionTimes.Count > _rejectionWindowSize)
_rejectionTimes.Dequeue();
if (_status == CircuitBreakerStatus.Closed && HasExcessiveRejections())
TripCircuitBreaker(string.Format("Excessive order rejections: {0}", reason));
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to record order rejection: {0}", ex.Message));
throw;
}
}
/// Returns true if an order should be allowed through.
public bool ShouldAllowOrder()
{
try
{
lock (_lock)
{
switch (_status)
{
case CircuitBreakerStatus.Closed:
return true;
case CircuitBreakerStatus.Open:
if (DateTime.UtcNow >= _nextRetryTime)
{
_status = CircuitBreakerStatus.HalfOpen;
LogWarn("Circuit breaker transitioning to Half-Open state");
return true;
}
LogDebug("Circuit breaker is Open - blocking order");
return false;
case CircuitBreakerStatus.HalfOpen:
LogDebug("Circuit breaker is Half-Open - allowing test order");
return true;
default:
return false;
}
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to check ShouldAllowOrder: {0}", ex.Message));
throw;
}
}
/// Returns the current circuit breaker state.
public CircuitBreakerState GetState()
{
try
{
lock (_lock)
{
return new CircuitBreakerState(
_status != CircuitBreakerStatus.Closed,
_status,
GetStatusReason(),
_failureCount);
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to get state: {0}", ex.Message));
throw;
}
}
/// Resets the circuit breaker to Closed state.
public void Reset()
{
try
{
lock (_lock)
{
_status = CircuitBreakerStatus.Closed;
_failureCount = 0;
_lastFailureTime = DateTime.MinValue;
LogInfo("Circuit breaker reset to Closed state");
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to reset circuit breaker: {0}", ex.Message));
throw;
}
}
/// Call after a successful order submission.
public void OnSuccess()
{
try
{
lock (_lock)
{
if (_status == CircuitBreakerStatus.HalfOpen)
{
Reset();
LogInfo("Circuit breaker reset after successful test operation");
}
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to handle OnSuccess: {0}", ex.Message));
throw;
}
}
/// Call after a failed order submission.
public void OnFailure()
{
try
{
lock (_lock)
{
_failureCount++;
_lastFailureTime = DateTime.UtcNow;
if (_status == CircuitBreakerStatus.HalfOpen ||
(_status == CircuitBreakerStatus.Closed && _failureCount >= _failureThreshold))
{
TripCircuitBreaker("Failure threshold exceeded");
}
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to handle OnFailure: {0}", ex.Message));
throw;
}
}
private void TripCircuitBreaker(string reason)
{
_status = CircuitBreakerStatus.Open;
_nextRetryTime = DateTime.UtcNow.Add(_timeout);
LogWarn(string.Format("Circuit breaker TRIPPED: {0}. Will retry at {1}", reason, _nextRetryTime));
}
private bool HasExcessiveLatency()
{
if (_executionTimes.Count < 3)
return false;
var avgLatency = TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
return avgLatency.TotalSeconds > 5.0;
}
private bool HasExcessiveRejections()
{
if (_rejectionTimes.Count < _rejectionWindowSize)
return false;
var recentWindow = TimeSpan.FromMinutes(1);
var recentRejections = _rejectionTimes.Count(dt => DateTime.UtcNow - dt <= recentWindow);
return recentRejections >= _rejectionWindowSize;
}
private string GetStatusReason()
{
switch (_status)
{
case CircuitBreakerStatus.Closed:
return "Normal operation";
case CircuitBreakerStatus.Open:
return string.Format("Tripped due to failures. Count: {0}, Last: {1}", _failureCount, _lastFailureTime);
case CircuitBreakerStatus.HalfOpen:
return "Testing recovery after timeout";
default:
return "Unknown";
}
}
/// Returns average execution latency.
public TimeSpan GetAverageExecutionTime()
{
try
{
lock (_lock)
{
if (_executionTimes.Count == 0)
return TimeSpan.Zero;
return TimeSpan.FromMilliseconds(_executionTimes.Average(ts => ts.TotalMilliseconds));
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to get average execution time: {0}", ex.Message));
throw;
}
}
/// Returns rejection rate as a percentage.
public double GetRejectionRate()
{
try
{
lock (_lock)
{
if (_rejectionTimes.Count == 0)
return 0.0;
var oneMinuteAgo = DateTime.UtcNow.AddMinutes(-1);
var recentRejections = _rejectionTimes.Count(dt => dt >= oneMinuteAgo);
return (double)recentRejections / _rejectionWindowSize * 100.0;
}
}
catch (Exception ex)
{
LogErr(string.Format("Failed to get rejection rate: {0}", ex.Message));
throw;
}
}
}
}