-
-
Notifications
You must be signed in to change notification settings - Fork 26
perf(health): stop rewriting a health record on every healthy cycle #466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,110 @@ | ||||||||||||||||||||||||||||||||||||||
| """A healthy plugin must not rewrite its health record every cycle. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Every successful plugin update called record_success(), which persisted the | ||||||||||||||||||||||||||||||||||||||
| record unconditionally. In steady state the only fields that had changed were | ||||||||||||||||||||||||||||||||||||||
| total_successes and last_success_time -- a counter and a timestamp that | ||||||||||||||||||||||||||||||||||||||
| health_monitor reads for display and that nothing reads back after a restart. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Measured on a rig running 24 plugins: about 17 health-file rewrites a minute, | ||||||||||||||||||||||||||||||||||||||
| roughly 25,000 a day. Each is ~400 bytes, but they land on an SD card where | ||||||||||||||||||||||||||||||||||||||
| the unit of cost is an erase-block cycle, not the byte count, and where wear is | ||||||||||||||||||||||||||||||||||||||
| what eventually kills the card. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| The circuit breaker still needs its own state to survive a restart, so the | ||||||||||||||||||||||||||||||||||||||
| write is kept for exactly the fields it is rebuilt from -- and a failure, a | ||||||||||||||||||||||||||||||||||||||
| circuit opening, or a recovery must still be written the moment it happens. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| from src.plugin_system.plugin_health import PluginHealthTracker, CircuitState | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class _Cache: | ||||||||||||||||||||||||||||||||||||||
| """Counts writes; serves back whatever was last written.""" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||
| self.store = {} | ||||||||||||||||||||||||||||||||||||||
| self.writes = 0 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def set(self, key, data, ttl=None, **kwargs): | ||||||||||||||||||||||||||||||||||||||
| self.writes += 1 | ||||||||||||||||||||||||||||||||||||||
| self.store[key] = data | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def get(self, key, max_age=None, memory_ttl=None, **kwargs): | ||||||||||||||||||||||||||||||||||||||
| return self.store.get(key) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @pytest.fixture | ||||||||||||||||||||||||||||||||||||||
| def tracker(): | ||||||||||||||||||||||||||||||||||||||
| cache = _Cache() | ||||||||||||||||||||||||||||||||||||||
| t = PluginHealthTracker(cache_manager=cache) | ||||||||||||||||||||||||||||||||||||||
| return t, cache | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_steady_state_success_stops_writing(tracker): | ||||||||||||||||||||||||||||||||||||||
| """The regression: 100 healthy cycles used to be 100 SD writes.""" | ||||||||||||||||||||||||||||||||||||||
| t, cache = tracker | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| first = cache.writes | ||||||||||||||||||||||||||||||||||||||
| for _ in range(100): | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| assert cache.writes == first, ( | ||||||||||||||||||||||||||||||||||||||
| f"{cache.writes - first} redundant writes across 100 healthy cycles" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+46
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert zero writes from the first healthy execution. Line 50 permits one write before the 100-cycle loop. The durable-state policy does not require an initial healthy success to persist, because the default state reconstructs the breaker state. Assert Proposed fix def test_steady_state_success_stops_writing(tracker):
"""The regression: 100 healthy cycles used to be 100 SD writes."""
t, cache = tracker
- t.record_success("weather")
- first = cache.writes
for _ in range(100):
t.record_success("weather")
- assert cache.writes == first, (
- f"{cache.writes - first} redundant writes across 100 healthy cycles"
- )
+ assert cache.writes == 0, (
+ f"{cache.writes} writes across 100 healthy cycles"
+ )📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_the_counters_are_still_accurate_in_memory(tracker): | ||||||||||||||||||||||||||||||||||||||
| """Skipping the write must not skip the bookkeeping.""" | ||||||||||||||||||||||||||||||||||||||
| t, _ = tracker | ||||||||||||||||||||||||||||||||||||||
| for _ in range(10): | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| state = t.get_health_state("weather") | ||||||||||||||||||||||||||||||||||||||
| assert state["total_successes"] == 10 | ||||||||||||||||||||||||||||||||||||||
| assert state["last_success_time"] is not None | ||||||||||||||||||||||||||||||||||||||
| assert state["last_success_time"] <= time.time() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_a_failure_is_written_immediately(tracker): | ||||||||||||||||||||||||||||||||||||||
| t, cache = tracker | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| before = cache.writes | ||||||||||||||||||||||||||||||||||||||
| t.record_failure("weather", RuntimeError("boom")) | ||||||||||||||||||||||||||||||||||||||
| assert cache.writes > before, "a failure must reach disk" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_recovery_after_failure_is_written(tracker): | ||||||||||||||||||||||||||||||||||||||
| """consecutive_failures returning to 0 is durable state changing.""" | ||||||||||||||||||||||||||||||||||||||
| t, cache = tracker | ||||||||||||||||||||||||||||||||||||||
| t.record_failure("weather", RuntimeError("boom")) | ||||||||||||||||||||||||||||||||||||||
| before = cache.writes | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| assert cache.writes > before, "recovery must reach disk" | ||||||||||||||||||||||||||||||||||||||
| assert t.get_health_state("weather")["consecutive_failures"] == 0 | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_a_closing_circuit_is_written(tracker): | ||||||||||||||||||||||||||||||||||||||
| """Success in half-open closes the circuit -- that must survive a restart.""" | ||||||||||||||||||||||||||||||||||||||
| t, cache = tracker | ||||||||||||||||||||||||||||||||||||||
| state = t.get_health_state("weather") | ||||||||||||||||||||||||||||||||||||||
| state["circuit_state"] = CircuitState.HALF_OPEN.value | ||||||||||||||||||||||||||||||||||||||
| state["half_open_start_time"] = time.time() | ||||||||||||||||||||||||||||||||||||||
| before = cache.writes | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
| assert cache.writes > before, "a circuit transition must reach disk" | ||||||||||||||||||||||||||||||||||||||
| assert t.get_health_state("weather")["circuit_state"] == CircuitState.CLOSED.value | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def test_durable_state_survives_a_restart(tracker): | ||||||||||||||||||||||||||||||||||||||
| """What is skipped must genuinely not matter to the breaker.""" | ||||||||||||||||||||||||||||||||||||||
| t, cache = tracker | ||||||||||||||||||||||||||||||||||||||
| for _ in range(3): | ||||||||||||||||||||||||||||||||||||||
| t.record_failure("weather", RuntimeError("boom")) | ||||||||||||||||||||||||||||||||||||||
| for _ in range(50): | ||||||||||||||||||||||||||||||||||||||
| t.record_success("weather") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| revived = PluginHealthTracker(cache_manager=cache) | ||||||||||||||||||||||||||||||||||||||
| state = revived.get_health_state("weather") | ||||||||||||||||||||||||||||||||||||||
| assert state["consecutive_failures"] == 0 | ||||||||||||||||||||||||||||||||||||||
| assert state["circuit_state"] == CircuitState.CLOSED.value | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Store snapshots in
_Cache._Cache.set()stores the mutablestatedictionary by reference. Laterrecord_success()calls mutate that same dictionary. A revived tracker can then observe an unpersisted circuit transition, sotest_durable_state_survives_a_restart()can pass even if the recovery write is removed.Copy data on
set()andget().Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents