Skip to content

fix(monitor): refresh cluster status on shutdown, run housekeeping off the main loop - #1504

Open
wmousa wants to merge 2 commits into
mainfrom
fix/monitor-status-latency
Open

wmousa wants to merge 2 commits into
mainfrom
fix/monitor-status-latency

Conversation

@wmousa

@wmousa wmousa commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Main port of #1503 (R26.3). See that PR for the full write-up.

Problem

On 2026-10-19 a customer cluster, freshly upgraded from R26.2-PRE, had two nodes shut down at 15:42:46. It stayed ACTIVE until 15:49:47: seven minutes with reduced redundancy and no status change.

  1. The shutdown never refreshes the cluster status. shutdown_storage_node sets the node to OFFLINE itself, so the monitor's set_node_offline never runs for it. That is the only per-node path that calls update_cluster_status, so the status waits for the monitor's main-loop tick.
  2. The main-loop tick was blocked by housekeeping. Retention housekeeping ran in that loop. Each sweep read the whole events table and cleared one key per expired event. R26.2-PRE never pruned events, so the first sweeps after the upgrade deleted months of them while the cluster status went unrefreshed.

Change

  • shutdown_storage_node: once the node is OFFLINE, calls the new apply_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 same set_cluster_status the monitor makes for those transitions. It deliberately does not run update_cluster_status, the monitor's full pass, because on a SUSPENDED cluster that pass calls cluster_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 thread: housekeeping runs on its own daemon thread, started by _start_housekeeping() in main(). The main loop only refreshes the status.
  • _delete_old_logs(cluster_id): removes one cluster's expired events with a single clear_range(<cluster>/, <cluster>/<cutoff ms>). Event keys are <cluster_uuid>/<date ms>/<uuid>, so those events form one contiguous key range.

Differences from #1503

  • Main-loop error log: main already logs this error with logger.exception, so that hunk keeps main's version.
  • test_monitor_thread_supervision.py: those tests count every thread main() creates. They now patch _start_housekeeping instead of _run_periodic_housekeeping, so their per-node thread counts are unchanged.
  • New test: the housekeeping thread is a daemon targeting _housekeeping_loop.

Integration CI failure (fixed)

test_cluster_status_is_refreshed_once_offline failed on R26.3 (py3.9) with 0 refreshes recorded.

  • Cause: test_secondary_promotion.py::test_escalates_unreachable_primary assigned a MagicMock to sys.modules["simplyblock_core.services.storage_node_monitor"] and never restored it.
  • Why 3.9 only: on Python 3.9, mock.patch resolves its target through package attributes, so the test patched the real module while the lazy import in shutdown_storage_node got the leaked mock. On 3.11+, patch resolves through sys.modules, which is why main passed.
  • Fix: the leaking test now uses patch.dict(sys.modules, ...), which restores the real module.
  • Reproduced locally: with a 3.9-style patch resolver in the same test order, the test fails without the fix and passes with it.

Tests

  • ruff: clean.
  • mypy: 2 errors, both in spdk_http_proxy_server.py (asyncio.open_unix_connection on Windows). This PR doesn't touch that file.
  • Unit tier (Windows): 4062 passed against 4057 on the baseline. The same 35 failures appear on the untouched baseline (spdk_proxy_unit, lblk_persistent_names, …).
  • Integration tier: not run locally, because it needs Docker + FDB. I ran test_shutdown_no_suspension.py locally with three extra FDB reads stubbed (get_active_lvol_migration, cancel_pending_node_restart_tasks, cancel_node_tasks): 17/17 pass. CI covers this tier.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the target/main Targets the development branch label Oct 19, 2026
wmousa and others added 2 commits October 19, 2026 18:09
…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
wmousa force-pushed the fix/monitor-status-latency branch from 3f51afb to 8b7cab4 Compare October 19, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

target/main Targets the development branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant