Repository navigation
fix(monitor): refresh cluster status on shutdown, run housekeeping off the main loop - #1503
Merged
Merged
Conversation
…f the main loop On 2026-10-19 two nodes were shut down at 15:42:46 and the cluster read ACTIVE until 15:49:47, seven minutes with reduced redundancy. shutdown_storage_node flips the node to OFFLINE itself, so the monitor's set_node_offline (the path that refreshes the cluster status) never runs for it. The only remaining refresh is the monitor main loop, and that loop also ran the retention housekeeping, which read the whole events table and cleared one key per expired event. The cluster had just been upgraded from R26.2-PRE, which never pruned events, so the first sweeps deleted months of them while the status went unrefreshed. - shutdown_storage_node refreshes the cluster status once the node is OFFLINE (best effort; a failure there does not fail the shutdown). - Housekeeping runs on its own thread; the main loop only refreshes the status. - _delete_old_logs clears the cluster's expired events with one range clear over <cluster_uuid>/ .. <cluster_uuid>/<cutoff ms>, instead of a full-table read and per-event clears. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… leaking a mock monitor Review of the shutdown refresh. update_cluster_status is the monitor's full pass: requeue and re-admit sweeps, rebalancing flags, shared-placement and write-protection switches, and on a SUSPENDED cluster cluster_activate. A CLI `sn shutdown` must not start an activation or wait on one. The shutdown now calls apply_node_loss_status, which applies only the downgrades a node loss can cause (ACTIVE -> DEGRADED/SUSPENDED, DEGRADED -> SUSPENDED) with the same set_cluster_status the monitor makes for them. CI failure: test_secondary_promotion assigned a MagicMock to sys.modules["simplyblock_core.services.storage_node_monitor"] and never restored it. On Python 3.9 mock.patch resolves its target through package attributes, so the shutdown test patched the real module while the lazy import in shutdown_storage_node got the leaked mock, and the call went nowhere. 3.11+ resolves through sys.modules, which is why main passed. The test now uses patch.dict(sys.modules, ...), which restores the module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wmousa
force-pushed
the
fix/r263-monitor-status-latency
branch
from
October 19, 2026 16:08
e877e71 to
4df7674
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On a customer cluster (2026-10-19, mgmt just upgraded R26.2-PRE → 26.3.2), two storage nodes on one host were shut down with
sbctl sn shutdown. The node went OFFLINE at 15:42:46, but the cluster only changed from ACTIVE to DEGRADED at 15:49:47. For seven minutes it reported full redundancy while one host was down.Two causes:
The shutdown never refreshes the cluster status.
shutdown_storage_nodesets the node to OFFLINE itself. The monitor then finds the node already OFFLINE, soset_node_offlinenever runs, and that is the only per-node path that callsupdate_cluster_status. The status only changes on the monitor's next main-loop tick.The main-loop tick was blocked by retention housekeeping.
_run_periodic_housekeepingran in the main loop betweenupdate_cluster_statuscalls. Each sweep:db.get_events()(every cluster, unfiltered);R26.2-PRE never pruned events, so the first sweeps after the upgrade deleted months of them, and the cluster status went unrefreshed for that whole time.
Change
shutdown_storage_node: once the node is OFFLINE, calls the newapply_node_loss_status(cluster_id)(monitor module). It applies only the downgrades a node loss can cause: ACTIVE → DEGRADED/SUSPENDED and DEGRADED → SUSPENDED. It uses the sameset_cluster_statusthe monitor makes for those transitions. It deliberately does not runupdate_cluster_status, the monitor's full pass, because on a SUSPENDED cluster that pass callscluster_activate, and a CLI shutdown must not start an activation or wait on one. Recovery transitions stay with the monitor. This is best effort: a failure is logged with a traceback and the shutdown still succeeds._housekeeping_loop, everyHOUSEKEEPING_INTERVAL_SEC, all clusters). The main loop only refreshes the status._delete_old_logs(cluster_id): event keys are<cluster_uuid>/<date ms>/<uuid>, so one cluster's expired events form a single contiguous key range. They are now removed with oneclear_range(<cluster>/, <cluster>/<cutoff ms>), with no table read and a single mutation however large the backlog is.Integration CI failure (fixed)
test_cluster_status_is_refreshed_once_offlinefailed on R26.3 (py3.9) with 0 refreshes recorded.test_secondary_promotion.py::test_escalates_unreachable_primaryassigned aMagicMocktosys.modules["simplyblock_core.services.storage_node_monitor"]and never restored it.mock.patchresolves its target through package attributes, so the test patched the real module while the lazy import inshutdown_storage_nodegot the leaked mock. On 3.11+,patchresolves throughsys.modules, which is why main passed.patch.dict(sys.modules, ...), which restores the real module.Tests
tests/unit/test_ucs_debounce_bound.py:apply_node_loss_statusapplies node-loss downgrades only: never a recovery step, and no verdict at all for SUSPENDED / IN_ACTIVATION / READONLY / IN_EXPANSION / UNREADY;_delete_old_logsclears only this cluster's expired events, keeps recent events and other clusters' events, does not read the events table, andcl-1does not reach intocl-10.tests/integration/test_shutdown_no_suspension.py:ruff checkandmypyare clean.test_shutdown_no_suspension.pylocally with two extra FDB reads stubbed (get_active_lvol_migration,cancel_pending_node_restart_tasks): 17/17 pass. Without those stubs, 6 tests fail on the unchanged baseline too, for lack of FDB. CI covers this tier.Notes
round(time.time()*1000)). A legacy event dated in seconds would sort by its digits and may now be kept where the old code deleted it.except Exception: logger.error("Error while updating cluster status")still drops the traceback;🤖 Generated with Claude Code