Look for a 405 neighbour only on paths the method has no route on - #2920
Open
ericproulx wants to merge 1 commit into
Open
Look for a 405 neighbour only on paths the method has no route on#2920ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
ericproulx
force-pushed
the
perf/skip-covered-neighbours
branch
from
September 10, 2026 13:26
ee0f729 to
5ccbd0a
Compare
Danger ReportNo issues found. |
When the routes for a request's method all miss its path, `#neighbours` walks `@union` -- one greedy route per path -- for a route to answer 405 with. A 404 therefore walks two unions: its method's, then every path. Most of that second walk cannot succeed. `collect_route_config_per_pattern` groups routes by `pattern_regexp` and hands each group's greedy route the pattern those routes share, so once the union for a method has missed a path, the greedy route of every path that method has a route on has missed it too. `compile!` now works out, per method, the greedy routes of the paths it has no route on and compiles those into a union of their own; after a miss, only that one is walked. In an API where every path answers GET, a GET leaves nothing to walk. The subset is built from the same members as `@union`, in the same order, so a path resolves to the same greedy route either way and the 405 and its Allow header are unchanged. A method with no routes of its own covers no path and still walks all of `@union` -- which is also what auto-OPTIONS does, unless OPTIONS routes are declared. Methods that leave the same paths uncovered, PUT and DELETE on a member say, share one union. And once a route has matched and cascaded the neighbour is never read, so it is no longer looked up. A subset of `@union` numbers its groups differently from `@union` itself, so each neighbour union carries the group of each of its routes rather than reading `regexp_capture_group`. Measured on Ruby 4.0.6 without YJIT, median of 5 interleaved rounds against an extract of master, on a CRUD-shaped API: GET and POST on each collection, GET, PUT and DELETE on each member, varied resource names, prefix + path version + `format :json`. At 10 / 100 / 250 resources: GET 404 +74.6% +97.9% +93.5% POST 404 +24.3% +40.4% +31.3% POST 405 +5.8% +27.7% +21.3% POST gains less because it has no route on the member paths, which therefore stay candidates. A route hit from the middle of the union and OPTIONS, both unchanged paths, stay within +/-5%. Boot, full API instance build: +5.1%, +8.5%, +5.8%, and +0.9% at 500 resources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/skip-covered-neighbours
branch
from
September 10, 2026 13:47
5ccbd0a to
2bbb1bd
Compare
ericproulx
marked this pull request as ready for review
September 10, 2026 19:12
5 tasks
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.
When the routes for a request's method all miss its path,
#neighbourswalks@union— one greedy route per path — for a route to answer 405 with. A 404 therefore walks two unions: its method's, then every path.Most of that second walk cannot succeed.
collect_route_config_per_patterngroups routes bypattern_regexpand hands each group's greedy route the pattern those routes share. So once the union for a method has missed a path, the greedy route of every path that method has a route on has missed it too.compile!now works out, per method, the greedy routes of the paths it has no route on, and compiles those into a union of their own; after a miss, only that one is walked. In an API where every path answers GET, a GET leaves nothing to walk.Behaviour is unchanged
@union, in the same order, so a path resolves to the same greedy route either way — same 405, sameAllowheader.@union. That is also what auto-OPTIONS does, unless OPTIONS routes are declared.A subset of
@unionnumbers its groups differently from@unionitself, so each neighbour union carries the group of each of its routes rather than readingregexp_capture_group.Measured
Ruby 4.0.6 without YJIT, median of 5 interleaved rounds against an extract of master. The API is CRUD-shaped: GET and POST on each collection, GET, PUT and DELETE on each member, prefix + path version +
format :json.POST gains less because it has no route on the member paths, which therefore stay candidates. Boot, full API instance build: +5.1%, +8.5%, +5.8%, and +0.9% at 500 resources.
The resource names are varied on purpose. Routes that share one literal (
resource1,resource2, …) let Onigmo reject a path lacking it by substring search without walking the union at all, which inflates every miss measurement and is not something a real API gets.🤖 Generated with Claude Code