feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration

- Add RateLimitConfig for configuration management with YAML binding support.
- Introduce RateLimitDecision to encapsulate the result of rate limit checks.
- Implement RateLimitMetrics for OpenTelemetry metrics tracking.
- Create RateLimitMiddleware for enforcing rate limits on incoming requests.
- Develop RateLimitService to orchestrate instance and environment rate limit checks.
- Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

@@ -0,0 +1,249 @@
// -----------------------------------------------------------------------------
// RateLimitConfig.cs
// Sprint: SPRINT_1200_001_001_router_rate_limiting_core
// Task: 1.1 - Rate Limit Configuration Models
// Description: Root configuration class with YAML binding support
// -----------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
namespace StellaOps.Router.Gateway.RateLimit;
/// <summary>
/// Root configuration for Router rate limiting.
/// Per advisory "Designing 202 + Retry-After Backpressure Control".
/// </summary>
public sealed class RateLimitConfig
{
/// <summary>
/// Activation gate: only check Valkey when traffic exceeds this threshold per 5 minutes.
/// Set to 0 to always check Valkey. Default: 5000.
/// </summary>
[ConfigurationKeyName("process_back_pressure_when_more_than_per_5min")]
public int ActivationThresholdPer5Min { get; set; } = 5000;
/// <summary>
/// Instance-level rate limits (in-memory, per router instance).
/// </summary>
[ConfigurationKeyName("for_instance")]
public InstanceLimitsConfig? ForInstance { get; set; }
/// <summary>
/// Environment-level rate limits (Valkey-backed, across all router instances).
/// </summary>
[ConfigurationKeyName("for_environment")]
public EnvironmentLimitsConfig? ForEnvironment { get; set; }
/// <summary>
/// Typo alias support for backwards compatibility.
/// </summary>
[ConfigurationKeyName("back_pressure_limtis")]
public RateLimitsSection? BackPressureLimtis { get; set; }
/// <summary>
/// Load configuration from IConfiguration.
/// </summary>
public static RateLimitConfig Load(IConfiguration configuration)
{
var config = new RateLimitConfig();
configuration.Bind("rate_limiting", config);
return config.Validate();
}
/// <summary>
/// Validate configuration values.
/// </summary>
public RateLimitConfig Validate()
{
if (ActivationThresholdPer5Min < 0)
throw new ArgumentException("Activation threshold must be >= 0", nameof(ActivationThresholdPer5Min));
ForInstance?.Validate("for_instance");
ForEnvironment?.Validate("for_environment");
return this;
}
/// <summary>
/// Whether rate limiting is enabled (at least one scope configured).
/// </summary>
public bool IsEnabled => ForInstance is not null || ForEnvironment is not null;
}
/// <summary>
/// Instance-level rate limit configuration (in-memory).
/// </summary>
public sealed class InstanceLimitsConfig
{
/// <summary>Time window in seconds.</summary>
[ConfigurationKeyName("per_seconds")]
public int PerSeconds { get; set; }
/// <summary>Maximum requests in the time window.</summary>
[ConfigurationKeyName("max_requests")]
public int MaxRequests { get; set; }
/// <summary>Burst window in seconds.</summary>
[ConfigurationKeyName("allow_burst_for_seconds")]
public int AllowBurstForSeconds { get; set; }
/// <summary>Maximum burst requests.</summary>
[ConfigurationKeyName("allow_max_burst_requests")]
public int AllowMaxBurstRequests { get; set; }
/// <summary>Typo alias for backwards compatibility.</summary>
[ConfigurationKeyName("allow_max_bust_requests")]
public int AllowMaxBustRequests { get; set; }
/// <summary>
/// Validate configuration.
/// </summary>
public void Validate(string path)
{
if (PerSeconds < 0 || MaxRequests < 0)
throw new ArgumentException($"{path}: Window (per_seconds) and limit (max_requests) must be >= 0");
if (AllowBurstForSeconds < 0 || AllowMaxBurstRequests < 0)
throw new ArgumentException($"{path}: Burst window and limit must be >= 0");
// Normalize typo alias
if (AllowMaxBustRequests > 0 && AllowMaxBurstRequests == 0)
AllowMaxBurstRequests = AllowMaxBustRequests;
}
}
/// <summary>
/// Environment-level rate limit configuration (Valkey-backed).
/// </summary>
public sealed class EnvironmentLimitsConfig
{
/// <summary>Valkey connection string.</summary>
[ConfigurationKeyName("valkey_connection")]
public string ValkeyConnection { get; set; } = "localhost:6379";
/// <summary>Valkey bucket/prefix for rate limit keys.</summary>
[ConfigurationKeyName("valkey_bucket")]
public string ValkeyBucket { get; set; } = "stella-router-rate-limit";
/// <summary>Circuit breaker configuration.</summary>
[ConfigurationKeyName("circuit_breaker")]
public CircuitBreakerConfig? CircuitBreaker { get; set; }
/// <summary>Time window in seconds.</summary>
[ConfigurationKeyName("per_seconds")]
public int PerSeconds { get; set; }
/// <summary>Maximum requests in the time window.</summary>
[ConfigurationKeyName("max_requests")]
public int MaxRequests { get; set; }
/// <summary>Burst window in seconds.</summary>
[ConfigurationKeyName("allow_burst_for_seconds")]
public int AllowBurstForSeconds { get; set; }
/// <summary>Maximum burst requests.</summary>
[ConfigurationKeyName("allow_max_burst_requests")]
public int AllowMaxBurstRequests { get; set; }
/// <summary>Per-microservice overrides.</summary>
[ConfigurationKeyName("microservices")]
public Dictionary<string, MicroserviceLimitsConfig>? Microservices { get; set; }
/// <summary>
/// Validate configuration.
/// </summary>
public void Validate(string path)
{
if (string.IsNullOrWhiteSpace(ValkeyConnection))
throw new ArgumentException($"{path}: valkey_connection is required");
if (PerSeconds < 0 || MaxRequests < 0)
throw new ArgumentException($"{path}: Window and limit must be >= 0");
CircuitBreaker?.Validate($"{path}.circuit_breaker");
if (Microservices is not null)
{
foreach (var (name, config) in Microservices)
{
config.Validate($"{path}.microservices.{name}");
}
}
}
}
/// <summary>
/// Per-microservice rate limit overrides.
/// </summary>
public sealed class MicroserviceLimitsConfig
{
/// <summary>Time window in seconds.</summary>
[ConfigurationKeyName("per_seconds")]
public int PerSeconds { get; set; }
/// <summary>Maximum requests in the time window.</summary>
[ConfigurationKeyName("max_requests")]
public int MaxRequests { get; set; }
/// <summary>Burst window in seconds (optional).</summary>
[ConfigurationKeyName("allow_burst_for_seconds")]
public int? AllowBurstForSeconds { get; set; }
/// <summary>Maximum burst requests (optional).</summary>
[ConfigurationKeyName("allow_max_burst_requests")]
public int? AllowMaxBurstRequests { get; set; }
/// <summary>
/// Validate configuration.
/// </summary>
public void Validate(string path)
{
if (PerSeconds < 0 || MaxRequests < 0)
throw new ArgumentException($"{path}: Window and limit must be >= 0");
}
}
/// <summary>
/// Circuit breaker configuration for Valkey resilience.
/// </summary>
public sealed class CircuitBreakerConfig
{
/// <summary>Number of failures before opening the circuit.</summary>
[ConfigurationKeyName("failure_threshold")]
public int FailureThreshold { get; set; } = 5;
/// <summary>Seconds to keep circuit open.</summary>
[ConfigurationKeyName("timeout_seconds")]
public int TimeoutSeconds { get; set; } = 30;
/// <summary>Seconds in half-open state before full reset.</summary>
[ConfigurationKeyName("half_open_timeout")]
public int HalfOpenTimeout { get; set; } = 10;
/// <summary>
/// Validate configuration.
/// </summary>
public void Validate(string path)
{
if (FailureThreshold < 1)
throw new ArgumentException($"{path}: failure_threshold must be >= 1");
if (TimeoutSeconds < 1)
throw new ArgumentException($"{path}: timeout_seconds must be >= 1");
if (HalfOpenTimeout < 1)
throw new ArgumentException($"{path}: half_open_timeout must be >= 1");
}
}
/// <summary>
/// Generic rate limits section (for typo alias support).
/// </summary>
public sealed class RateLimitsSection
{
[ConfigurationKeyName("per_seconds")]
public int PerSeconds { get; set; }
[ConfigurationKeyName("max_requests")]
public int MaxRequests { get; set; }
}