Repository navigation
Node model: Don't lose locks when lock_many hits a DB error - #154
Merged
Merged
Conversation
Node.update() is wrapped in retryOperation, which handles a DB error by rolling back the session and calling update() again. That is fine when the transaction only changes one node, but Node.lock_many() locks all of the nodes it found in a single transaction. If the flush for the third of four nodes fails (e.g. with a serialization failure, since lock_many runs SERIALIZABLE), the rollback also undoes the first two, only the third is retried, and the fourth follows. The request then commits and returns all four nodes, two of which are still unlocked in the database. teuthology believes it holds all four and starts the job; the next lock_many request is handed the two that are still free. Both jobs then reimage and run ansible on the same machines at the same time, which shows up as reimages landing on the wrong OS, LVM/dmsetup errors in zap_disks, lost ssh connections and so on. Every double allocation with surviving records on the tracker has this shape: the job that lost nodes had asked for at least two, kept at least one of them, and the job that took the others was dispatched within seconds. Split the retry out of update() so that lock_many can update nodes without it. A DB error now propagates to the retryOperation that already wraps lock_many_post, which rolls back and redoes the whole query and update. Fixes: https://tracker.ceph.com/issues/74774 Fixes: https://tracker.ceph.com/issues/80969 Signed-off-by: David Galloway <[email protected]> Assisted-by: Claude Fable 5.1 (claude-fable-5-1)
retryOperation only logged once it had run out of attempts. A retry that succeeded left no trace, even though it rolls back the session first and so discards everything the transaction had done up to that point. That made the lost node locks in https://tracker.ceph.com/issues/74774 impossible to see in the logs. Log a warning with the function and the error each time we roll back and retry. Signed-off-by: David Galloway <[email protected]> Assisted-by: Claude Fable 5.1 (claude-fable-5-1)
dmick
approved these changes
Oct 9, 2026
djgalloway
added a commit
that referenced
this pull request
Oct 9, 2026
Resolves the conflicts with the lock_many lost-locks fix (#154): - decorators.py: keep main's retry logging, roll back request.session (the SQLAlchemy 2 per-request session) instead of the removed paddles.models.Session. - models/nodes.py: lock_many uses the unretried _update() per node, as on main, so a DB error propagates and the whole request is retried. No commit() in the model; the controller commits the request session. - test_nodes.py: port test_lock_many_db_error_mid_request to the pytest-fixture style. Nothing flushes per node on this branch, so the injected OperationalError fires on the first flush that carries dirty objects instead of on a flush call count. - test_decorators.py: test_retry_is_logged patches paddles.decorators.request like the other retry tests, since the retry path now touches request.session. Assisted-by: Claude Fable 5 Signed-off-by: David Galloway <[email protected]>
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.
Fixes the double allocation of test nodes tracked in https://tracker.ceph.com/issues/74774 (most recent instance: https://tracker.ceph.com/issues/80969).
The bug
Node.update()is wrapped inretryOperation, which handles a DB error by rolling back the session and callingupdate()again.Node.lock_many()locks every node it found in one transaction, so when the flush for node K fails:The response claims N nodes; only N-K+1 are locked in the database. teuthology starts the job, and the next
lock_manyis handed the nodes that are still free. Two jobs then reimage and runcephlab.ymlon the same machines at once.lock_manyruns atSERIALIZABLE, so serialization failures are expected whenever other requests are writing tonodes, which is exactly when a run is being dispatched.Evidence
In job
skanta-2026-09-20_02:51:43-rados-wip-bharath22-testing-20260920.002814-main-distro-default-trial/570050, paddles returned trial029, trial120, trial143 and trial177. Two seconds later job 570051 was given trial029 and trial177. 570050's owncheck_lockoutput shows those two nodes with 570051's archive path as the lock description.I went through the incidents on 74774 and its 2026 duplicates that still have records. Nine are real double allocations, and all nine have the same shape:
Every job that lost nodes had asked for at least two and kept at least one. None was a single-node job, although four of the nine jobs that took the nodes were. That is what a partial rollback inside a multi-node transaction produces, and this bug cannot affect a one-node request.
The paddles log already on 74774 (2026-02-11) fits too: the same pod logged
Locked trial190 trial137for 44743 and thenLocked trial190for 44744 half a second later.The change
Node.update()keeps its retry; the body moves toNode._update().Node.lock_many()calls_update(), so a DB error propagates to theretryOperationthat already wrapslock_many_post, which rolls back and redoes the whole query and update.retryOperationlogs a warning each time it rolls back and retries. Until now a retry that succeeded left nothing in the logs, which is why this went unseen.Testing
test_lock_many_db_error_mid_requestinjects anOperationalErrorwhile the third of four nodes is being locked. Onmainit fails with['n2', 'n3'] == ['n0', 'n1', 'n2', 'n3']: four nodes claimed, two locked. With this change it passes.test_retry_is_logged.flake8 --select=Fis clean.Limits
update()no longer flushes there. It still carries the decorator and has no test for this; the test should be ported once this merges.Merging deploys this to Sepia through the ImageStream within a few minutes. There is no migration, so it is an ordinary rolling restart.
Prepared with Claude Fable 5.1.