fix(startup): bound + parallelize check_and_cleanup_servers (#0043) — 0.21.134 - #1050
Merged
Conversation
….21.134 Sibling of #15. #15 moved the orphaned-CLIENT reap off the readiness path and made it concurrent, but left the SERVER-check phase (RedisStore.check_and_cleanup_servers, awaited inline in init()) with the same O(N x timeout) shape that CrashLooped the pod: * list_servers() scans services:*|*:public/*:built-in@* — the built-in of EVERY public-workspace client, so after a crash/rollout a non-reset (prod) Redis carries a pile of DEAD built-in registrations. * The old loop pinged each SEQUENTIALLY; a dead client never answers, so get_remote_service(..., {"timeout": 2}) burned the full timeout per server, and svc.ping("ping") itself had NO explicit timeout — a half-open peer that resolves but never replies could hang boot forever. Fix (readiness path stays bounded): * Probe servers CONCURRENTLY, concurrency-capped (HYPHA_SERVER_CHECK_CONCURRENCY, default 50) → one pass ~ceil(N/cap) x timeout, not N x timeout. * Bound EACH probe: both get_remote_service resolution AND the ping round-trip get an explicit timeout (HYPHA_SERVER_CHECK_TIMEOUT, default 2). * Bound the WHOLE phase with an overall deadline (HYPHA_SERVER_CHECK_DEADLINE, default 60); on deadline, log + continue startup (the background reaper trims the rest) rather than block readiness. * Preserve the duplicate-self-id guard (a LIVE server already holding this server's id still raises RuntimeError) and the exact dead-server cleanup key patterns (server-owned services only; user services are untouched, as before). Also bound the ping in _cleanup_orphaned_client_services._probe with asyncio.wait_for (item 4): resolution was bounded but the ping was not. Tests (real, docker-free, fakeredis; reproduce-before-fix): seed genuine dead built-in registrations in a non-reset Redis (prod-like — a reset boot flushes the pile and can never reproduce it) and assert boot is prompt and the dead registrations are cleaned. Pre-fix: init took 24.3s for 12 dead servers (12 x ~2s serial); post-fix ~1s. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Summary
Sibling of #15. #15 moved the orphaned-client reap off the readiness path and made it concurrent, but left the server-check phase —
RedisStore.check_and_cleanup_servers, awaited inline ininit()— with the sameO(N × timeout)shape that CrashLooped the pod on a large post-crash Redis pile.Root cause
list_servers()scansservices:*|*:public/*:built-in@*— the built-in service of every public-workspace client, not just live hypha instances. After a crash/rollout a non-reset (prod,HYPHA_RESET_REDIS=false) Redis carries a pile of dead built-in registrations.get_remote_service(..., {"timeout": 2})burned the full timeout per server, andsvc.ping("ping")itself had no explicit timeout — a half-open peer that resolves but never replies could hang boot forever.So N dead registrations added
O(N × 2s)(or unbounded) to the readiness path — exactly the #15 failure mode on a different phase.Fix (readiness path stays bounded)
HYPHA_SERVER_CHECK_CONCURRENCY, default 50) → one pass is~ceil(N/cap) × timeout, notN × timeout.get_remote_serviceresolution and the ping round-trip get an explicit timeout (HYPHA_SERVER_CHECK_TIMEOUT, default 2).HYPHA_SERVER_CHECK_DEADLINE, default 60); on deadline, log + continue startup (the background reaper trims the rest) rather than block readiness.RuntimeErrorif a live server already holds this server's id; dead-server cleanup key patterns are byte-for-byte unchanged (server-owned services only — user services untouched).Also bounds the ping in
_cleanup_orphaned_client_services._probewithasyncio.wait_for(item 4): resolution was bounded there but the ping itself was not.Tests
tests/test_server_check_bounded.py(real, docker-free, fakeredis; reproduce-before-fix): seed genuine dead built-in registrations in a non-reset Redis (prod-like — a reset boot flushes the pile and can never reproduce it), boot a realRedisStore, and assert boot is prompt and the dead registrations are cleaned.init()took 24.3s for 12 dead servers (12 × ~2s serial).HYPHA_SERVER_CHECK_TIMEOUT).Regression:
test_orphan_reaper.py,test_login_registration_stale_marker.py,test_server_disconnection.py(13, incl. the two direct callers ofcheck_and_cleanup_servers) all green.🤖 Generated with Claude Code