Skip to content

cleanup logic reworked - #1204

Open
EbiRider wants to merge 60 commits into
R26.3from
main
Open

cleanup logic reworked#1204
EbiRider wants to merge 60 commits into
R26.3from
main

Conversation

@EbiRider

Copy link
Copy Markdown
Collaborator

No description provided.

EbiRider and others added 15 commits July 30, 2026 08:59
reworked cleanup implementation for lvol migration
…lt values and API parity params change (#1198)

* Add restart phase for mass create tests

* Fixing reserved cpu set in openshift baremetal k8s and changing default values and API parity params change

* Fixing reserved cpu set in openshift baremetal k8s and changing default values and API parity params change

* Fixing reserved cpu set in openshift baremetal k8s and changing default values and API parity params change

* Add K8s host-level core dump collection to e2e test framework

Previously, check_core_dump() only detected core dumps inside SPDK pods
at /etc/simplyblock/ but never copied them, and completely missed host-level
core dumps at /var/lib/systemd/coredump/. When SPDK crashed in K8s, all
crash evidence was silently lost after test runs.

Changes:
- k8s_utils.py: Add copy_core_dumps_from_spdk_pod() to kubectl cp core
  files from inside SPDK pods to the NFS log directory
- k8s_utils.py: Add collect_host_core_dumps() which uses the running SPDK
  pod (privileged, accesses host via /proc/1/root/) as primary path, with
  platform-aware fallback when SPDK pod is down:
  - OpenShift: oc debug node/ + chroot /host
  - Vanilla K8s: privileged pod with nsenter
  - Talos: privileged pod with hostPath volume mount (no host binaries needed)
- k8s_utils.py: Promote get_all_k8s_node_names() and detect_openshift()
  from continuous_k8s_native_failover.py to shared K8sUtils class
- cluster_test_base.py: Enhance check_core_dump() to actually copy pod core
  dumps and collect host-level core dumps via _check_host_core_dumps_k8s()
- continuous_k8s_native_failover.py: Refactor to delegate to K8sUtils methods

* Separate mass stress test into restart and no-restart scenarios

Existing mass create/delete tests had node outages (Phase 3b/7b) baked
into every run, conflating capacity testing with restart resilience.
This splits them into two distinct scenario types:

- Existing tests: ENABLE_NODE_OUTAGE defaults to False, so all current
  tests now run at full entity count without node restarts, enabling
  proper max-capacity delete testing.

- New restart variants: 6 new classes (Docker + K8s) with a 6000 total
  entity cap, node outages at Phase 3b/7b, and PERSISTENT_RETRY=True.
  Entity cap formula (MAX_ENTITY_COUNT // (1 + SNAPSHOTS_PER_LVOL))
  dynamically reduces lvol count per snapshot ratio:
    1 snap/lvol  → 3000 lvols
    6 snaps/lvol →  857 lvols
   10 snaps/lvol →  545 lvols

* Fix restore wait to fail test immediately on restore failure

_wait_for_restore_task_done had two bugs that masked restore failures:

1. except Exception swallowed AssertionError raised when task status was
   "failed", turning it into a warning log and continuing to poll until
   timeout. Added explicit re-raise for AssertionError.

2. After timeout expired, the method logged a warning and returned,
   letting callers proceed with checksum verification on incomplete
   restores — producing misleading "checksum mismatch" failures. Now
   asserts immediately on timeout.

Also adds missing non-restart mass stress test variants:
- MassCreateDeletePersistent_300x10_6Snap_K8s
- MassCreateDeletePersistent_300x10_10Snap_K8s
- MassCreateDeletePersistent_300x10_10Snap_Docker

* Capping clone creates

* Capping clone creates

* Fix K8s mass stress test: cleanup deleting infra PVCs, FIO leak, stall timeouts

- Scope PVC cleanup to test prefixes only (mcd-pvc-*, clone-pvc-*).
  The cleanup was deleting ALL PVCs in the namespace including
  simplyblock infrastructure PVCs, destabilising the cluster.
  Remove the kubectl delete pvc --all fallback on timeout.

- Delete Phase 6 FIO Jobs before Phase 7 clone PVC deletion.
  FIO pods left running block PVC finalizer removal, causing
  50 PVCs to stall for 300s+ and fail the test.

- Add DELETE_STALL_TIMEOUT (600s) separate from BOUND_STALL_TIMEOUT
  for delete verification. CSI provisioner processes PV deletions
  serially (~1.5s/vol), so 300s is too short at 850+ PVCs.

- Fix backend verification to call sbcli_utils.list_lvols() and
  sbcli_utils.list_snapshots() instead of k8s_utils which lacks
  these methods, causing AttributeError and skipping verification.

* Increases max entity to 9000

* Fix K8s mass create cleanup: respect deadline and preserve_resources_on_failure

- Increase MAX_TEST_DURATION from 6h to 10h for K8s tests
- Increase CLEANUP_TIMEOUT from 30min to 1h
- All cleanup steps now check deadline before running
- Replace unbounded sbcli_utils.delete_all_snapshots() with
  deadline-aware _cleanup_delete_backend_snapshots_with_deadline()
- Increase volumesnapshot batch delete size from 50 to 200
- Skip internal cleanup when preserve_resources_on_failure is set
  and test has failed (soft failure or exception)

* Fix upgrade test infrastructure: migration script, version detection, helm release

- Docker R25→R26 migration script: add missing mini lvol/snapshot re-write
  steps that existed in K8s version and UPGRADE.md but were absent in Docker
- Docker _is_r25_to_r26_upgrade(): fix detection to handle 'main' as target
  branch (trigger when base is R25 and target is NOT R25, instead of
  requiring target to start with 'r26')
- K8s HELM_RELEASE_SBCLI: change default from empty string to 'sbcli' so
  the R25 sbcli chart is actually uninstalled during R25→R26 migration
- K8s workflow: export HELM_RELEASE_SBCLI env var to test execution step
  with conditional value based on upgrade_type

* Update K8s upgrade workflow: r25_base_config default to remove_snode_init_container

The p2p-migration branch is outdated; remove_snode_init_container is the
current R25 branch used for K8s R25→R26 upgrade bootstrapping.

* Update K8s workflow image defaults: docker.io → ECR/Docker Hub shorthand

Replace outdated docker.io/simplyblock/ registry prefixes across all K8s
workflow files to match the current convention (topology-suite pattern):
- simplyblock_repository: public.ecr.aws/simply-block/simplyblock
- operator_repository: simplyblock/simplyblock-operator
- csi_repository: simplyblock/spdkcsi
- spdk_image: simplyblock/spdk:main-latest

Affected workflows: k8s-native-e2e, k8s-native-stress, k8s-native-e2e-add-node,
k8s-native-e2e-node-migration, monitoring-suite-k8s-native, k8s-native-upgrade.

* Align upgrade workflow defaults with e2e/stress pipelines

- Fix IP defaults: 192.168.10.211 → .210 (mgmt), 205-208 → 201-204 (storage)
- Remove NR_HUGEPAGES input (hardcode '2048' in env, override via EXTRA_SN_ARGS)
- Fix BOOTSTRAP_DATA_CHUNKS default: 2 → 1 (match e2e/stress)
- Fix BOOTSTRAP_ENABLE_NODE_AFFINITY default: false → true
- Add missing inputs: EXTRA_CLUSTER_ARGS, EXTRA_SN_ARGS, CLUSTER_SECURITY
- Add "Write cluster security/backup config" step before bootstrap
- Add EXTRA_CLUSTER_ARGS/EXTRA_SN_ARGS support to bootstrap step

Applied to: upgrade-bootstrap.yml, upgrade-bootstrap-single.yml,
upgrade-bootstrap-single-v2.yml.

* Fix upgrade test: mandatory target images, unconditional DB migration, pip error handling

- Make --target_spdk_image and --target_docker_image required in upgrade_e2e.py
  to prevent silent failures when images are empty
- Rename _is_r25_to_r26_upgrade() to _needs_db_migration() and run migration
  for all cross-version upgrades (not just R25→R26)
- Add raise_on_error=True to _pip_install_target so pip failures are caught
- Add default TARGET_SPDK_IMAGE and TARGET_DOCKER_IMAGE env vars in all 3
  upgrade workflow files

* Fix Slack notification skip and add test names to pipeline run-name

- Add send_slack_notification to e2e-bootstrap.yml workflow_dispatch inputs
  (was only in workflow_call, causing null != false to skip Slack step)
- Add run-name with test class/case to e2e-bootstrap.yml and e2e-docker.yml

* Fix api-parity-audit: pass send_slack_notification and lower max-subsys to 40

* Fix api-parity-audit: register TestAPIParityAudit in ALL_TESTS and add parity keyword

- Add TestAPIParityAudit to ALL_TESTS list in e2e/__init__.py so the
  e2e runner can discover it
- Import get_parity_tests and add "parity" keyword handler in e2e.py
- Fix K8s backup merge detection in TestBackupPolicyVersionsOne: detect
  pruning by backup count decrease (K8s CRD) in addition to status field
  (Docker/sbcli mode)

* Add missing sbcli_utils_v2.py required by TestAPIParityAudit

The file was missed when test_api_parity_audit.py was originally
committed (e2ee3ba). The import fails in CI because the module
is not tracked. Force-added past the sbcli* gitignore pattern.

* Fix api-parity-audit: register TestAPIParityAudit in ALL_TESTS and add parity keyword

- Fix get_io_stats() missing cluster_id argument in cluster.iostats audit
- Add detailed summary output to test log (findings by category)
- Fail the test on error-level findings instead of silently passing
- Register TestAPIParityAudit in ALL_TESTS so --testname discovery works
- Disable lvol-level backup delete tests (SFAM-2792):
  TestBackupRetentionMergeAfterDelete, TestBackupDeleteAndRestore,
  TestBackupDeleteInProgress
- Revert SBCLI_BRANCH default back to 'main' (bootstrap uses it)

* Add parity report artifact upload to e2e pipeline

Copy api_parity_report.html and api_parity_findings.json from NFS
into sbcli/e2e/logs/parity_report/ so they are included in the
existing log artifact upload step.

* Disable backup delete operations in stress tests (SFAM-2792)

- BackupStressMarathon: remove delete_and_backup from weighted
  operations and comment out _do_delete_and_backup method
- BackupStressRetentionMergeCycles: disable entirely since its
  sole purpose is delete-merge-restore cycles

* Add parity audit findings summary to e2e-bootstrap job summary

When TEST_CLASS is TestAPIParityAudit, parse the api_parity_findings.json
sidecar and append a severity breakdown table (errors/warnings/info) to
the GitHub Actions job summary.
Comment on lines +45 to +57
uses: ./.github/workflows/e2e-bootstrap.yml
with:
TEST_CLASS: TestAPIParityAudit
RUN_LABEL: api-parity
STORAGE_PRIVATE_IPS: ${{ inputs.STORAGE_PRIVATE_IPS || '192.168.10.201 192.168.10.202 192.168.10.203 192.168.10.204' }}
API_INVOKE_URL: ${{ inputs.API_INVOKE_URL || 'http://192.168.10.210/' }}
BASTION_IP: ${{ inputs.BASTION_IP || '192.168.10.210' }}
MNODES: ${{ inputs.MNODES || '192.168.10.210' }}
SBCLI_BRANCH: ${{ inputs.sbcli_branch || 'main' }}
CUSTOM_IMAGES: ${{ inputs.CUSTOM_IMAGES || 'spdk="simplyblock/spdk:main-latest" docker="simplyblock/simplyblock:main"' }}
send_slack_notification: ${{ inputs.send_slack_notification || github.event_name == 'schedule' }}
BOOTSTRAP_MAX_SUBSYS: "40"
secrets: inherit
mxsrc and others added 14 commits August 1, 2026 19:49
More recent linter versions support more checks and enable them by
default. Generally, we should try to adopt them to arrive at a more
cherent codebase. Instead of pinning an old ruff version, this
explicitly disables all checks that we fail at present. We should try
and remove the violations and enable the checks successively. For those
checks that we simply do not agree with, we can document explicit
exceptions.
…d CVEs

The Trivy job in .github/workflows/security.yml was failing daily. Two
separate causes:

- docker/Dockerfile's `COPY . /app` was shipping the repo's local
  .tox/.mypy_cache/.ruff_cache/.pytest_cache dirs into the scanned image
  (.tox alone added ~900MB across several stale python3.9/3.12/3.13
  virtualenvs, each with its own old, genuinely vulnerable pip/wheel).
  None of these are excluded by .dockerignore, so every local tox/mypy/
  ruff run before a build fattens the shipped image with real CVEs.
- The remaining 3 findings (msgpack 1.1.2, setuptools 70.3.0) come from
  pip 26.2's own vendored bundle (pip/_vendor), not from anything we
  install; pip still ships those exact versions upstream, so there is
  no fix available from our side. Added .trivyignore.yaml to suppress
  just those 3 finding IDs, wired via the trivyignores input.

Verified locally end-to-end (build docker/Dockerfile, scan with
aquasec/trivy:latest replicating the workflow's flags) — image shrank
from 1.96GB to 1.09GB and the scan now exits 0 with zero findings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix: lvol migration getting killed and cleanup not running
#1203)

calculate_core_allocations()'s colocate_lvs branch put lvol_poller_core
  on app_thread_core's slot. Since jc_singleton_core is assigned in every
  branch, storage_node_ops.py's poller-group RPC unconditionally
  preferred jc_singleton_mask (added in e3e8fd0 to force the two onto
  the same core) � which silently clobbered the >=32 vCPU tier's
  deliberately dedicated lvol_poller core every time, defeating the
  point of giving it one.

  Colocate with jc_singleton_core's slot instead, so lvol_poller_mask
  becomes the single source of truth: equal to jc_singleton_mask when
  nothing dedicated was assigned, distinct when it was. Update
  add_node()/_restart_storage_node_impl() to use lvol_poller_mask
  directly (jc_singleton_mask only as a last-resort fallback if that
  reservation came up empty) and fix their now-stale comments to match.

  Add regression coverage for calculate_core_allocations' colocation
  behavior across all three size tiers � there was none before.
…9 incident)

A healthy node was force-shut and restarted a second time: while its
restart task was still running on the parallel pool, the dispatch mode
flipped (fd_dead_recovery_allowed went false as the first domain peers
came back ONLINE) and the main loop's inline path — which consulted
neither _restart_inflight nor _node_inflight — re-entered the same task.
Every guard in the second entry was blinded by two lost updates from
full-object writes of stale in-memory copies: the defer path's
task.write_to_db un-canceled the task and wiped its owner lease, and
_persist_target_device_event reverted the node's committed
in_restart->online flip back to in_restart.

- tasks_runner_restart.main(): single dispatch path — every execution
  checks the inflight maps and registers its future; serialized mode
  submits identically and waits (fut.result()), so parallel<->inline
  mode flips are harmless in both directions.
- tasks_runner_restart: new _task_finish/_task_update helpers write
  tasks via db.atomic_update (CAS on the fresh row, updated_at lease
  stamp) — a write can no longer resurrect a concurrently canceled/done
  task or clobber its owner lease. All task_runner_node writes
  converted; a lost CAS means another actor owns the outcome and the
  runner stops. Plus a fresh task re-read immediately before the
  destructive shutdown step.
- distr_controller._persist_target_device_event: atomic_update whose
  mutator touches only the device entries — concurrent node status
  flips survive device-event fan-out during restart waves.
- storage_node_ops.shutdown_storage_node: honour the result of the
  final OFFLINE set_node_status instead of reporting success over a
  half-committed shutdown; deliberately NOT whitelisting the
  RESTARTING->OFFLINE flip (it would strand a genuine concurrent
  restart whose final ONLINE CAS then gets refused).
- shutdown_storage_node/check_node_shutdown_preconditions: thread
  current_restart_task_id (bare uuid) through so the restart runner's
  own cleanup shutdown is not reported as a competing restart task.
- tests/unit/tasks: fake db mirrors the atomic_update contract.

Follow-ups (not in this change): unconditional node-RESTARTING entry
guard + lease-aware watchdog skip, atomic _add_task creation (duplicate
task TOCTOU), gating the self-heal force-ONLINE branch, converting
task_runner_device writes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design rule: the control plane forces LVS leadership only where it is
structural and race-free — lvstore creation, activation, and the restart
flow's fenced demote->grant handoff (recreate_lvstore ### 5/### 7).
Recovery paths repair the redirect topology and wait for IO-driven
self-promotion; they never grant.

Incident 2026-07-30 (sb_logs_20260730_195000_30m, LVS_9): the guarded
last-resort grant fired from the lvol-migration runner 0.45s before the
dead primary's restart task was even created (the "no handoff task
active" guard raced task creation), seating the secondary as writer.
When the primary's restart later ran its own fenced demote->grant
handoff, writer_conflict events fired on jm_vuid=9 and the demoted
secondary's JM write lock lingered for its full ~64s lease
(jfi_r_wr_lock tms_delta=64103ms on both JMs), blocking the new
leader's journal writes for that window. Earlier motivations stand too:
a CP-forced grant outside the restart flow skips the primary's blob-md
reload (2026-07-06 LVS_13 stale metadata) and the run-20260725
grant/demote flapping.

- _recover_leaderless_lvs: keep the single-flight lock, hublvol repair
  and bounded self-promotion wait; drop the last-resort
  bdev_lvol_set_leader(leader=True) — still-leaderless now returns None
  and object operations keep failing fast (no_leader_cache) until IO
  promotes the primary or a restart re-places leadership.
- Remove the grant-only guard helpers _leadership_moving_tasks_active
  and _taker_jm_quorum_ok.
- tests: leaderless recovery must never call bdev_lvol_set_leader.

Open data-plane item (separate): the demote path does not release the
JM write lock, so every handoff — including IO-driven self-promotion —
waits out the ~64s lease before the new writer can write the journal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

The namespaced-subsystem pick (get_next_available_subsystem_on_node) and
the in_creation record write were separate reads/writes, so two concurrent
creates/clones could both count the same shared subsystem as having one
free namespace slot and both join it past capacity.

DBController.claim_lvol_ns_slot now runs pick + record write in ONE FDB
transaction: the record itself is the slot claim (occupancy is recounted
from lvol records inside the transaction). A per-node allocator key gives
concurrent claims a read conflict so the loser retries and recounts with
the winner's record present, while the lvol-table read itself is a
snapshot read (no conflict range over the whole table — unrelated lvol
writes must not abort claims). Wired into create_lvol, snapshot clone,
and the -32602 add-time fallback, which now also excludes the subsystem
SPDK just rejected instead of being able to re-pick it forever.

release_lvol_ns_slot is the rollback half: record + mini removed in one
transaction, releasing the slot atomically. All create/clone rollback
paths and the delete-path record removals go through it.

Also: get_next_available_subsystem_on_node treats only all_lvols=None as
"not provided" — an empty in-transaction snapshot result used to trigger
a fresh full-table read outside the caller's transaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Commit 5c4c2ff gated every migration target-lvol create on
_ensure_lvstore_primary_leader, which queries bdev_lvol_get_lvstores —
a method the migration-tier mock SPDK server never implemented. Every
migration integration test has failed at start_migration since
("Lvstore lvs_tgt not found on <node>", 112 failures, red since
2026-07-27's first post-merge run).

The mock node is by construction the sole primary/leader of its
lvstore, so the handler reports lvs_primary + "lvs leadership" true and
returns an empty list for unknown lvs names (which the guard maps to
its not-found error). Registered in _METHOD_ERROR_CODES so failure-rate
injection exercises the guard's retry path too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
michixs and others added 30 commits August 4, 2026 20:14
…sts, replica-gate suspend

Three independent breakages:

1. argcomplete 3.7.1 (released 2026-08-04, unpinned) uses PEP 604 unions
   at runtime and breaks py3.9 at import — the whole unit job died at
   collection. Pinned <3.7.1.

2. 13 mock-based migration tests drifted from production signatures:
   _get_target_secondary_node grew a required src_node_id (overlap-drain
   rule), _collect_snap_ancestry now returns the set (the protect helper
   was folded into set subtraction), _delete_bdev_blocking passes
   sync/special_delete and polls time.monotonic(), start_migration grew
   cluster preconditions, and snaps owned by a volume that migrated away
   are now cleanup candidates. Updated the tests to current behavior and
   added coverage for the new overlap-drain and migrated-away-owner rules.

3. Real bug caught by test_secondary_blocked_when_secondary_in_bad_state:
   a target secondary/tertiary in a transient state (in_restart, ...)
   returned an error triple from the phase handlers, which the dispatcher
   counts against the migration retry budget — 5 quick retries later the
   migration entered cleanup_target and FAILED because a replica was
   restarting. The six replica-state gates now suspend via error_message
   without charging retries, matching the target-offline and
   cluster-not-active gates (same design as 5e57b45 for source-offline).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nvariant, no FD migration

Role placement on FD clusters only affects availability (durability is
covered independently by chunk anti-affinity and the JM per-domain caps),
so the role-level contract is: every LVS keeps at least one cross-domain
non-leader role, ensuring a full-domain outage never strands an LVS with
zero paths while its data is still readable.

- planner: fd_interleaved_host_order, rotation_layout (public, with the
  FTT+1 host floor), compute_fd_layout_violations, fd_balance_violation.
  Interleaved rotation makes every secondary cross-domain when balanced
  and confines the +1 case to exactly one degraded LVS (tertiary-covered).
- preconditions: add/remove/current admission on per-HOST (mgmt_ip)
  domain counts — spread <= 1, and >= 2 hosts per domain once an HA
  layout exists. Pre-activation assembly stays unrestricted.
- executor: FD-aware fresh planning — recover the actual rotation from
  the secondary-pointer chain (DB-order fallback repairs drift), pass the
  actual layout as current_layout, and choose the newcomer insertion
  point by trying cyclic shifts; refuse when no placement satisfies the
  invariant (e.g. FTT1 at odd populations). Multi-slot hosts are refused
  on FD expansion for now.
- storage_node_ops: host failure-domain is immutable (re-add/slot-add
  with a different id is rejected — FD migration is not supported);
  expansion add and node removal run the FD admission checks;
  _pick_replica_relocation_node enforces cross-domain HARD when the
  relocated replica would be the primary's only cross-domain role.
- cluster_ops: fresh activation of an FD cluster requires >= 2 domains,
  EQUAL host counts per domain and single-domain hosts, and assigns
  sec/tert via the interleaved rotation instead of greedy picks.
  Re-activation is deliberately not blocked (recovery path).

tests: tests/unit/test_fd_topology_policy.py (38 cases); full unit tier
green (912 passed), ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test takes a random node offline for 2s mid-migration and asserted
STATUS_DONE unconditionally. When the random pick is the TARGET and a
runner tick observes the outage, the runner's deliberate policy is to
fail the migration into cleanup_target (rollback) — not suspend/resume
as the docstring claimed — so the test failed ~25% of runs by
construction (observed: green and red slow-job runs on the same sha).

Accept both documented outcomes for the target case: DONE (outage window
missed between ticks) or FAILED with a verified completed rollback (phase
cleanup_target, volume still served from the source). Source/passive
picks still require DONE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This makes dependency and venv handling automatic.
This avoids version warnings about exectutions on the wrong node
version.
In this case (method not found) we use RPC jc_get_jm_status
Three hard limits, enforced on every create path (lvol create, snapshot
create, clone):

- 6000 objects per lvstore (MAX_OBJECTS_PER_LVSTORE): lvols + clones +
  snapshots, counted against the owning node — each node owns exactly
  one lvstore, so this is the per-lvstore count and a host temporarily
  serving a second LVS (takeover) gives each active lvstore an
  independent budget. Replaces the per-core cap (cores x 2000); the
  hard limit no longer scales with the SPDK core mask and no longer
  exempts nodes with a missing/unparseable mask.
  check_node_object_limit -> check_lvstore_object_limit.

- 50 namespaces per subsystem (MAX_NAMESPACES_PER_SUBSYSTEM):
  caller-supplied max_namespace_per_subsys above the cap is rejected at
  create; joins into legacy subsystems recorded with a larger max stop
  at the cap (get_next_available_subsystem_on_node bounds the recorded
  max — the FDB claim transaction recounts through the same function,
  so the join path is enforced transactionally); new subsystems seeded
  in _claim_lvol_ns_slot_tx clamp their recorded max to the cap.
  The default for namespaced creates stays
  LVO_MAX_NAMESPACES_PER_SUBSYS (32).

- 75 subsystems per node (MAX_SUBSYSTEMS_PER_NODE): applied as a
  ceiling over the node's configured max_lvol
  (max_subsystems_for_node = min(max_lvol, 75), preserving smaller or
  zero configured values) at placement, the advisory pre-check, the
  clone path, and authoritatively in _claim_lvol_ns_slot_tx.

Counting stays on the TTL-cached mini records (no full-table scan on
the create path — run-20260721 regression). Concurrent creates within
one cache window can overshoot the lvstore cap slightly; strict
never-exceed would need a transactional per-lvstore counter like the
namespace-slot claim.

Tests: tests/unit/test_object_limit_per_lvstore.py (replaces the
per-core suite), tests/unit/test_subsystem_limits.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run mass_create_delete_docker-20260730-134740: the async+sync delete
chain of a SINGLE lvol/snapshot took 4-7 minutes (a 7-object
clone->snapshot chain ~50 min), while every SPDK step completes in
milliseconds (e.g. SNAP_2379: API phase-1 15:35:22, the monitor's one
and only touch at 15:39:11 finished poll + 3 sync deletes + DB in
73 ms). The latency was entirely monitor-side:

1. Inline sleeps inside the SERIAL per-object loop: snapshot_monitor
   slept 1s after every phase-1 (1,006 sleeps = ~17 min of one hour;
   cycle time 4-5 min), lvol_monitor slept 3s. Removed - the
   delete-status poll already handles a still-running async delete
   (ret 1 = retry next cycle).
2. Leadership probe (1-3 bdev_lvol_get_lvstores RPCs) ran once PER
   OBJECT per cycle. Now cached per cycle/pass; mid-cycle leadership
   moves are still handled by the poll error codes (-35/4).
3. Chain hops cost a full extra cycle each:
   - the clone-blocker check used the cycle-start mini-lvol snapshot,
     so a snapshot stayed blocked a whole cycle after its clone
     finished; re-read the single blocking record before deferring.
   - the 30s idle interval bounds every hop; while any in-deletion
     object exists both monitors now re-scan every 2s
     (LVOL_MONITOR_DELETION_INTERVAL_SEC), idle clusters keep 30s.

Expected: single-object delete finishes within one short cycle
(seconds); an N-hop chain drains in ~N short cycles instead of
N x 4-7 min. Full test suite passes (923 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion use ensuring 10 second cooldown between detch and attach
…d compression-thread path (#1217)

The compression-thread JM CPU layout was gated behind
JM_COMPRESSION_THREAD_ENABLED, permanently False on main with no way to
enable it -- both the dedicated-compression-core allocation and the
lvol_poller/jc_singleton colocation it gated were dead code. Removed the
flag and the dead compression-core allocation entirely (bdev_jm_create's
compression_thread/compression_cpu_mask params, the RPC call, and the
model field are left in place but now always inert, since nothing
references the deleted constant anymore).

Behavior change: colocate_lvs (lvol_poller co-locating with
jc_singleton's core below 32 vCPU, freeing a core) is now unconditional
on vcpu count instead of also requiring the removed flag -- this part of
the gated logic is kept and permanently enabled, only the
compression-specific half is dropped.

The return tuple's 9th (compression_core) slot is kept, always empty via
the existing assigned.get(..., []) default, rather than removed --
several callers index into this tuple positionally (distribution[8]),
and shrinking it would silently IndexError rather than fail loudly.

tests/unit/test_lvol_poller_jc_colocation.py (previously untracked,
written for the still-unmerged lvol_poller/jc_singleton colocation fix
that landed without it) rewritten to drop the now-removed
compression_enabled parameter and assert the sole remaining behavior
across all three size tiers.
2026-08-06 soak iter-50: a manual CLI `sn restart` and the restart task
runner drove the SAME node's restart concurrently. The task runner calls
restart_storage_node with force=True (bypassing every status/task guard),
try_set_node_restarting's FDB tx only excluded PEERS (it explicitly skipped
the target node), and the task lease could not discriminate the actors:
both share the one NODE_RESTART task, and the lease owner id (hostname)
collides for a CLI and a runner service on the same mgmt host. Result:
dueling spdk_process_start calls replacing each other's SPDK container
mid-restart (banners 3s apart), the refused CLI attempt's failure cleanup
killing the runner's container, and the runner's retry shutting down over
the CLI's next in-flight attempt at finalization. a61b00a's inflight maps
could not catch this — they are process-local to the runner service.

The claim is an (owner-token, timestamp) pair on the StorageNode row:

- acquired atomically inside _try_set_node_restarting_tx: refuse when the
  target itself is RESTARTING/IN_SHUTDOWN under a FRESH claim held by
  anyone else — in BOTH tx modes (allow_concurrent_peers relaxes peer
  exclusion, never same-node exclusion). force does not bypass it.
- a stale claim (older than RESTART_CLAIM_TTL_SEC) or an empty one is
  takeover-able: the transferable-ownership resume path for a driver that
  died mid-restart stays fully alive.
- heartbeated by the restart_storage_node wrapper (owner-matched CAS, lands
  only once acquired), released on every exit path.
- the wrapper's failure cleanup (_kill_spdk_until_dead + OFFLINE flip) now
  runs only when THIS call holds the claim — proven by token, not inferred
  from pre_status, which was also true for attempts refused before
  acquisition and made their cleanup destroy the rightful owner's
  in-flight SPDK container.
- check_node_shutdown_preconditions refuses (not force-overridable, not
  task-id-exemptable — both actors share the task) to shut down a node
  mid-transition under a live foreign claim; a dead driver's claim expires
  within the TTL, which is the sanctioned takeover wait.
- task_runner_node defers without consuming retry budget when a live
  foreign claim exists, mirroring the peer-exclusion pre-check.

27 new unit tests (tests/unit/test_restart_claim.py); unit tier 950 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants