fix(rollingops): stop etcd integration tests racing the worker poll loop - #640
Merged
patriciareinoso merged 3 commits intoSep 1, 2026
Conversation
The `rollingops` machine integration tests fail on main and on PRs, most often as `test_retry_hold_operation_two_units_single_app` seeing only unit_b's events, and sometimes as `test_retry_release_two_units_single_app` finding 10 of 12 events or a `peer` processing backend. Each unit runs its own etcd worker, which polls its queue every 30s while idle and retries lock acquisition every 15s. The etcd lock is a compare-and-set spinlock, so it is granted in worker-poll order, not in operation-request order: a unit that requests an operation 5s later can still acquire the lock 20s earlier. `time.sleep(2)` between two actions cannot establish ordering against that, and a fixed `time.sleep(60 * 3)` cannot bound a sequence whose length depends on how the poll intervals of two workers line up. Wait for the events the tests expect instead. A second hook can also run after another hook already executed the claimed operation, or while the worker has requeued a retried operation but not yet claimed it again. The in-progress queue is legitimately empty then, but the charm reported that as an etcd/peer inconsistency and fell back to the peer backend for the rest of the operation. Tell that transient apart from a unit that genuinely has no etcd state, and only fall back for the latter. Refs canonical#593 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tonyandrewmeyer
force-pushed
the
fix/rollingops-integration-flake
branch
from
August 25, 2026 02:32
7d54e41 to
fe07c67
Compare
Contributor
|
Thank you @tonyandrewmeyer for taking the time on this. This was an annoying error I was not able to reproduce locally. I will take a look |
patriciareinoso
self-requested a review
August 28, 2026 16:23
patriciareinoso
approved these changes
Aug 31, 2026
patriciareinoso
left a comment
Contributor
There was a problem hiding this comment.
this is excellent work, thanks a lot for it
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.
The
rollingopsmachine integration tests fail regularly onmainand on PRs (#638 has several examples). The dominant signature istest_retry_hold_operation_two_units_single_appseeing only unit_b's events, and it appears in 4 of the last 4 failing runs onmain:test_retry_release_two_units_single_appalso fails, in two variants:assert 10 == ((2 * 2) * 3)andassert 'peer' == 'etcd'.The etcd lock is granted in worker-poll order, not request order
Each unit runs its own etcd worker (
_etcd/_rollingops.py), which sleepsNEXT_OP_SLEEP = 30between idle queue polls andLOCK_ACQUIRE_SLEEP = 15between acquisition attempts. The lock itself (EtcdLock.try_acquire) is a compare-and-set spinlock, not a FIFO queue. So the time between enqueuing an operation and acquiring the lock is essentially uniform in [0, 30] s per unit, and the order in which units get the lock bears no relation to the order in which the operations were requested.From the Juju log of a failing run:
The test used
time.sleep(2)between the two actions to establish ordering, which cannot win against a 30 s poll interval. It now waits until unit_a has actually recorded_deferred_restart:start(holds the lock) before unit_b requests, which is what the test means to exercise.Likewise,
test_retry_release_two_units_single_appused a fixedtime.sleep(60 * 3)to bound 12 events whose total duration depends on how the two workers' poll intervals line up. In theassert 10 == 12failure, 10 events had been recorded and the remaining two arrived roughly 30 s after the sleep expired. Both that test andtest_retry_release_alternates_executionin the peer suite now poll for the events they expect.The library only promises that operations run "at most one unit at a time" — it does not promise request ordering across units — so this is the tests over-specifying, not a backend bug.
Spurious fallback to the peer backend
The
assert 'peer' == 'etcd'variant is a library defect._on_update_statusexecutes the in-progress operation, so the worker's own already-queued lock-granted hook can arrive after another hook has already executed and finalised the claimed operation. The same window exists between the worker'srequeue_completed()andclaim_next()on a retry.peek_current()is empty then, andmirror_outcomeclassified that as an etcd/peer inconsistency:The unit then processed the rest of the operation on the peer backend, so the recorded
processing_backendflipped topeer._on_run_with_locknow distinguishes "lock held, work queued, nothing in progress yet" (OPERATION_PENDING, a normal transient) from "etcd has no work at all for this unit" (NO_OPERATION, a genuine divergence). Only the latter falls back. Genuine etcd corruption is still caught by the worker's own consistency check, which dispatches the etcd-failed hook.Refs #593