ENG-525: provider-aware warm pool#1
Merged
clawhavenapp merged 2 commits intoJul 7, 2026
Merged
Conversation
The warm pool was single-dimensioned: one POOL_SIZE, all hosts created with the default provider. With per-request provider selection, a warm host for one provider cannot satisfy a request for another, so pool sizing, maintenance, and claiming become provider-aware. - POOL_SIZES (JSON, provider -> target) replaces the scalar as the primary sizing knob; POOL_SIZE stays as the default provider's alias, overridden by a POOL_SIZES entry for the same provider. Effective targets compose in Settings.get_pool_targets(), dropping zeroes. - maintain_pool() counts, tops up, and sheds per provider. The global POOL_MAX_CREATES_PER_TICK still bounds total creates per tick, with the budget round-robined across providers so one large deficit can't starve the others. Providers dropped from the targets shed to zero. - A pool-eligible POST /hosts (default image, no env) claims only warm hosts whose provider matches the requested one — pinned providers now claim from their own pool instead of always provisioning fresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 tasks
…provider The round-robin batch was built from a sorted provider order, so whenever POOL_MAX_CREATES_PER_TICK was smaller than the number of underfilled providers, every tick spent its budget on the same leading provider(s) — permanently starving the rest if the leader's creates kept failing. There is no cross-tick state to rotate a cursor through, so shuffle the underfilled order each tick instead; the within-tick round-robin is unchanged. Co-Authored-By: Claude Fable 5 <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.
What
Makes the warm-host pool provider-aware. The pool was single-dimensioned (one
POOL_SIZE, all hosts warmed with the default provider), so a warm host for one provider could never satisfy a request pinned to another.POOL_SIZESJSON env ({"exe": 2, "hetzner": 1}) is the primary sizing knob. Effective targets compose inSettings.get_pool_targets():POOL_SIZEseeds the default provider,POOL_SIZESentries override it, zero targets drop out. Values are validated>= 0.POOL_MAX_CREATES_PER_TICKstays a single global blast cap;POOL_HOST_MAX_AGE_HOURSunchanged.hosts.pool): counts warm members per provider in oneGROUP BYquery, then tops up / sheds each provider toward its own target. The per-tick create budget is round-robined across providers so one large deficit can't starve the others. The excess sweep covers the union of configured and present providers, so a provider dropped from the targets sheds to zero instead of idling until its max-age TTL.create_hosttargets the specific provider.get_or_create_host): a pool-eligible request (default image, no env) resolves its provider (provider or DEFAULT_HOST_PROVIDER) and claims only warm hostsWHERE provider == P, gated on P having a warm target. Pinned providers now claim from their own pool instead of always provisioning fresh; a miss falls through to fresh provisioning against P with the same response shape.POOL_SIZE alias decision
Kept
POOL_SIZEworking as an alias for the default provider's target (the ticket's stated default, per no-silent-removals): existing deployments keep their warm pool without a config migration. Precedence is explicit — aPOOL_SIZESentry for the same provider wins, and an explicit{"exe": 0}disables that provider's pool even when the alias would seed it.Acceptance criteria
POST /hostsfor provider P (default image, no env) claims a warm P host and never a host of another providerPOOL_MAX_CREATES_PER_TICKstill bounds total creates across all providers per tickCodex review
Round 1 flagged one P2: the round-robin batch was built from a sorted provider order, so a per-tick cap smaller than the number of underfilled providers always spent its budget on the same leading provider — permanently starving the rest if the leader's creates kept failing. Fixed by shuffling the underfilled-provider order each tick (there is no cross-tick state to rotate a cursor through); the within-tick round-robin is unchanged, and a regression test pins that a permanently failing provider cannot starve the others.
Verification
uv run ruff check,uv run ruff format --check,uv run pyright,uv run pytest(282 passed). New tests cover target composition/validation, per-provider top-up and shed, dropped-provider shed, cross-provider cap with round-robin, provider-scoped claim, the no-cross-provider-claim guarantee, and no-starvation under a tight cap with a failing provider.🤖 Generated with Claude Code