Skip to content

Implement hot reload for mock file changes without server restart#31

Open
Copilot wants to merge 5 commits intomainfrom
copilot/feature-hot-reload-mock-files
Open

Implement hot reload for mock file changes without server restart#31
Copilot wants to merge 5 commits intomainfrom
copilot/feature-hot-reload-mock-files

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

MockStore was Arc<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: MockStoreArc<RwLock<HashMap<String, Vec<MockConfig>>>>
  • loader.rs: Extract load_mocks_map() returning LoadResult { mocks, errors } for reload use; load_mocks() wraps it. All load_single_mock failures are logged via warn! with file path and error message.
  • matcher.rs: find_matching_mock takes &HashMap directly — stays synchronous, caller manages the lock
  • handler.rs: Acquire read lock for mock access, release before record_request to minimize hold time
  • main.rs: Spawn background task that reloads mocks from disk every 2s using spawn_blocking to avoid blocking the async runtime. Reload is skipped when errors are detected, preserving the previous mock set. Each successful reload logs at debug level; endpoint count changes log at info level.
// Background reload task
tokio::spawn(async move {
    let mut interval = tokio::time::interval(Duration::from_secs(RELOAD_INTERVAL_SECS));
    interval.tick().await; // skip immediate first tick
    loop {
        interval.tick().await;
        let result = tokio::task::spawn_blocking(move || load_mocks_map(mocks_dir)).await...;
        if result.errors > 0 {
            warn!("Hot reload: errors loading mocks, keeping previous mock set");
            continue;
        }
        let mut store = reload_mocks.write().await;
        *store = result.mocks;
    }
});

Adds test_reload_mocks_reflects_file_changes and test_reload_skips_on_errors covering add/remove of mock files and error-resilient reload through the RwLock store. All 88 tests pass, zero clippy warnings.

Original prompt

This section details on the original issue you should resolve

<issue_title>[Feature] Hot reload — reflect mock file changes without server restart</issue_title>
<issue_description>## Summary

Mocks are loaded once at startup into an immutable Arc<HashMap>. Any change to a mock file (create, edit, delete) requires a full server restart (docker compose restart mimic). This breaks the development feedback loop.

The README currently lists "Hot Reload" as a shipped feature in the Features section, but it is not implemented — this must be fixed.

Motivation

The primary use case for Mimic is local frontend/backend development. Developers expect to:

  1. Edit or create a mock JSON file
  2. See the change reflected immediately on the next request — no restart required

The current restart requirement adds 1–3 seconds of friction per change and interrupts long-running test suites.

Implementation Plan

Change MockStore to use RwLock

// types.rs
pub type MockStore = Arc<RwLock<HashMap<String, Vec<MockConfig>>>>;

All read paths in handler.rs acquire a read lock:

let mocks = state.mocks.read().await;

File Watcher

Add notify = "6" to Cargo.toml (cross-platform file system events, wraps inotify/kqueue/FSEvents).

In main.rs, spawn a background task:

tokio::spawn(async move {
    watch_mocks_directory(mocks_dir, mocks_store).await;
});

The watcher debounces events (e.g., 200ms) to avoid partial-file reads during saves, then calls load_mocks() and swaps the inner map under a write lock.

Reload Logging

INFO  Mock files changed, reloading...
INFO  Reloaded 12 mock(s) successfully

/health Enhancement

{
  "status": "healthy",
  "mocks_loaded": 12,
  "service": "mimic",
  "last_reload": "2026-02-23T10:05:00Z"
}

Manual Reload Endpoint (optional)

POST /admin/reload

Useful for environments where file-watching is unreliable (some Docker volume mounts).

Considerations

  • Docker volume mounts: inotify events may not fire reliably for bind mounts on some platforms. Fall back to polling (notify's PollWatcher) configurable via MIMIC_RELOAD_POLL_INTERVAL_MS env var.
  • Atomic swap: The write lock must be held only for the duration of the HashMap swap, not during the file I/O, to minimize lock contention.
  • Read-only volumes: If the volume is mounted :ro, the watcher still works (watching for events on a read-only mount is valid).

Acceptance Criteria

  • Mock changes (create, edit, delete) are reflected without server restart
  • Reload is logged with count of loaded mocks
  • Invalid JSON in edited file logs an error but does not crash the server or clear existing mocks
  • last_reload timestamp added to /health response
  • README Features section updated to accurately reflect hot reload capability
  • Roadmap item [ ] Hot reload for mock files marked as [x]
    </issue_description>

Comments on the Issue (you are @copilot in this section)


💡 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.

Copilot AI and others added 2 commits March 9, 2026 12:49
…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>
Copilot AI changed the title [WIP] Implement hot reload for mock file changes Implement hot reload for mock file changes without server restart Mar 9, 2026
@ragilhadi ragilhadi requested a review from Copilot March 9, 2026 13:06
@ragilhadi ragilhadi marked this pull request as ready for review March 9, 2026 13:07
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MockStore to Arc<RwLock<HashMap<...>>> to allow live updates.
  • Add load_mocks_map() to support reloading into a fresh HashMap and 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.

Comment thread src/loader.rs
Comment on lines 26 to 45
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);
}

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 41bb2aa and 601f2c7. Changes:

  • load_mocks_map now returns LoadResult { mocks, errors } — all load_single_mock errors are logged via warn! instead of silently dropped
  • The reload task uses tokio::task::spawn_blocking for 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 debug level; endpoint count changes log at info level
  • Added test_reload_skips_on_errors test; non-existent paths count as errors

Comment thread src/main.rs
Comment on lines +60 to +72
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();
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/main.rs Outdated
Comment on lines +68 to +72
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();
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/main.rs
Comment on lines +70 to +78
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
);
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot AI and others added 2 commits March 9, 2026 13:55
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Hot reload — reflect mock file changes without server restart

3 participants