|
1 | | -# Config Reload |
| 1 | +# Configuration Hot Reload Design |
| 2 | + |
| 3 | +## Introduction |
| 4 | + |
| 5 | +In the light-4j framework, minimizing downtime is crucial for microservices. The Configuration Hot Reload feature allows services to update their configuration at runtime without restarting the server. This design document outlines the centralized caching architecture used to achieve consistent and efficient hot reloads. |
| 6 | + |
| 7 | +## Architecture Evolution |
| 8 | + |
| 9 | +### Previous Approach (Decentralized) |
| 10 | +Initially, configuration reload was handled in a decentralized manner: |
| 11 | +- Each handler maintained its own static configuration object. |
| 12 | +- A `reload()` method was required on every handler to manually refresh this object. |
| 13 | +- The `ConfigReloadHandler` used reflection to search for and invoke these `reload()` methods. |
| 14 | + |
| 15 | +**Drawbacks:** |
| 16 | +- **Inconsistency:** Different parts of the application could hold different versions of the configuration. |
| 17 | +- **Complexity:** Every handler needed boilerplate code for reloading. |
| 18 | +- **State Management:** Singleton classes (like `ClientConfig`) often held stale references that were difficult to update. |
| 19 | + |
| 20 | +### Current Approach (Centralized Cache) |
| 21 | +The new architecture centralizes the "source of truth" within the `Config` class itself. |
| 22 | + |
| 23 | +- **Centralized Cache**: The `Config` class maintains a `ConcurrentHashMap` of all loaded configurations. |
| 24 | +- **Cache Invalidation**: Instead of notifying components to reload, we simply invalidate the specific entry in the central cache. |
| 25 | +- **Lazy Loading**: Consumers (Handlers, Managers) fetch the configuration from the `Config` class at the moment of use. If the cache is empty (cleared), `Config` reloads it from the source files. |
| 26 | + |
| 27 | +## Detailed Design |
| 28 | + |
| 29 | +### 1. The Config Core (`Config.java`) |
| 30 | + |
| 31 | +The `Config` class is enhanced to support targeted cache invalidation. |
| 32 | + |
| 33 | +```java |
| 34 | +public abstract class Config { |
| 35 | + // ... existing methods |
| 36 | + |
| 37 | + // New method to remove a specific config from memory |
| 38 | + public abstract void clearConfigCache(String configName); |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +When `clearConfigCache("my-config")` is called: |
| 43 | +1. The entry for "my-config" is removed from the internal `configCache`. |
| 44 | +2. The next time `getJsonMapConfig("my-config")` or `getJsonObjectConfig(...)` is called, the `Config` class detects the miss and reloads the file from the filesystem or external config server. |
| 45 | + |
| 46 | +### 2. The Admin Endpoint (`ConfigReloadHandler`) |
| 47 | + |
| 48 | +The `ConfigReloadHandler` exposes the `/adm/config-reload` endpoint. Its responsibility has been simplified: |
| 49 | + |
| 50 | +1. **Receive Request**: Accepts a list of modules/plugins to reload. |
| 51 | +2. **Resolve Config Names**: Looks up the configuration file names associated with the requested classes using the `ModuleRegistry`. |
| 52 | +3. **Invalidate Cache**: Calls `Config.getInstance().clearConfigCache(configName)` for each identified module. |
| 53 | + |
| 54 | +It no longer relies on reflection to call methods on the handlers. |
| 55 | + |
| 56 | +### 3. Configuration Consumers |
| 57 | + |
| 58 | +Handlers and other components must follow a stateless pattern regarding configuration. |
| 59 | + |
| 60 | +**Anti-Pattern (Old Way):** |
| 61 | +```java |
| 62 | +public class MyHandler { |
| 63 | + static MyConfig config = MyConfig.load(); // Static load at startup |
| 64 | + |
| 65 | + public void handleRequest(...) { |
| 66 | + // Use static config - will remain stale even if file changes |
| 67 | + if (config.isEnabled()) ... |
| 68 | + } |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +**Recommended Pattern (New Way):** |
| 73 | +```java |
| 74 | +public class MyHandler { |
| 75 | + // No static config field |
| 76 | + |
| 77 | + public void handleRequest(...) { |
| 78 | + // Fetch fresh config from central cache every time |
| 79 | + // This is fast due to HashMap lookup |
| 80 | + MyConfig config = MyConfig.load(); |
| 81 | + |
| 82 | + if (config.isEnabled()) ... |
| 83 | + } |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +**Implementation in Config Classes:** |
| 88 | +The `load()` method in configuration classes (e.g., `CorrelationConfig`) simply delegates to the cached `Config` methods: |
| 89 | + |
| 90 | +```java |
| 91 | +private CorrelationConfig(String configName) { |
| 92 | + // Always ask Config class for the Map. |
| 93 | + // If cache was cleared, this call triggers a file reload. |
| 94 | + mappedConfig = Config.getInstance().getJsonMapConfig(configName); |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### 4. Handling Singletons (e.g., ClientConfig) |
| 99 | + |
| 100 | +For Singleton classes that parse configuration into complex objects, they must check if the underlying configuration has changed. |
| 101 | + |
| 102 | +```java |
| 103 | +public static ClientConfig get() { |
| 104 | + // Check if the Map instance in Config.java is different from what we typically hold |
| 105 | + Map<String, Object> currentMap = Config.getInstance().getJsonMapConfig(CONFIG_NAME); |
| 106 | + if (instance == null || instance.mappedConfig != currentMap) { |
| 107 | + synchronized (ClientConfig.class) { |
| 108 | + if (instance == null || instance.mappedConfig != currentMap) { |
| 109 | + instance = new ClientConfig(); // Re-parse and create new instance |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + return instance; |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | + } |
| 118 | + return instance; |
| 119 | +} |
| 120 | + |
| 121 | +### 5. Thread Safety |
| 122 | + |
| 123 | +The `Config` class handles concurrent access using the **Double-Checked Locking** pattern to ensure that the configuration file is loaded **exactly once**, even if multiple threads request it simultaneously immediately after the cache is cleared. |
| 124 | + |
| 125 | +**Scenario:** |
| 126 | +1. **Thread A** and **Thread B** both handle a request for `MyHandler`. |
| 127 | +2. Both call `MyConfig.load()`, which calls `Config.getJsonMapConfig("my-config")`. |
| 128 | +3. Both see that the cache is empty (returning `null`) because it was just cleared by the reload handler. |
| 129 | +4. **Thread A** acquires the lock (`synchronized`). **Thread B** waits. |
| 130 | +5. **Thread A** checks the cache again (still null), loads the file from disk, puts it in the `configCache`, and releases the lock. |
| 131 | +6. **Thread B** acquires the lock. |
| 132 | +7. **Thread B** checks the cache again. This time it finds the config loaded by **Thread A**. |
| 133 | +8. **Thread B** uses the existing config without loading from disk. |
| 134 | + |
| 135 | +This ensures no race conditions or redundant file I/O operations occur in high-concurrency environments. |
| 136 | + |
| 137 | +## Workflow Summary |
| 138 | + |
| 139 | +1. **Update**: User updates `values.yml` or a config file on the server/filesystem. |
| 140 | +2. **Trigger**: User calls `POST https://host:port/adm/config-reload` with the module name. |
| 141 | +3. **Clear**: `ConfigReloadHandler` tells `Config.java` to `clearConfigCache` for that module. |
| 142 | +4. **Processing**: |
| 143 | + - **Step 4a**: Request A arrives at `MyHandler`. |
| 144 | + - **Step 4b**: `MyHandler` calls `MyConfig.load()`. |
| 145 | + - **Step 4c**: `MyConfig` calls `Config.getJsonMapConfig()`. |
| 146 | + - **Step 4d**: `Config` sees cache miss, reads file from disk, parses it, puts it in cache, and returns it. |
| 147 | + - **Step 4e**: `MyHandler` processes request with NEW configuration. |
| 148 | +5. **Subsequent Requests**: Step 4d is skipped; data is served instantly from memory. |
| 149 | + |
| 150 | +## Benefits |
| 151 | + |
| 152 | +1. **Performance**: Only one disk read per reload cycle. Subsequent accesses are Hash Map lookups. |
| 153 | +2. **Reliability**: Config state is consistent. No chance of "half-reloaded" application state. |
| 154 | +3. **Simplicity**: drastic reduction in boilerplate code across the framework. |
0 commit comments