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:
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user