# Step 25: Configuration Hot-Reload **Phase 7: Testing & Documentation** **Estimated Complexity:** Medium **Dependencies:** All previous configuration steps --- ## Overview Configuration hot-reload enables dynamic updates to router and microservice configuration without restarts. This includes route definitions, rate limits, circuit breaker settings, and JWKS rotation. --- ## Goals 1. Support YAML configuration hot-reload 2. Implement file watcher for configuration changes 3. Provide atomic configuration updates 4. Support validation before applying changes 5. Enable rollback on invalid configuration --- ## Configuration Watcher ```csharp namespace StellaOps.Router.Configuration; /// /// Watches configuration files for changes and triggers reloads. /// public sealed class ConfigurationWatcher : IHostedService, IDisposable { private readonly IConfiguration _configuration; private readonly IOptionsMonitor _routerConfig; private readonly ILogger _logger; private readonly List _watchers = new(); private readonly Subject _changes = new(); private readonly TimeSpan _debounceInterval = TimeSpan.FromMilliseconds(500); private readonly ConcurrentDictionary _lastChange = new(); public IObservable Changes => _changes; public ConfigurationWatcher( IConfiguration configuration, IOptionsMonitor routerConfig, ILogger logger) { _configuration = configuration; _routerConfig = routerConfig; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { // Watch all YAML configuration files var configPaths = GetConfigurationFilePaths(); foreach (var path in configPaths) { if (!File.Exists(path)) continue; var directory = Path.GetDirectoryName(path)!; var fileName = Path.GetFileName(path); var watcher = new FileSystemWatcher(directory) { Filter = fileName, NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size, EnableRaisingEvents = true }; watcher.Changed += OnConfigurationFileChanged; _watchers.Add(watcher); _logger.LogInformation("Watching configuration file: {Path}", path); } // Also subscribe to IOptionsMonitor for programmatic changes _routerConfig.OnChange(config => { _changes.OnNext(new ConfigurationChange { Section = "Router", ChangeType = ChangeType.Modified, Timestamp = DateTimeOffset.UtcNow }); }); return Task.CompletedTask; } private void OnConfigurationFileChanged(object sender, FileSystemEventArgs e) { // Debounce rapid changes var now = DateTimeOffset.UtcNow; if (_lastChange.TryGetValue(e.FullPath, out var lastChange) && now - lastChange < _debounceInterval) { return; } _lastChange[e.FullPath] = now; _logger.LogInformation("Configuration file changed: {Path}", e.FullPath); // Delay to allow file writes to complete Task.Delay(100).ContinueWith(_ => { try { // Validate configuration before notifying if (ValidateConfiguration(e.FullPath)) { _changes.OnNext(new ConfigurationChange { Section = DetermineSectionFromPath(e.FullPath), ChangeType = ChangeType.Modified, FilePath = e.FullPath, Timestamp = now }); } } catch (Exception ex) { _logger.LogError(ex, "Failed to process configuration change for {Path}", e.FullPath); } }); } private bool ValidateConfiguration(string path) { try { var yaml = File.ReadAllText(path); var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); // Try to deserialize to validate YAML syntax var doc = deserializer.Deserialize>(yaml); return doc != null; } catch (Exception ex) { _logger.LogWarning(ex, "Invalid configuration file: {Path}", path); return false; } } private string DetermineSectionFromPath(string path) { var fileName = Path.GetFileNameWithoutExtension(path).ToLower(); return fileName switch { "router" => "Router", "routes" => "Routes", "ratelimits" => "RateLimits", "endpoints" => "Endpoints", _ => "Unknown" }; } private IEnumerable GetConfigurationFilePaths() { // Get paths from configuration providers var paths = new List(); if (_configuration is IConfigurationRoot root) { foreach (var provider in root.Providers) { if (provider is FileConfigurationProvider fileProvider) { var source = fileProvider.Source; if (source.FileProvider?.GetFileInfo(source.Path ?? "") is { Exists: true } fileInfo) { paths.Add(fileInfo.PhysicalPath ?? ""); } } } } return paths.Where(p => !string.IsNullOrEmpty(p)); } public Task StopAsync(CancellationToken cancellationToken) { foreach (var watcher in _watchers) { watcher.EnableRaisingEvents = false; } return Task.CompletedTask; } public void Dispose() { foreach (var watcher in _watchers) { watcher.Dispose(); } _changes.Dispose(); } } public sealed class ConfigurationChange { public string Section { get; init; } = ""; public ChangeType ChangeType { get; init; } public string? FilePath { get; init; } public DateTimeOffset Timestamp { get; init; } } public enum ChangeType { Added, Modified, Removed } ``` --- ## Route Configuration Reloader ```csharp namespace StellaOps.Router.Configuration; /// /// Handles hot-reload of route configurations. /// public sealed class RouteConfigurationReloader : IHostedService { private readonly ConfigurationWatcher _watcher; private readonly IRouteRegistry _routeRegistry; private readonly ILogger _logger; private IDisposable? _subscription; public RouteConfigurationReloader( ConfigurationWatcher watcher, IRouteRegistry routeRegistry, ILogger logger) { _watcher = watcher; _routeRegistry = routeRegistry; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _subscription = _watcher.Changes .Where(c => c.Section == "Routes") .Subscribe(OnRoutesChanged); return Task.CompletedTask; } private void OnRoutesChanged(ConfigurationChange change) { _logger.LogInformation("Reloading routes from {Path}", change.FilePath); try { _routeRegistry.Reload(); _logger.LogInformation("Routes reloaded successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to reload routes, keeping previous configuration"); } } public Task StopAsync(CancellationToken cancellationToken) { _subscription?.Dispose(); return Task.CompletedTask; } } ``` --- ## Rate Limit Configuration Reloader ```csharp namespace StellaOps.Router.Configuration; /// /// Handles hot-reload of rate limit configurations. /// public sealed class RateLimitConfigurationReloader : IHostedService { private readonly ConfigurationWatcher _watcher; private readonly IRateLimiter _rateLimiter; private readonly IOptionsMonitor _config; private readonly ILogger _logger; private IDisposable? _subscription; public RateLimitConfigurationReloader( ConfigurationWatcher watcher, IRateLimiter rateLimiter, IOptionsMonitor config, ILogger logger) { _watcher = watcher; _rateLimiter = rateLimiter; _config = config; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _subscription = _watcher.Changes .Where(c => c.Section == "RateLimits") .Subscribe(OnRateLimitsChanged); _config.OnChange(OnRateLimitConfigChanged); return Task.CompletedTask; } private void OnRateLimitsChanged(ConfigurationChange change) { _logger.LogInformation("Rate limit configuration changed, applying updates"); ApplyRateLimitChanges(); } private void OnRateLimitConfigChanged(RateLimitConfig config) { _logger.LogInformation("Rate limit options changed, applying updates"); ApplyRateLimitChanges(); } private void ApplyRateLimitChanges() { try { // Rate limiter will pick up new config from IOptionsMonitor // Clear any cached tier information if (_rateLimiter is ICacheableRateLimiter cacheable) { cacheable.ClearCache(); } _logger.LogInformation("Rate limit configuration applied successfully"); } catch (Exception ex) { _logger.LogError(ex, "Failed to apply rate limit changes"); } } public Task StopAsync(CancellationToken cancellationToken) { _subscription?.Dispose(); return Task.CompletedTask; } } public interface ICacheableRateLimiter { void ClearCache(); } ``` --- ## JWKS Hot-Reload ```csharp namespace StellaOps.Router.Configuration; /// /// Handles JWKS rotation and cache refresh. /// public sealed class JwksReloader : IHostedService { private readonly IJwksCache _jwksCache; private readonly JwtAuthenticationConfig _config; private readonly ILogger _logger; private Timer? _refreshTimer; public JwksReloader( IJwksCache jwksCache, IOptions config, ILogger logger) { _jwksCache = jwksCache; _config = config.Value; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { // Periodic refresh of JWKS var interval = _config.JwksRefreshInterval; _refreshTimer = new Timer( RefreshJwks, null, interval, interval); _logger.LogInformation( "JWKS refresh scheduled every {Interval}", interval); return Task.CompletedTask; } private async void RefreshJwks(object? state) { try { _logger.LogDebug("Refreshing JWKS cache"); await _jwksCache.RefreshAsync(CancellationToken.None); _logger.LogDebug("JWKS cache refreshed successfully"); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to refresh JWKS cache, will retry"); } } public Task StopAsync(CancellationToken cancellationToken) { _refreshTimer?.Dispose(); return Task.CompletedTask; } } ``` --- ## Configuration Validation ```csharp namespace StellaOps.Router.Configuration; /// /// Validates configuration before applying changes. /// public interface IConfigurationValidator { ValidationResult Validate(T config) where T : class; } public sealed class ConfigurationValidator : IConfigurationValidator { private readonly ILogger _logger; public ConfigurationValidator(ILogger logger) { _logger = logger; } public ValidationResult Validate(T config) where T : class { var errors = new List(); // Use data annotations validation var context = new ValidationContext(config); var results = new List(); if (!Validator.TryValidateObject(config, context, results, validateAllProperties: true)) { errors.AddRange(results.Select(r => r.ErrorMessage ?? "Unknown validation error")); } // Type-specific validation errors.AddRange(config switch { RouterConfig router => ValidateRouterConfig(router), RateLimitConfig rateLimit => ValidateRateLimitConfig(rateLimit), _ => Enumerable.Empty() }); if (errors.Any()) { _logger.LogWarning( "Configuration validation failed: {Errors}", string.Join(", ", errors)); } return new ValidationResult { IsValid = !errors.Any(), Errors = errors }; } private IEnumerable ValidateRouterConfig(RouterConfig config) { if (config.MaxPayloadSize <= 0) yield return "MaxPayloadSize must be positive"; if (config.RequestTimeout <= TimeSpan.Zero) yield return "RequestTimeout must be positive"; } private IEnumerable ValidateRateLimitConfig(RateLimitConfig config) { foreach (var (tier, limits) in config.Tiers) { if (limits.RequestsPerMinute <= 0) yield return $"Tier {tier}: RequestsPerMinute must be positive"; } } } public sealed class ValidationResult { public bool IsValid { get; init; } public IReadOnlyList Errors { get; init; } = Array.Empty(); } ``` --- ## Atomic Configuration Update ```csharp namespace StellaOps.Router.Configuration; /// /// Provides atomic configuration updates with rollback support. /// public sealed class AtomicConfigurationUpdater { private readonly IConfigurationValidator _validator; private readonly ILogger _logger; private readonly ReaderWriterLockSlim _lock = new(); public AtomicConfigurationUpdater( IConfigurationValidator validator, ILogger logger) { _validator = validator; _logger = logger; } /// /// Atomically updates configuration with validation and rollback. /// public async Task UpdateAsync( T currentConfig, T newConfig, Func applyAction, Func? rollbackAction = null) where T : class { // Validate new configuration var validation = _validator.Validate(newConfig); if (!validation.IsValid) { _logger.LogWarning( "Configuration update rejected: {Errors}", string.Join(", ", validation.Errors)); return false; } _lock.EnterWriteLock(); try { // Store current config for rollback var backup = currentConfig; try { await applyAction(newConfig); _logger.LogInformation("Configuration updated successfully"); return true; } catch (Exception ex) { _logger.LogError(ex, "Configuration update failed, rolling back"); if (rollbackAction != null) { try { await rollbackAction(backup); _logger.LogInformation("Configuration rolled back successfully"); } catch (Exception rollbackEx) { _logger.LogError(rollbackEx, "Rollback failed!"); } } return false; } } finally { _lock.ExitWriteLock(); } } } ``` --- ## Configuration API Endpoints ```csharp namespace StellaOps.Router.Gateway; /// /// API endpoints for configuration management. /// public static class ConfigurationEndpoints { public static IEndpointRouteBuilder MapConfigurationEndpoints( this IEndpointRouteBuilder endpoints, string basePath = "/api/config") { var group = endpoints.MapGroup(basePath) .RequireAuthorization("admin"); group.MapGet("/", GetConfiguration); group.MapGet("/{section}", GetConfigurationSection); group.MapPost("/reload", ReloadConfiguration); group.MapPost("/validate", ValidateConfiguration); return endpoints; } private static async Task GetConfiguration( IConfiguration configuration) { var sections = new Dictionary(); foreach (var child in configuration.GetChildren()) { sections[child.Key] = GetSectionValue(child); } return Results.Ok(sections); } private static object GetSectionValue(IConfigurationSection section) { var children = section.GetChildren().ToList(); if (!children.Any()) { return section.Value ?? ""; } if (children.All(c => int.TryParse(c.Key, out _))) { // Array return children.Select(c => GetSectionValue(c)).ToList(); } // Object return children.ToDictionary(c => c.Key, c => GetSectionValue(c)); } private static IResult GetConfigurationSection( string section, IConfiguration configuration) { var configSection = configuration.GetSection(section); if (!configSection.Exists()) { return Results.NotFound(new { error = $"Section '{section}' not found" }); } return Results.Ok(GetSectionValue(configSection)); } private static async Task ReloadConfiguration( ConfigurationWatcher watcher, ILogger logger) { logger.LogInformation("Manual configuration reload triggered"); // Trigger reload notification // In practice, would re-read configuration files return Results.Ok(new { message = "Configuration reload triggered" }); } private static async Task ValidateConfiguration( HttpRequest request, IConfigurationValidator validator) { var body = await request.ReadFromJsonAsync>(); if (body == null) { return Results.BadRequest(new { error = "Invalid request body" }); } // Basic syntax validation return Results.Ok(new { valid = true }); } } ``` --- ## YAML Configuration ```yaml Configuration: # Enable hot-reload HotReload: Enabled: true DebounceInterval: "00:00:00.500" ValidateBeforeApply: true # Files to watch WatchPaths: - "/etc/stellaops/router.yaml" - "/etc/stellaops/routes.yaml" - "/etc/stellaops/ratelimits.yaml" # JWKS refresh settings Jwks: RefreshInterval: "00:05:00" RefreshOnError: true MaxRetries: 3 ``` --- ## Deliverables 1. `StellaOps.Router.Configuration/ConfigurationWatcher.cs` 2. `StellaOps.Router.Configuration/RouteConfigurationReloader.cs` 3. `StellaOps.Router.Configuration/RateLimitConfigurationReloader.cs` 4. `StellaOps.Router.Configuration/JwksReloader.cs` 5. `StellaOps.Router.Configuration/IConfigurationValidator.cs` 6. `StellaOps.Router.Configuration/ConfigurationValidator.cs` 7. `StellaOps.Router.Configuration/AtomicConfigurationUpdater.cs` 8. `StellaOps.Router.Gateway/ConfigurationEndpoints.cs` 9. Configuration reload tests 10. Validation tests --- ## Next Step Proceed to [Step 26: End-to-End Testing](26-Step.md) to implement comprehensive integration tests.