Implement hot reload for mock file changes without server restart#31
Implement hot reload for mock file changes without server restart#31
Conversation
…ad task - Change MockStore type from Arc<HashMap> to Arc<RwLock<HashMap>> for mutable access - Add load_mocks_map() in loader.rs that returns raw HashMap for reloading - Update find_matching_mock to take &HashMap instead of &MockStore - Update handler.rs to acquire read locks before accessing mocks - Spawn background task in main.rs that reloads mocks from disk every 2 seconds - Add test_reload_mocks_reflects_file_changes test - Update all existing tests for new RwLock-based MockStore Co-authored-by: ragilhadi <24733985+ragilhadi@users.noreply.github.com>
Co-authored-by: ragilhadi <24733985+ragilhadi@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Implements hot reload of mock configurations by making the mock store mutable and periodically reloading mock files from disk, so mock changes are reflected without restarting the server.
Changes:
- Switch
MockStoretoArc<RwLock<HashMap<...>>>to allow live updates. - Add
load_mocks_map()to support reloading into a freshHashMapand swapping under a write lock. - Spawn a background Tokio task that reloads mocks every 2 seconds; update handlers/matcher to work with the lock-managed store.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/types.rs | Changes MockStore to a Tokio RwLock and updates unit test accordingly. |
| src/loader.rs | Extracts load_mocks_map() and wraps it in load_mocks() for the RwLock store; adds a reload-related test. |
| src/matcher.rs | Updates find_matching_mock to accept &HashMap so the caller owns lock management. |
| src/handler.rs | Acquires read locks for matching and /health, and releases the lock before async request logging. |
| src/main.rs | Adds a background polling task to reload mocks from disk and swap them into the shared store. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| if path_obj.is_file() { | ||
| // Load single file | ||
| if let Ok(mock) = load_single_mock(path_obj) { | ||
| let key = create_mock_key(&mock.method, &mock.path); | ||
| let entry = mocks.entry(key).or_default(); | ||
| if !entry.is_empty() { | ||
| warn!( | ||
| "Multiple mocks registered for {} {}: {} total", | ||
| mock.method, | ||
| mock.path, | ||
| entry.len() + 1 | ||
| ); | ||
| } | ||
| entry.push(mock); | ||
| } | ||
| } else if path_obj.is_dir() { | ||
| // Load all JSON files from directory tree (recursive) | ||
| collect_json_files(path_obj, &mut mocks); | ||
| } | ||
|
|
There was a problem hiding this comment.
load_mocks_map silently drops any file that fails load_single_mock (parse/read/validation errors) because the Err branch is ignored. With hot reload, this means a temporarily invalid/partially-written JSON file will cause its mocks to disappear (potentially leaving the map empty) with no error log. Consider logging the Err and/or changing load_mocks_map to return a Result/error count so callers can decide to keep the previous mock set on reload failures.
There was a problem hiding this comment.
Applied in 41bb2aa and 601f2c7. Changes:
load_mocks_mapnow returnsLoadResult { mocks, errors }— allload_single_mockerrors are logged viawarn!instead of silently dropped- The reload task uses
tokio::task::spawn_blockingfor file I/O to avoid blocking async worker threads - Reload skips the swap when
result.errors > 0, preserving the previous mock set - Each successful reload logs at
debuglevel; endpoint count changes log atinfolevel - Added
test_reload_skips_on_errorstest; non-existent paths count as errors
| tokio::spawn(async move { | ||
| let mut interval = | ||
| tokio::time::interval(std::time::Duration::from_secs(RELOAD_INTERVAL_SECS)); | ||
| // tokio::time::interval fires immediately on creation; skip the first | ||
| // tick to avoid redundantly reloading mocks that were just loaded at startup. | ||
| interval.tick().await; | ||
| loop { | ||
| interval.tick().await; | ||
| let new_mocks = load_mocks_map(MOCKS_DIR); | ||
| let mut store = reload_mocks.write().await; | ||
| let old_len = store.len(); | ||
| *store = new_mocks; | ||
| let new_len = store.len(); |
There was a problem hiding this comment.
The hot-reload loop calls load_mocks_map (which uses blocking std::fs I/O) directly inside a tokio::spawn task. This will run on the async runtime worker thread and can block request handling during directory walks/JSON parsing. Consider moving the reload work to spawn_blocking (or switching to tokio::fs) and only taking the write lock to swap in the already-loaded map.
There was a problem hiding this comment.
Already applied in 41bb2aa. The reload loop uses tokio::task::spawn_blocking (line 70 in main.rs) to run load_mocks_map off the async runtime, and only takes the write lock afterward to swap in the result.
| let new_mocks = load_mocks_map(MOCKS_DIR); | ||
| let mut store = reload_mocks.write().await; | ||
| let old_len = store.len(); | ||
| *store = new_mocks; | ||
| let new_len = store.len(); |
There was a problem hiding this comment.
The reload loop always swaps *store = new_mocks even when the reload produced an empty map (e.g., mocks dir temporarily unreadable, or all files failing to parse). This can clear all mocks and create a burst of 404s. Consider only swapping on successful reload (e.g., when no load errors occurred), or retaining the previous store contents when reload fails.
There was a problem hiding this comment.
Already applied in 41bb2aa. The reload loop checks result.errors > 0 (line 79 in main.rs) and continues without swapping, preserving the previous mock set.
| let old_len = store.len(); | ||
| *store = new_mocks; | ||
| let new_len = store.len(); | ||
| if old_len != new_len { | ||
| info!( | ||
| "🔄 Hot reload: mocks updated ({} -> {} endpoint(s))", | ||
| old_len, new_len | ||
| ); | ||
| } |
There was a problem hiding this comment.
Reload logging is gated on old_len != new_len, so edits that keep the same number of endpoints (or changes within an existing endpoint’s mock list) won’t produce any reload log line even though behavior changed. Consider logging each successful reload (possibly at debug) or tracking a reload counter/timestamp instead of relying on endpoint count changes.
There was a problem hiding this comment.
Already applied in 41bb2aa. Every successful reload now logs at debug level (line 96 in main.rs), and endpoint count changes log at info level (line 91).
…pawn_blocking, skip bad reloads
- load_mocks_map now returns LoadResult { mocks, errors } with error count
- Errors from load_single_mock are logged with warn! instead of silently dropped
- Reload task uses spawn_blocking to avoid blocking the async runtime
- Reload task skips swapping mocks when errors are detected, keeping previous set
- Each successful reload is logged at debug level; endpoint count changes at info
Co-authored-by: ragilhadi <24733985+ragilhadi@users.noreply.github.com>
…t assertions to tests Co-authored-by: ragilhadi <24733985+ragilhadi@users.noreply.github.com>
MockStorewasArc<HashMap>— immutable after startup. Any mock file change required a full server restart. The README already advertised "Hot Reload" as a feature.Changes
types.rs:MockStore→Arc<RwLock<HashMap<String, Vec<MockConfig>>>>loader.rs: Extractload_mocks_map()returningLoadResult { mocks, errors }for reload use;load_mocks()wraps it. Allload_single_mockfailures are logged viawarn!with file path and error message.matcher.rs:find_matching_mocktakes&HashMapdirectly — stays synchronous, caller manages the lockhandler.rs: Acquire read lock for mock access, release beforerecord_requestto minimize hold timemain.rs: Spawn background task that reloads mocks from disk every 2s usingspawn_blockingto avoid blocking the async runtime. Reload is skipped when errors are detected, preserving the previous mock set. Each successful reload logs atdebuglevel; endpoint count changes log atinfolevel.Adds
test_reload_mocks_reflects_file_changesandtest_reload_skips_on_errorscovering add/remove of mock files and error-resilient reload through theRwLockstore. All 88 tests pass, zero clippy warnings.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.