Fix load balancing on catch-all server broken by default-route dedup - #255
Conversation
umputun
left a comment
There was a problem hiding this comment.
fix looks correct for the two paths it touches, and the tests genuinely fail against the old logic. One gap worth folding in before merge, plus a couple of minor test notes.
MTStatic return still has the same bug (discovery.go:239)
the helper now runs at the early return and the end-of-loop return, but the MTStatic branch does its own return res and skips it. So a concrete proxy + a same-src catch-all static still leak a mixed match. Repro:
// example.com concrete proxy on ^/(.*), * catch-all static (AssetsWebRoot "/") on ^/(.*)
res := svc.Match("example.com", "/foo")
// res.Routes == 2: [proxy http://concrete:8080/foo, static /:/var/web/:norm]
// res.MatchType == MTStaticdownstream that mis-serves the catch-all assets (or 500s) for a request that should proxy to the concrete backend. It's pre-existing, the branch predates this PR, but since the whole point here is consistent dedup at every return point it's worth closing in the same PR. One caveat: a plain dropDefaultsIfConcrete call at 239 isn't enough. After dropping the static route res.MatchType is still MTStatic with only a proxy route left, so MatchType needs recomputing from the filtered set (or just don't append the default static when a concrete already matched).
minor
TestService_MatchConcreteDropsDefaultOnEarlyReturncomment says it covers both*and""keys, but the early return fires in the*bucket before the""mapper is reached. The""route never entersres.Routes, so theServer != ""check isn't exercised. Either fix the comment or add a case that actually hits it.- could add coverage for multiple concretes + defaults, and for all-default LB preserved via the early-return path (test 1 only hits that via end-of-loop).
- optional:
Server == "*" || Server == ""is now in 3 spots (:193, :271, :340), a one-lineisDefaultServerwould centralize it. Not a blocker.
Previously, the dedup added for umputun#192 removed a default-server route whenever more than one route matched, without checking that a concrete server match was actually present. With multiple routes for the same source on the catch-all server (a legit load-balancing setup) one of them was silently dropped, so traffic always went to a single backend and failover could return 502 while a healthy backend existed. It also removed only one default route when several matched alongside a concrete one. The dedup ran only at the end of the match loop, so both the presorted-src early return and the MTStatic branch's own return bypassed it: a concrete proxy plus a same-source catch-all route (proxy or assets) leaked a mixed match, in the static case even flipping the result to MTStatic. After this change the default/concrete dedup runs at every return point of Match. A shared isDefaultServer helper centralises the "*"/"" check, default-server routes are dropped only when at least one concrete server route matched, all of them are dropped in that case, and an all-default match keeps every route so catch-all load balancing works.
c5aa573 to
70e6576
Compare
|
Thanks, good catch on the MTStatic leak ( Minor 1 (misleading comment) — you're right, the early return fires in the Minor 2 (extra coverage) — the table now also covers multiple concretes + defaults, and all-default LB preserved via the early-return path (not just end-of-loop). Minor 3 ( Ran a codex review over the diff before pushing; tests fail against the old logic and pass with the fix, race + lint clean. |
umputun
left a comment
There was a problem hiding this comment.
round 2, all addressed and verified:
- MTStatic return now returns the concrete match instead of appending the catch-all assets. Confirmed with the original repro:
Match("example.com", "/foo")with a concrete proxy +*static returns a single concrete route withMatchType=MTProxy. - new table covers the cases I flagged: the
""-key default via end-of-loop dedup, multiple concretes surviving, all-default LB via the early-return path, and the MTStatic branch. isDefaultServer/hasConcreteRouteclean up the duplicated predicate.
lint clean, race clean, CI green.
lgtm
Previously, the dedup added for #192 removed a default-server route from the match result whenever more than one route matched, without checking that a concrete-server route was actually present. With multiple routes for the same source on the catch-all (
*/ default) server — the documented load-balancing setup — one route was silently dropped, so traffic always went to a single backend and failover could return 502 while a healthy backend still existed. When several default routes matched alongside a concrete one, only the first default route was removed.The dedup also ran only at the end of the match loop, so the presorted-src early return (triggered by a shorter default
srcfollowing the matched one) bypassed it entirely and leaked a catch-all route alongside the concrete match.After this change, the default/concrete dedup is a single helper applied at both the early return and the final return: default-server routes are dropped only when at least one concrete server route matched, all of them are dropped in that case, and a match consisting solely of default-server routes keeps every route so catch-all load balancing and failover work as documented.
Added
TestService_MatchCatchAllLoadBalancing(all catch-all routes kept for an unknown server, catch-all dropped when a concrete server matches) andTestService_MatchConcreteDropsDefaultOnEarlyReturn(covers the early-return path and both*and""default server keys). Both fail against the old logic and pass with the fix.