Fix ETCD service watcher discovery - #71
Merged
Merged
Conversation
billy-guo
marked this pull request as draft
June 29, 2026 20:37
billy-guo
force-pushed
the
bguo/fix-etcd-routing-shortlist
branch
from
June 29, 2026 20:38
479dbe7 to
e9282d9
Compare
…tances
The async etcd discovery watch loop consumed continue_watching() one event at
a time via run_in_executor, recreating the generator per event. That reset its
resume index and dedup mux and defeated the generator's documented infinite,
self-healing recovery (timeouts / event_index_cleared / transient errors). On
shortlist pod turnover an empty FullSyncRecursive (key-deleted / index_cleared)
wiped a worker's instance set, which then never repopulated, so select() raised
"No service for smite_shortlist" forever until the worker (not the shortlist)
was restarted.
Drive a single persistent continue_watching() generator instead, in the
_watch_loop of both AsyncServiceWatcher (ROUND_ROBIN / shortlist) and
_AsyncHashRing (SCALAR, same latent bug):
- the generator is created once and pumped each iteration with
run_in_executor(None, lambda: next(it)), never recreated per event, so its
resume index and dedup mux are preserved;
- events are applied on the loop thread, keeping instance/ring mutation
single-threaded and consistent with select() and the DOWN listener's
create_task;
- the generator is reopened only on StopIteration, and on error it is logged
and reopened after exponential backoff (1s -> 30s).
This mirrors the sibling AsyncEtcdSourcesProvider._watch_loop and stays on the
existing run_in_executor model: no per-watcher OS threads, no cross-thread
queue, and stop() remains a deterministic task cancel/await.
No empty-sync guard: continuous consumption lets the set self-repopulate via
incremental upserts, and a guard would break scale-to-zero and could retain
stale dead-pod entries.
Adds test_async_directory.py covering recovery-after-empty-full-sync, stop()
teardown, and the ring recovery path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
billy-guo
force-pushed
the
bguo/fix-etcd-routing-shortlist
branch
from
June 29, 2026 20:59
e9282d9 to
bbce9ee
Compare
billy-guo
force-pushed
the
bguo/fix-etcd-routing-shortlist
branch
from
June 29, 2026 22:47
299ff8e to
8eab176
Compare
billy-guo
marked this pull request as ready for review
June 30, 2026 13:08
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.
Description
_AsyncHashRingandAsyncServiceWatcherto iterate over each event generated fromEtcdWatcher.continue_watching()_blocking_nextfunction which only consumed the first eventOsprey workers are unable to discover new pods through ETCD because we only take the first event from watcherd and then abandon the generator. As a result, we lose track of the ongoing stream of updates, so when new pods declare themselves on ETCD, the workers never receive that update.
Checklist
uv run ruff check .passes (no unused imports or other lint errors)uv tool run fawltydeps --check-unused --pyenv .venvpasses (no unused dependencies)CHANGELOG.mdwith my changes, if applicable