Commit 1dd0a09
authored
* fix(engine): publish the rule cache via atomic rebind to close the unpinned torn-read
The unpinned path returned _RULE_CACHE["exact"], ["regex"], ["memo"] as three
separate dict reads while a reload mutated those keys in place, so a concurrent
reload on another worker thread could pair one rule-set version's rules with a
different version's memo and then memoize a stale-version result into the live
memo (persisting until the next rule edit).
_get_enabled_rules() now reads the module global once into a local, and a reload
publishes the new set by rebinding _RULE_CACHE to a fresh dict in a single atomic
assignment. A reader sees the whole old set or the whole new one — never exact
from V1 paired with memo from V2. The pin-prime path captures from the same
consistent local, so it can no longer snapshot a torn pair either.
* fix(config): raise min_version to 4.3.0 to match the documented floor
PR #49 raised the stated compatibility floor to NetBox >= 4.3.0 in README.md and
docs/installation.md but left the enforced PluginConfig.min_version at 4.2.0, so a
4.2.x install would still load despite the docs declaring it unsupported. Align
the gate with the docs and pin them together with a test.
* test(engine): harden rule-cache tests and guard _VERSION_COLUMNS exhaustiveness
- Add test_reload_publishes_a_fresh_cache_dict_atomically: asserts a reload swaps
in a new cache dict instead of mutating the old one in place — the invariant
behind the unpinned torn-read fix.
- Rewrite test_pinned_block_holds_snapshot_across_concurrent_cache_reload to use a
real second thread, proving _pin is thread-local (a plain global now fails it),
with try/finally cache restoration so the simulated reload can't leak.
- test_memo_is_bounded now checks the cap after every insert, catching a mid-loop
leak toward 2*cap that the old end-of-loop snapshot missed.
- Add EnabledRuleFingerprintColumnsTest: fails if any concrete model column is
neither fingerprinted nor explicitly excluded, so a future match-affecting field
can't silently break fingerprint-based cache invalidation.
* fix(engine): make the rule-cache memo read atomic and isolate the pinned memo
CodeRabbit (Major, #50): find_matching_rule's `if sig in memo: return memo[sig]` is a
non-atomic compound read — another thread sharing the per-version memo can `memo.clear()`
at the cap between the membership test and the subscript, raising a sporadic KeyError under
threaded workers (the switch can land between the two bytecodes even under the GIL). The
pinned path made it worse by aliasing the shared memo into thread-local state.
- Read via a single atomic `memo.get(sig, _MEMO_MISS)` (sentinel distinct from a memoized
None), so the separate membership test the race needs no longer exists — fixes both the
unpinned and pinned paths.
- Copy the memo into the pin (`_pin.memo = dict(cache["memo"])`) and return it, so a pinned
batch neither shares nor races the global memo and a concurrent clear can't evict its
warmed entries mid-loop.
Both tests red-checked: test_find_matching_rule_survives_concurrent_memo_clear reproduces the
KeyError deterministically with a dict that clears on its membership test;
test_pinned_block_uses_a_private_memo_copy asserts the pin holds a private copy.
1 parent 9e3aecb commit 1dd0a09
4 files changed
Lines changed: 237 additions & 29 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
30 | 33 | | |
31 | 34 | | |
32 | 35 | | |
| |||
35 | 38 | | |
36 | 39 | | |
37 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
38 | 48 | | |
39 | 49 | | |
40 | 50 | | |
| |||
198 | 208 | | |
199 | 209 | | |
200 | 210 | | |
| 211 | + | |
| 212 | + | |
201 | 213 | | |
202 | 214 | | |
203 | 215 | | |
| |||
207 | 219 | | |
208 | 220 | | |
209 | 221 | | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
210 | 227 | | |
211 | | - | |
| 228 | + | |
212 | 229 | | |
213 | | - | |
| 230 | + | |
214 | 231 | | |
215 | 232 | | |
216 | 233 | | |
217 | 234 | | |
218 | | - | |
219 | | - | |
220 | | - | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
221 | 241 | | |
222 | 242 | | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
228 | 249 | | |
229 | 250 | | |
230 | | - | |
| 251 | + | |
231 | 252 | | |
232 | 253 | | |
233 | 254 | | |
| |||
675 | 696 | | |
676 | 697 | | |
677 | 698 | | |
678 | | - | |
679 | | - | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
680 | 704 | | |
681 | 705 | | |
682 | 706 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
801 | 801 | | |
802 | 802 | | |
803 | 803 | | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
0 commit comments