Audit 2026-05-02 (Tâche 6 Phase A) had identified 3 inconsistent formats across the ADR corpus : - F1 list bullets : `* Status:` / `* Date:` / `* Deciders:` (11 ADRs) - F2 bold fields : `**Status:**` / `**Date:**` / `**Authors:**` (9 ADRs) - F3 dedicated section : `## Status\n**Value** ✅` (5 ADRs) Mixed metadata names (Authors / Deciders / Decision Date / Implementation Date / Implementation Status / Last Updated) and decorative emojis on status values made the corpus hard to scan or template against. Canonical format adopted (see adr/README.md for full template) : # NN. Title **Status:** <Proposed|Accepted|Implemented|Partially Implemented| Approved|Rejected|Deferred|Deprecated|Superseded by ADR-NNNN> **Date:** YYYY-MM-DD **Authors:** Name(s) [optional **Field:** ... lines] ## Context... Transformations applied (via /tmp/homogenize-adrs.py) : - F1 list bullets → bold fields - F2 cleanup : `**Deciders:**` → `**Authors:**`, strip status emojis - F3 sections : `## Status\n**Value** ✅` → `**Status:** Value` - Strip decorative emojis from `**Status:**` and `**Implementation Status:**` - Convert any `* Implementation Status:` / `* Last Updated:` / `* Decision Drivers:` / `* Decision Date:` to bold equivalents - Date typo fix : `2024-04-XX` → `2026-04-XX` for ADRs 0018, 0019 (already noted in PR #17 but here re-applied since branch starts from origin/main pre-PR17) - Normalize multiple blank lines after header (max 1) 21 / 23 ADRs modified. 0010 and 0012 were already conform. 0011 and 0014 do not exist in the repo (cf. README index update). Body content of each ADR is preserved unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
264 lines
8.7 KiB
Markdown
264 lines
8.7 KiB
Markdown
# Config Hot Reloading Strategy
|
|
|
|
**Status:** Proposed
|
|
**Authors:** Gabriel Radureau, AI Agent
|
|
**Date:** 2026-04-05
|
|
|
|
## Context and Problem Statement
|
|
|
|
The dance-lessons-coach application currently loads configuration once at startup using Viper, which supports file-based configuration, environment variables, and defaults. However, the current implementation does not support runtime configuration changes without restarting the application.
|
|
|
|
We need to determine whether and how to implement config hot reloading - the ability to detect changes to the optional `config.yaml` file and apply those changes without requiring a full application restart.
|
|
|
|
## Decision Drivers
|
|
|
|
* **Development convenience**: Hot reloading would allow developers to change configuration without restarting the server during development
|
|
* **Production flexibility**: Ability to adjust certain configuration parameters without downtime
|
|
* **Complexity**: Hot reloading adds significant complexity to the codebase
|
|
* **Safety**: Some configuration changes require careful handling to avoid runtime errors
|
|
* **Viper capabilities**: Viper already supports file watching through `viper.WatchConfig()`
|
|
* **Configuration scope**: Not all configuration parameters can or should be hot-reloaded
|
|
|
|
## Considered Options
|
|
|
|
### Option 1: Full Hot Reloading with Viper WatchConfig
|
|
|
|
Implement comprehensive hot reloading using Viper's built-in `WatchConfig()` functionality to monitor the config file and automatically reload when changes are detected.
|
|
|
|
### Option 2: Selective Hot Reloading
|
|
|
|
Only allow hot reloading for specific configuration sections that are safe to change at runtime (e.g., logging level, feature flags) while requiring restart for others (e.g., server host/port, database credentials).
|
|
|
|
### Option 3: Manual Reload Endpoint
|
|
|
|
Add an admin endpoint (e.g., `POST /api/admin/reload-config`) that triggers configuration reload when called, giving explicit control over when reloading happens.
|
|
|
|
### Option 4: No Hot Reloading
|
|
|
|
Maintain the current approach of loading configuration only at startup, requiring application restart for any configuration changes.
|
|
|
|
## Decision Outcome
|
|
|
|
Chosen option: **"Selective Hot Reloading"** because it provides the benefits of runtime configuration changes while maintaining safety and control. This approach:
|
|
|
|
* Allows safe configuration changes without restart
|
|
* Prevents dangerous runtime changes to critical parameters
|
|
* Leverages Viper's existing capabilities
|
|
* Provides a clear boundary between hot-reloadable and non-hot-reloadable settings
|
|
|
|
## Implementation Strategy
|
|
|
|
### Hot-Reloadable Configuration
|
|
|
|
The following configuration parameters will support hot reloading:
|
|
|
|
* **Logging level** (`logging.level`)
|
|
* **Feature flags** (`api.v2_enabled`)
|
|
* **Telemetry sampling** (`telemetry.sampler.type`, `telemetry.sampler.ratio`)
|
|
* **JWT TTL** (`auth.jwt.ttl`)
|
|
|
|
### Non-Hot-Reloadable Configuration
|
|
|
|
These parameters will require application restart:
|
|
|
|
* **Server settings** (`server.host`, `server.port`)
|
|
* **Database credentials** (`database.*`)
|
|
* **JWT secret** (`auth.jwt_secret`)
|
|
* **Admin credentials** (`auth.admin_master_password`)
|
|
|
|
### Implementation Plan
|
|
|
|
```go
|
|
// Add to config package
|
|
type ConfigManager struct {
|
|
config *Config
|
|
viper *viper.Viper
|
|
changeChan chan struct{}
|
|
stopChan chan struct{}
|
|
}
|
|
|
|
func NewConfigManager() (*ConfigManager, error) {
|
|
// Initialize Viper and load initial config
|
|
// Start file watcher if config file exists
|
|
}
|
|
|
|
func (cm *ConfigManager) StartWatching() {
|
|
if cm.viper != nil {
|
|
cm.viper.WatchConfig()
|
|
cm.viper.OnConfigChange(func(e fsnotify.Event) {
|
|
cm.handleConfigChange()
|
|
})
|
|
}
|
|
}
|
|
|
|
func (cm *ConfigManager) handleConfigChange() {
|
|
// Reload only safe configuration sections
|
|
// Update logging level if changed
|
|
// Update feature flags if changed
|
|
// Notify other components of changes
|
|
|
|
log.Info().Msg("Configuration reloaded (partial)")
|
|
}
|
|
|
|
// Safe getter methods that work with hot reloading
|
|
func (cm *ConfigManager) GetLogLevel() string {
|
|
// Return current value, potentially updated via hot reload
|
|
}
|
|
```
|
|
|
|
### Configuration File Monitoring
|
|
|
|
```go
|
|
// In main application setup
|
|
func main() {
|
|
configManager, err := config.NewConfigManager()
|
|
if err != nil {
|
|
log.Fatal().Err(err).Msg("Failed to initialize config")
|
|
}
|
|
|
|
// Start watching for config changes
|
|
configManager.StartWatching()
|
|
|
|
// Use configManager throughout application instead of direct config access
|
|
}
|
|
```
|
|
|
|
## Pros and Cons of the Options
|
|
|
|
### Option 1: Full Hot Reloading with Viper WatchConfig
|
|
|
|
* **Good**: Maximum flexibility for configuration changes
|
|
* **Good**: Leverages Viper's built-in capabilities
|
|
* **Good**: Good for development workflow
|
|
* **Bad**: High risk of runtime errors from unsafe changes
|
|
* **Bad**: Complex to implement safely
|
|
* **Bad**: Hard to debug configuration-related issues
|
|
|
|
### Option 2: Selective Hot Reloading (Chosen)
|
|
|
|
* **Good**: Safe approach with clear boundaries
|
|
* **Good**: Balances flexibility and stability
|
|
* **Good**: Easier to implement and maintain
|
|
* **Good**: Clear documentation of what can be changed
|
|
* **Bad**: More complex than no hot reloading
|
|
* **Bad**: Requires careful design of config access patterns
|
|
|
|
### Option 3: Manual Reload Endpoint
|
|
|
|
* **Good**: Explicit control over when reloading happens
|
|
* **Good**: Can be secured with authentication
|
|
* **Good**: Good for production environments
|
|
* **Bad**: Less convenient for development
|
|
* **Bad**: Requires additional API endpoint management
|
|
* **Bad**: Still needs same safety considerations as automatic reloading
|
|
|
|
### Option 4: No Hot Reloading
|
|
|
|
* **Good**: Simplest approach
|
|
* **Good**: No risk of runtime configuration errors
|
|
* **Good**: Easier to reason about application state
|
|
* **Bad**: Requires restart for any configuration change
|
|
* **Bad**: Less flexible for production adjustments
|
|
* **Bad**: Slower development iteration
|
|
|
|
## Configuration Change Handling
|
|
|
|
### Safe Change Pattern
|
|
|
|
```go
|
|
// Example: Logging level change
|
|
func (cm *ConfigManager) handleConfigChange() {
|
|
// Get new config values
|
|
newConfig := &Config{}
|
|
if err := cm.viper.Unmarshal(newConfig); err != nil {
|
|
log.Error().Err(err).Msg("Failed to unmarshal new config")
|
|
return
|
|
}
|
|
|
|
// Apply safe changes
|
|
if newConfig.Logging.Level != cm.config.Logging.Level {
|
|
if err := cm.applyLogLevelChange(newConfig.Logging.Level); err != nil {
|
|
log.Error().Err(err).Msg("Failed to apply log level change")
|
|
}
|
|
}
|
|
|
|
// Update other safe parameters...
|
|
}
|
|
|
|
func (cm *ConfigManager) applyLogLevelChange(newLevel string) error {
|
|
// Validate new level
|
|
level := parseLogLevel(newLevel)
|
|
|
|
// Apply change
|
|
zerolog.SetGlobalLevel(level)
|
|
cm.config.Logging.Level = newLevel
|
|
|
|
log.Info().Str("new_level", newLevel).Msg("Log level updated")
|
|
return nil
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
* Invalid configuration changes are logged but don't crash the application
|
|
* Failed changes revert to previous known-good values
|
|
* Critical errors during reload trigger application shutdown
|
|
* All changes are logged for audit purposes
|
|
|
|
## Links
|
|
|
|
* [Viper WatchConfig Documentation](https://github.com/spf13/viper#watching-and-re-reading-config-files)
|
|
* [Viper OnConfigChange](https://github.com/spf13/viper#example-of-watching-a-config-file)
|
|
* [ADR-0006: Configuration Management](0006-configuration-management.md)
|
|
|
|
## Configuration File Example with Hot-Reloadable Settings
|
|
|
|
```yaml
|
|
# config.yaml - These settings can be hot-reloaded
|
|
server:
|
|
host: "0.0.0.0"
|
|
port: 8080
|
|
|
|
logging:
|
|
level: "info" # Can be changed without restart
|
|
json: false
|
|
output: ""
|
|
|
|
api:
|
|
v2_enabled: false # Can be changed without restart
|
|
|
|
telemetry:
|
|
enabled: false
|
|
sampler:
|
|
type: "parentbased_always_on" # Can be changed without restart
|
|
ratio: 1.0
|
|
```
|
|
|
|
## Migration Plan
|
|
|
|
1. **Phase 1**: Implement ConfigManager wrapper around existing config
|
|
2. **Phase 2**: Add selective hot reloading for logging level
|
|
3. **Phase 3**: Extend to feature flags and telemetry settings
|
|
4. **Phase 4**: Add documentation and examples
|
|
5. **Phase 5**: Update all components to use ConfigManager instead of direct config access
|
|
|
|
## Monitoring and Observability
|
|
|
|
* Log all configuration changes with timestamps
|
|
* Include previous and new values in change logs
|
|
* Add metrics for configuration reload events
|
|
* Provide admin endpoint to view current configuration
|
|
|
|
## Security Considerations
|
|
|
|
* Config file permissions should be restrictive
|
|
* Hot reloading should be disabled in production by default
|
|
* Configuration changes should be audited
|
|
* Sensitive parameters should never be hot-reloadable
|
|
|
|
## Future Enhancements
|
|
|
|
* Configuration change webhooks
|
|
* Configuration versioning and rollback
|
|
* Configuration validation before applying changes
|
|
* Multi-file configuration support |