fix(state-watch): watch canonical ini files atomically#1953
Conversation
- Purpose: switch the emhttp state watcher back to canonical per-file watches while explicitly supporting atomic replacement writes. - Before: the watcher used a directory-level path filter and treated .ini.new files as meaningful state updates. - Problem: that added extra normalization logic around temporary files and made the watcher semantics harder to reason about for atomic replacements. - Now: each state file is watched directly on its canonical .ini path with an explicit atomic window, while disks and shares keep their existing polling behavior. - How: restore the per-file watcher shape, remove .ini.new path normalization, and update tests to cover canonical-file atomic replacement behavior.
WalkthroughThe state file watcher has been refactored to monitor individual canonical state files (e.g., Changes
Sequence Diagram(s)sequenceDiagram
participant FileSystem as File System
participant Chokidar as Chokidar (per-file)
participant StateManager as StateManager
participant Dispatcher as Dispatcher
Note over FileSystem,Dispatcher: Old Approach: Directory Watch
FileSystem->>Chokidar: emit 'add' or 'change' for var.ini.new
Chokidar->>StateManager: event detected
StateManager->>StateManager: normalize filename, check if relevant
alt relevant file detected
StateManager->>Dispatcher: dispatch loadSingleStateFile()
end
Note over FileSystem,Dispatcher: New Approach: Per-File Atomic Watch
FileSystem->>FileSystem: write var.ini.new
FileSystem->>FileSystem: atomic rename var.ini.new → var.ini
Chokidar->>StateManager: 'change' event on var.ini (within 200ms window)
StateManager->>Dispatcher: dispatch loadSingleStateFile(StateFileKey.var)
Dispatcher->>Dispatcher: process state update
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
- Purpose: make the state watcher integration test match the production atomic-replacement path. - Before: the integration test renamed var.ini.new into place when the canonical var.ini file did not already exist. - Problem: CI exercised a file-creation edge case instead of the real replacement flow, which made the test flaky on Ubuntu. - Now: the test setup creates canonical state files before watcher startup so the rename path is a true atomic replacement. - How: seed var.ini and disks.ini in the temp state directory before initializing StateManager.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1953 +/- ##
==========================================
- Coverage 51.75% 51.73% -0.02%
==========================================
Files 1026 1026
Lines 70750 70718 -32
Branches 7897 7881 -16
==========================================
- Hits 36615 36586 -29
+ Misses 34012 34009 -3
Partials 123 123 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
api/src/__test__/store/watch/state-watch.integration.test.ts (1)
103-110: Consider increasing the timing margin for CI stability.The 250ms wait before asserting that no dispatch occurred is shorter than the
ATOMIC_REPLACEMENT_WINDOW_MS(200ms). While this should be sufficient, CI environments with I/O contention could introduce timing variability.🔧 Consider a small timing buffer
await writeFile(join(statesDirectory, 'disks.ini.new'), '[disk1]\nname=disk1\n'); - await new Promise((resolve) => setTimeout(resolve, 250)); + await new Promise((resolve) => setTimeout(resolve, 300)); expect(store.dispatch).not.toHaveBeenCalled();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/src/__test__/store/watch/state-watch.integration.test.ts` around lines 103 - 110, The 250ms sleep in the test "does not react to disks.ini.new before the canonical file is replaced" is too close to ATOMIC_REPLACEMENT_WINDOW_MS and can flake on CI; update the test to wait using the actual ATOMIC_REPLACEMENT_WINDOW_MS plus a small buffer (e.g., ATOMIC_REPLACEMENT_WINDOW_MS + 100) instead of a hardcoded 250, importing ATOMIC_REPLACEMENT_WINDOW_MS from the module that defines it (the watcher code) and replace the new Promise setTimeout call in that test with a wait based on that constant so CI timing variability is tolerated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@api/src/__test__/store/watch/state-watch.integration.test.ts`:
- Around line 103-110: The 250ms sleep in the test "does not react to
disks.ini.new before the canonical file is replaced" is too close to
ATOMIC_REPLACEMENT_WINDOW_MS and can flake on CI; update the test to wait using
the actual ATOMIC_REPLACEMENT_WINDOW_MS plus a small buffer (e.g.,
ATOMIC_REPLACEMENT_WINDOW_MS + 100) instead of a hardcoded 250, importing
ATOMIC_REPLACEMENT_WINDOW_MS from the module that defines it (the watcher code)
and replace the new Promise setTimeout call in that test with a wait based on
that constant so CI timing variability is tolerated.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6c4bdfd5-9022-4ca5-9bb1-fa0407ff87f0
📒 Files selected for processing (3)
api/src/__test__/store/watch/state-watch.integration.test.tsapi/src/__test__/store/watch/state-watch.test.tsapi/src/store/watch/state-watch.ts
🤖 I have created a release *beep* *boop* --- ## [4.31.1](v4.31.0...v4.31.1) (2026-03-23) ### Bug Fixes * **onboarding:** separate apply and completion flows ([#1948](#1948)) ([5be53a4](5be53a4)) * reload var state when emhttp writes temp files ([#1950](#1950)) ([7265105](7265105)) * **state-watch:** watch canonical ini files atomically ([#1953](#1953)) ([6471b3f](6471b3f)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
*.inifiles directly instead of using directory-level.ini.newnormalization.ini.new -> .inireplacements collapse into canonical file change handlingdisks.iniandshares.ini, and update the state watcher tests to cover the canonical-file watcher shapeTesting
pnpm --filter ./api test src/__test__/store/watch/state-watch.test.tspnpm --filter ./api test src/__test__/store/watch/state-watch.integration.test.tsapitodevgenviapnpm --filter ./api unraid:deploy devgendevgenthat an atomicvar.ini.new -> var.inireplacement emittedLoading state file for var after changein/var/log/graphql-api.logSummary by CodeRabbit
Tests
Refactor