Nobody has started this yet — be first.
Business impact
This is the difference between "one upstream, with additional_upstream_base_urls unused" and actually being able to run more than one instance of an upstream behind a single route -- which is the entire point of adding the field in the first place. A route stuck always hitting its first configured upstream cannot be horizontally scaled by adding more upstream instances to its config; every added upstream is dead weight that never receives traffic.
Problem
RouteConfig.additional_upstream_base_urls exists (default []) so a route can list extra upstreams beyond its primary upstream_base_url, and app/proxy/forwarder.py already calls pick_upstream_base_url(route) to decide where each individual request actually goes. But is a stub -- it always returns , completely ignoring .
Current behavior
A route configured with upstream_base_url plus two entries in additional_upstream_base_urls (three upstreams total) still sends every single request to the primary upstream_base_url -- pick_upstream_base_url(route) never reads the extra list.
Expected behavior
When a route has additional_upstream_base_urls set, requests to that route are distributed round-robin across upstream_base_url plus every URL in additional_upstream_base_urls -- roughly evenly, in a consistent rotation, across many requests. Failover / health-aware routing and per-upstream circuit breakers are explicitly out of scope: RouteConfig.breaker_key still resolves to the single primary upstream_base_url for the whole route regardless of how many upstreams it has.
Steps to reproduce
Why this matters
pick_upstream_base_url(route) is called once per request, freshly, with only the route object -- it does not currently have anywhere to keep state (like "which upstream did the last request for this route go to") between calls. Round robin needs per-route rotation state that survives across requests but stays scoped to one route.
Suggested approach
Think about where per-route rotation state could live so it survives across requests but stays scoped to one route -- look at how BreakerRegistry keeps one Breaker per upstream key across requests, as a pattern for "state associated with a route/upstream, created lazily, kept somewhere on app.state," not because this ticket needs a class shaped exactly like BreakerRegistry, but because it is already solving the closely related problem of where per-key state that must outlive a single request lives in this codebase.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_14_multi_upstream_round_robin.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b feat/multi-upstream-round-robinFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/multi-upstream-round-robinSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inpick_upstream_base_urlroute.upstream_base_urladditional_upstream_base_urlshttp://localhost:8080fast_echo/echo/.../large/{size_mb}slow_upstream/delay/{s}/flaky/always-downroutes.yamlcurl localhost:8080/healthzcurl localhost:8080/readyz/admin/*Authorization: Bearer <GATEWAY_ADMIN_TOKEN>You do not need Docker running at all to work the tickets — the test suite fakes Redis with fakeredis (plus lupa for real Lua-script support), so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_09_breaker_threshold_off_by_one.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 15, clean pass/fail summary
./practice_tickets/run_tickets.sh 06 09 15 # just a few, by number
All 15 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ runs the project's own main suite — as of writing it shows 8 pre-existing failures caused by ticket bugs 02, 04, 06, and 09; fixing those tickets fixes those failures too, not a separate task.
Level 2
Implement a feature
Extend the system within its own patterns.