Skip to content

fix(lvol): never drop the FDB record before the teardown is confirmed… - #1383

Merged
Hamdy-khader merged 2 commits into
R26.3from
R26.3-fix-lvol-record-teardown-gate
Sep 23, 2026
Merged

Hamdy-khader merged 2 commits into
R26.3from
R26.3-fix-lvol-record-teardown-gate

Conversation

@Hamdy-khader

Copy link
Copy Markdown
Collaborator

fix(lvol): never drop the FDB record before the teardown is confirmed (#1380)

  • fix(lvol): never drop the FDB record before the teardown is confirmed

R26.3 field report: four lvols existed as bdevs in SPDK with no record in
FDB. One had been created and deleted per the cluster log but never passed
through in_deletion; two showed a create and no delete at all; the fourth
left no cluster-log trace whatsoever.

Every path behind that shares one shape -- the record removal did not
depend on the data-plane teardown having happened:

  • _remove_bdev_stack returned a constant True. A failed removal was logged
      and the entry stamped status='deleted' anyway, so delete_lvol_from_node
      reported success and lvol_monitor erased the record over a live bdev.
      It now returns False and leaves the entry unmarked so a retry re-attempts
      it.

  • The "already gone?" probe was if not rpc_client.get_bdevs(name).
      get_bdevs returns None both for -ENODEV and for a non-200 from the SPDK
      proxy, so one transient hiccup during a mass delete skipped the delete
      entirely and recorded it as done, leaving nothing but an INFO line.
      _bdev_present() is tri-state on get_bdevs_2 (which existed for exactly
      this and was never wired in); an unknown answer now falls through to the
      delete rather than skipping it, since the probe was only an optimisation.
      -19 from the delete itself counts as confirmation.

  • delete_lvol_from_node answered True for nodes it had decided not to touch
      (check_non_leader_for_operation -> "skip"/"queue"). It now returns
      NodeTeardown DONE/DEFERRED/FAILED, of which only DONE is truthy, so the
      legacy if not ret: call sites keep working with the stricter meaning.

  • process_lvol_delete_finish skipped the leader's sync delete outright when
      the leader was merely unreachable or restarting, logged a failure and
      stepped over it, and removed the record regardless -- although that sync
      delete is the ONLY operation that removes the blob metadata and
      unregisters the bdev. It is now gated on DONE plus a fresh probe
      confirming the bdev is absent, and on no live peer still owing its leg
      (sync_delete_on_peer's return value was discarded). Anything less keeps
      the record in_deletion for the next pass. Record-only retirement of
      landing volumes (empty bdev_stack) short-circuits before the gate: their
      blob is meant to survive as the converted snapshot.

  • The in_deletion guard before release_lvol_ns_slot existed on exactly one
      of nine create/clone rollback sites. rollback_create_record() is now the
      single owner, and _fail_after_bdev persists the deletion intent BEFORE
      any teardown RPC, so a rollback whose RPCs raise can no longer leave a
      record the caller then erases.

  • delete_lvol's missing-node force branch erased the record without a
      single RPC and without an event -- for an HA volume the blob lives on
      every LVS member, whose node records usually still exist. It now tears
      down on the surviving peers and logs loudly when it cannot confirm.

  • lvol_events._lvol_event dropped the event when the node record was
      unreadable, so a volume whose node was gone produced no create, delete or
      status entry at all. That is why three of the four had no trace. It now
      falls back to the pool's cluster.

Reconciliation: storage_node_ops.auto_repair already compared SPDK's
inventory against the database, but only as an operator-invoked CLI command
that prints to stdout, so nothing ever ran it and every leak above was
permanent and invisible (the bdev is re-registered from lvstore metadata on
the next node restart). The blob-level comparison moves to
find_orphan_lvstore_blobs(), and lvol_monitor runs a detect-only sweep
(LVOL_MONITOR_ORPHAN_CHECK, every 30 min) that logs and raises a cluster
event. It never deletes: a blob it cannot correlate is the one whose
ownership it understands least.

Tests: tests/unit/test_lvol_teardown_confirmation.py covers the pure logic
(NodeTeardown truthiness, the tri-state probe, _remove_bdev_stack's honest
result). tests/integration/test_lvol_delete_record_gate.py covers the
record-lifetime gate, the rollback guard and the orphan sweep against real
FDB.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  • fix(lvol): drop the NodeTeardown enum, revert delete_lvol_from_node to bool

delete_lvol_from_node now returns a plain bool again: True only for a
confirmed teardown, False for a deferred (skip/queue) or a failed one alike
-- callers can no longer tell those two apart from the return value. Two call
sites that used the DEFERRED/FAILED distinction to avoid raising or
re-triggering leader failover on a merely-deferred teardown lose that
nuance and now treat both as a failure.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  • fix(lvol): address review — exception-based teardown signaling, RPCClient.bdev_get

Addresses mxsrc's review comments on PR #1380:

  • delete_lvol_from_node now raises PreconditionError for a deferred
      teardown (skip/queue) and RuntimeError for a failed one, returning
      normally only once the teardown is confirmed complete. This restores the
      deferred-vs-failed distinction lost when NodeTeardown was removed, using
      the exception machinery the codebase already has instead of a bespoke
      enum, and matches the "raise specific exceptions, never return
      None/booleans for errors" convention.
  • Added RPCClient.bdev_get (mirrors subsystem_get): a single-bdev lookup
      that returns None for "gone" and raises RPCConnectionError/RPCHTTPError/
      RPCProtocolError/RPCRemoteError for a genuine failure. _remove_bdev_stack
      and lvol_bdev_absent_on_node now use it directly, replacing the
      module-local _bdev_present tri-state helper and get_bdevs_2.
  • lvol_events._lvol_event now always resolves the event's cluster via the
      volume's pool instead of falling back from the (possibly missing) node
      record — the pool reliably exists for the whole LVol lifecycle.
  • Updated every delete_lvol_from_node call site (lvol_controller.py,
      lvol_monitor.py) for the raise-not-return contract, and the affected
      unit/integration tests.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  • fix(lvol): fix two pre-existing bugs in process_lvol_delete_finish

Both were pre-dating this branch (present since the original commit
aeb3361 / the merge-from-main at 243acf9) and failing CI's
integration tier independently of the NodeTeardown work:

  • post_lvol_delete_rebalance indexed db.get_cluster_capacity(...)[0]
      unguarded. lvol_del_start_time defaults to 0.0, so a delete finishing
      without pre_lvol_delete_rebalance() having run first (or after a
      monitor restart) computes diff from the epoch, requests a capacity
      window over that whole span, and IndexErrors on an empty result.
      Now guards on an empty records list.

  • The primary-not-yet-confirmed-absent branch returned before the peer
      sync-delete loop ran at all, so a peer already cleared while the
      primary catches up was never recorded in sync_deleted_nodes -- the
      next pass would re-walk an already-clean peer blob tree. The peer
      loop, its bookkeeping, and the del-sync-lock reset now always run;
      only the final record removal is gated on the primary's confirmation.

Verified via a full tox integration run (1300 tests) on a dev box with
real Docker/FDB -- both fixed tests pass and nothing else regresses.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

  • fix(constants): update orphan check interval to run once a day

Co-authored-by: wmousa eng.ledoo@gmail.com
Co-authored-by: Claude Opus 5 (1M context) noreply@anthropic.com

@github-actions github-actions Bot added the target/R26.3 Targets release branch R26.3 label Sep 22, 2026
…#1380)

Backport of PR #1380 to R26.3.

R26.3 field report: four lvols existed as bdevs in SPDK with no record in
FDB. One had been created and deleted per the cluster log but never passed
through in_deletion; two showed a create and no delete at all; the fourth
left no cluster-log trace whatsoever.

Every path behind that shares one shape -- the record removal did not
depend on the data-plane teardown having happened:

* _remove_bdev_stack returned a constant True. A failed removal was logged
  and the entry stamped status='deleted' anyway, so delete_lvol_from_node
  reported success and lvol_monitor erased the record over a live bdev.
  It now returns False and leaves the entry unmarked so a retry re-attempts
  it.

* The "already gone?" probe was `if not rpc_client.get_bdevs(name)`.
  get_bdevs returns None both for -ENODEV and for a non-200 from the SPDK
  proxy, so one transient hiccup during a mass delete skipped the delete
  entirely and recorded it as done, leaving nothing but an INFO line.
  RPCClient.bdev_get is tri-state; an unknown answer now falls through to
  the delete rather than skipping it, since the probe was only an
  optimisation. -19 from the delete itself counts as confirmation.

* delete_lvol_from_node now raises PreconditionError for a deferred
  teardown (skip/queue) and RuntimeError for a failed one, returning
  normally only once the teardown is confirmed complete -- callers that
  used to check a truthy/falsy return now catch the exception instead.

* process_lvol_delete_finish skipped the leader's sync delete outright when
  the leader was merely unreachable or restarting, logged a failure and
  stepped over it, and removed the record regardless -- although that sync
  delete is the ONLY operation that removes the blob metadata and
  unregisters the bdev. It is now gated on DONE plus a fresh probe
  confirming the bdev is absent, and on no live peer still owing its leg.
  Record-only retirement of landing volumes (empty bdev_stack) short-
  circuits before the gate: their blob is meant to survive as the
  converted snapshot.

* The in_deletion guard before release_lvol_ns_slot existed on exactly one
  of nine create/clone rollback sites. rollback_create_record() is now the
  single owner, and _fail_after_bdev persists the deletion intent BEFORE
  any teardown RPC, so a rollback whose RPCs raise can no longer leave a
  record the caller then erases. Two other create/clone rollback sites
  that called delete_lvol_from_node with LVol/StorageNode objects instead
  of ids (silently deleting nothing, per the old function's
  `except KeyError: return True`) now pass ids and route through
  rollback_create_record too.

* delete_lvol's missing-node force branch erased the record without a
  single RPC and without an event -- for an HA volume the blob lives on
  every LVS member, whose node records usually still exist. It now tears
  down on the surviving peers and logs loudly when it cannot confirm.

* lvol_events._lvol_event dropped the event when the node record was
  unreadable, so a volume whose node was gone produced no create, delete or
  status entry at all. It now falls back to the pool's cluster.

Reconciliation: storage_node_ops.auto_repair already compared SPDK's
inventory against the database, but only as an operator-invoked CLI command
that prints to stdout, so nothing ever ran it and every leak above was
permanent and invisible. The blob-level comparison moves to
find_orphan_lvstore_blobs(), and lvol_monitor runs a detect-only sweep
(LVOL_MONITOR_ORPHAN_CHECK, once a day) that logs and raises a cluster
event. It never deletes: a blob it cannot correlate is the one whose
ownership it understands least.

Two pre-existing bugs in process_lvol_delete_finish, independent of the
above and failing CI's integration tier on their own:

* post_lvol_delete_rebalance indexed db.get_cluster_capacity(...)[0]
  unguarded; a delete finishing without pre_lvol_delete_rebalance() having
  run first computed diff from the epoch and IndexErrored on an empty
  result. Now guards on an empty records list.
* The primary-not-yet-confirmed-absent branch returned before the peer
  sync-delete loop ran at all, so an already-cleared peer was never
  recorded in sync_deleted_nodes. The peer loop, its bookkeeping, and the
  del-sync-lock reset now always run; only the final record removal is
  gated on the primary's confirmation.

Tests: tests/unit/test_lvol_teardown_confirmation.py covers the pure logic
(the tri-state probe, _remove_bdev_stack's honest result). tests/
integration/test_lvol_delete_record_gate.py covers the record-lifetime
gate, the rollback guard and the orphan sweep against real FDB.

Also fixes two mypy findings unrelated to this fix but blocking this PR's
type-checker gate: simplyblock_core/services/tasks_runner_node_add.py's
module-level `_inflight`/`_inflight_addrs` sets lacked element type
annotations, which a newer mypy on this branch now flags.

R26.3 lacks several main-only features the original PR's diff assumed as
context (the HA replica is_primary sync-delete path in _fail_after_bdev,
and rolling back every placed node -- not just the target -- in the
migration/fail-over clone path). Those hunks are adapted to R26.3's
simpler node model rather than ported wholesale: this backport applies
only the teardown-confirmation gate, not those unrelated main-only
features.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Hamdy-khader
Hamdy-khader force-pushed the R26.3-fix-lvol-record-teardown-gate branch from 5b7b750 to 76565e5 Compare September 22, 2026 18:21
R26.3's CI runs on Python 3.9 (tox.ini basepython, and the pip/mypy
workflow jobs). `def bdev_get(self, name) -> dict | None:` parses fine
everywhere but evaluates `dict | None` as a real expression at function-
definition time, and `type.__or__` doesn't exist before 3.10 --
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'.

Because rpc_client.py is imported transitively by nearly every module,
this broke collection for the type checker (238 files) and for pytest
across ~150 unrelated test files that never touch RPCClient directly --
the traceback in each case bottoms out at this one line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Hamdy-khader
Hamdy-khader merged commit 01eadc7 into R26.3 Sep 23, 2026
7 checks passed
@Hamdy-khader
Hamdy-khader deleted the R26.3-fix-lvol-record-teardown-gate branch September 23, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

target/R26.3 Targets release branch R26.3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant