Repository navigation
fix(plugin): cache negative match results only when the candidate set is path-determined (#7977) - #7984
Conversation
Aias00
left a comment
There was a problem hiding this comment.
Review: #7984 fix(plugin): cache negative match results only when the candidate set is path-determined (#7977)
Verdict: approved. The diagnosis is right, the guard is placed at the right two call sites, and the tests are the correct shape — two red cases and two positive controls.
The bug is real and the fix addresses it
MatchDataCache keys negative entries by (pluginName, path) only. defaultMatchSelector / defaultMatchRule wrote those entries on any miss, including a miss caused purely by a missing header, so a single header-less request poisoned the path for the whole TTL. The new gates are:
isPathDeterminedSelectorSet— skips disabled selectors (matching already ignores them), rejectsmatchRestful, and rejectsCUSTOM_FLOWselectors whose conditions are not alluri. Non-CUSTOM_FLOWselectors are correctly not condition-checked, sinceFULL_FLOWignores conditions.isPathDeterminedRuleSet— rejectsmatchRestfuland any non-uricondition.- The rule path additionally requires the whole selector set to be path-determined. That is the part that is easy to miss and it is the part that matters:
ruleshere comes fromBaseDataCache.obtainRuleData(selectorData.getId()), i.e. the rules of one selector, while the cache key carries no selector id — so a negative rule entry written while selector matching is dynamic would also suppress a different selector on the same path.negativeRuleCacheMustRequirePathDeterminedSelectorspins exactly that, and it is the test I would have asked for.
Treating an empty condition list as path-determined is sound in both directions: a CUSTOM_FLOW selector with no conditions never matches (so a negative entry is stable), and empty rule conditions are request-independent. Either way no wrong entry can be produced.
Positive caching is untouched, which is correct — this PR should not absorb #7981.
Verified
defaultMatchRulenow receives the plugin's full selector list. Inexecutethe localselectorsat the call site is the same list used bydefaultMatchSelector, andrulesis the selected selector's rule list, so both predicates see the sets they claim to.MatchDataCache.cleanRuleDataData()exists (MatchDataCache:248), so the added teardown inclearCachecompiles and the singleton caches are actually reset between tests.- The two positive controls (
negativeSelectorCacheStillAppliesToPathOnlyCandidates,negativeRuleCacheStillAppliesToPathOnlyCandidates) assert a cached entry with a null id, so the optimisation is proven to still fire rather than merely not-crash.
Non-blocking
- Cost on the miss path.
isPathDeterminedSelectorSetwalks every selector for the plugin andisPathDeterminedRuleSetevery rule of the selected selector, on each miss that reaches the cache write. That is cheap next to matching, but it runs on the hot path for plugins with many selectors; if this shows up in profiles, memoising the predicate perBaseDataCachegeneration would remove it entirely. - Old entries survive the upgrade. Entries written before this change stay in
MatchDataCacheuntil their TTL expires (60 s by default), so the bug is still observable for one TTL after a rolling restart. Worth one line in the PR description so nobody reports it as "the fix does not work". isUriOnlyConditionListrelies onURI_CONDITION_TYPE.equals(condition.getParamType()). AnullparamTypeis treated as non-uri, which fails safe (no negative cache) — fine, just noting the direction.
Follow-up: the positive cache entry is still unsound (my approval stands, the fix is incomplete)I approved this PR earlier, and the negative-entry gate it adds is correct and well tested. But #7990 (open, same root cause from #7981) demonstrates a case this PR does not cover, so please read it before merging either one.
The test in #7990 ( So the condition this PR applies to the Suggested resolution, so the two PRs do not both land half a fix:
If you would rather land this PR as-is and follow up, that is acceptable for the negative-entry bug, but the positive-entry case should then get its own issue and be fixed before 2.7.2, because "the selector cache pins the first tenant's route for a path" is a correctness bug that is very hard to diagnose in production. |
|
You are right, and #7981 makes the positive case vivid — pushed the combined resolution you sketched:
Full module 306/306, checkstyle clean. With this, the PR is a superset of #7990 (its @stone-98 the positive-entry gate and tests from your PR are incorporated here, thanks for the reproduction. |
Aias00
left a comment
There was a problem hiding this comment.
Approving — this now covers what I asked for, and does it in the right place.
Verified in detail:
- Positive and negative entries are gated for both the selector and the rule cache, so #7981's positive-cache variant is covered here and not only #7977's negative one.
defaultMatchRuletakes the selector set and requires both sets to be path-determined, which is necessary because the rule cache key carries no selector id.Objects.equals(SelectorTypeEnum.CUSTOM_FLOW.getCode(), selector.getType())is safe:getCode()returns a primitiveint, so the comparison unboxes.MatchDataCache.cleanRuleDataData()exists on master, and addingcleanSelectorData()/cleanRuleDataData()toclearCache()is a genuine isolation fix — without it the new rule tests could read an entry left behind by an earlier test.- An empty condition list is correctly treated as cacheable: for a
CUSTOM_FLOWselector the match outcome is request-independent either way, and forFULL_FLOWthe condition list is not consulted at all. - Non-
CUSTOM_FLOWselectors are correctly exempt from the condition check.
Two follow-ups:
-
The
matchRestfulbranch you added has no test. #7990 (the duplicate PR against #7981) has two parameterized tests that fit it exactly —restfulSelectorCandidateShouldPreventResultCachingandrestfulRuleCandidateShouldPreventResultCaching. Please port them so the new branch is pinned. -
#7984 and #7990 are now the same change. Both touch the same four call sites in
AbstractShenyuPluginand are functionally equivalent, so they conflict textually and, worse, one could land on top of the other's gate. I recommend landing this one — it has the stronger positive/negative control coverage — and closing #7990; I am leaving that comment there now.
Nit (shared with #7990): selector.getType() is a boxed Integer, and a row with a null type is treated as non-CUSTOM_FLOW, which skips the condition gate entirely. If type can be null in any persisted data, make the guard null-safe (or treat null as CUSTOM_FLOW); otherwise a one-line comment stating that type is never null would be enough.
CI is currently all queued on this branch, so nothing to judge yet.
|
Follow-up: #7990 is now closed as the duplicate, so this PR is the only place where these two test cases can land. Please port them before this is merged:
Everything else on this PR is approved; this is the last piece. CI on |
|
Both are in place.
The follow-up commit is 2 files, Understood on |
Aias00
left a comment
There was a problem hiding this comment.
Re-approving: the two tests requested in #7990 are now ported and the gate is complete.
Verified:
isPathDeterminedSelectorSetskips disabled selectors, rejectsmatchRestfuland non-URI conditions onCUSTOM_FLOWselectors.Objects.equals(int, Integer)boxes the left side, so the comparison is safe.- The rule cache key still carries no selector id, which is why gating on both the selector set and the rule set is required; that reasoning is correct and documented in the comment.
verify(..., never()).getConditionList()on the second pass proves the cache really short-circuits matching, and the two@ParameterizedTestcases pin the RESTful gating for both hit and miss.- Empty condition list is treated as path-determined, which is right: such a selector matches every path, so a per-path entry stays consistent.
Note: the gate now walks the whole candidate set on every cache write (only on a miss), which is acceptable but worth keeping in mind if selector counts grow.
|
hi @konlue please resolve the conflicts, thanks |
|
/hold |
|
@konlue Heads-up: this PR is approved and every check is green, so the only thing blocking the merge is the merge conflict GitHub reports ( Please rebase onto current |
43e225f to
a82632b
Compare
|
Rebased onto the latest
@yuluo-yx the conflicts are resolved — could you remove the |
Aias00
left a comment
There was a problem hiding this comment.
Requesting changes. The intent - do not cache positive matches whose candidate set is not path-determined - is correct, but isUriOnlyConditionList() only checks paramType == uri and does not inspect the operator. URI time predicates such as TimeAfter/TimeBefore have paramType == uri yet their match result depends on the current time, so this PR would still cache a positive result for a selector/rule carrying a time predicate and shadow the competing candidate as time advances. #8106 already implements the correct fix (a PATH_ONLY_OPERATORS whitelist that excludes TimeAfter/TimeBefore and header/query conditions) with comprehensive tests. Please either adopt that operator-allowlist approach here, or close this PR in favor of #8106. Happy to help align the implementation.
a82632b to
ed8ace7
Compare
|
#8106 landed on
Verified: ./mvnw -pl shenyu-plugin/shenyu-plugin-base -DskipTests=false test # 334/334
./mvnw -pl shenyu-plugin/shenyu-plugin-base checkstyle:check # 0 violations |
Aias00
left a comment
There was a problem hiding this comment.
Re-reviewed after my earlier operator-eligibility change request. The current negative-cache logic uses the path-only operator allowlist, checks all enabled selector candidates, and additionally checks enabled rule candidates before caching a rule miss. TimeAfter/TimeBefore and dynamic header/query conditions now have regressions; path-only misses retain caching. No remaining blocking finding in this patch.
NegativeMatchCacheEligibilityTest and AbstractShenyuPluginTest passed all 30 tests, zero failures/errors/skips. Validation baseline: PR head plus a clean, uncommitted merge of master 52339b2 in the isolated worktree. No source was manually patched or pushed.
Used isolated worktrees and the Maven wrapper with JDK 21/project compiler settings. Checkstyle passed for the targeted Maven runs; RAT and JaCoCo were skipped. No full reactor/e2e pass is claimed.
Problem
AbstractShenyuPluginwrites a negative (empty-id) selector or rule entry under(pluginName, path)whenever nothing matches. The blank-id branch incacheSelectorData/cacheRuleDatareturns before the all-urieligibility check, so a request that only missed because a header was absent poisons the path for the whole TTL (60 s by default): the next request carrying that header is short-circuited anddoExecuteis never called.defaultMatchRule/cacheRuleDatahave the same pattern.In-process reproduction (real
BaseDataCache/MatchDataCache,MockServerWebExchange,execute(...).block()):header X-Tenant = a;GET /xwithout the header misses and caches an empty selector;GET /xwithX-Tenant: astill does not match;The same two-request sequence repros for a rule miss.
Change
isUriOnlyConditionListand candidate-set predicates. A cache entry is written only when every enabled candidate is determined by the path: no RESTful matching, and (forCUSTOM_FLOWselectors, and for rules) every condition isuri.CUSTOM_FLOWselector with no conditions never matches, and empty rule conditions are request-independent. Disabled entries are skipped because matching already ignores them.Objects.equalstype check is null-safe, and a nulltypecannot reach it:filterSelectoralready unboxesselector.getType()for every enabled candidate while matching.MatchDataCachekeys rule entries by(pluginName, path)with no selector id, so a negative rule entry written for selector A would otherwise also suppress selector B on the same path. Positive cross-selector rule reuse remains [BUG] Rule match cache reuses a rule from another selector for the same request path #7976 and is not changed here.Relationship to #7681
#7681 (
fix(plugin): isolate match cache across config updates) proposes a broader generation-scoped match-cache redesign that also gates caching on "every enabled candidate is URI-only", and scopes rule cache keys by the selected selector. That PR is blocked on unrelated sync-layerclean*cascades (Aias00 CHANGES_REQUESTED, e2e red), not on the eligibility gate, which the review explicitly asks to keep.This PR is the minimal, independently testable fix for #7977. If #7681 lands first, this change can be dropped; if this lands first, #7681 only needs its eligibility hunks reconciled.
Tests
Nine new cases in
AbstractShenyuPluginTest:doExecute.positiveSelectorCacheMustNotShadowDynamicMatch: warming withX-Tenant: bcaches no positive selector entry, and the nextX-Tenant: arequest executes the tenant selector.positiveRuleCacheMustNotShadowDynamicMatch: the same sequence on the rule side.positiveSelectorCacheStillAppliesToPathOnlyCandidatesandpositiveRuleCacheStillAppliesToPathOnlyCandidates: path-only candidate sets still cache positive entries, and a second identical request is served from the cache (getConditionList/getConditionDataListis never called again on the candidate).restfulSelectorCandidateShouldPreventResultCaching(parameterized overmatched): a RESTful selector candidate keeps any selector entry out of the cache, for both a matching and a non-matching path.restfulRuleCandidateShouldPreventResultCaching(parameterized overmatched): the same on the rule side.Red on
master: the three negative-cache assertions fail with an emptySelectorData/RuleDataalready present. The two positive-cache shadow cases fail on the previous head with the pinnedgenericentry, exactly as #7990 reports. The two RESTful parameterized cases (four executions) fail with the candidate entry pinned once thematchRestfulguard is removed. The cache-reuse assertions are live too: clearing the cache between the two requests makes both path-only controls fail withNeverWantedButInvoked. Green after the change.Verification
Fixes #7977