Repository navigation
fix(plugin): isolate match cache across config updates - #7681
sunnysabor wants to merge 4 commits into
Conversation
| public void cleanPluginData() { | ||
| public synchronized void cleanPluginData() { | ||
| pluginMap.clear(); | ||
| cleanSelectorData(); |
There was a problem hiding this comment.
[blocking] This cascade breaks the per-group refresh protocol of the sync layer and is the direct cause of the e2e failure on this PR. cleanPluginData() is not only a delete-all primitive: CommonPluginDataSubscriber.refreshPluginDataAll() (CommonPluginDataSubscriber.java:186-188) calls it, and the HTTP sync calls that whenever the PLUGIN group changes on admin — PluginDataRefresh.refresh() (shenyu-sync-data-center/shenyu-sync-data-http/.../refresh/PluginDataRefresh.java:70-78) does refreshPluginDataAll() and then re-caches only the plugins. On master cleanPluginData() cleared pluginMap only, so selectors/rules survived; with this line, every plugin enable/disable/update over HTTP sync empties selectorMap and ruleMap, and only plugins are refilled, so the gateway answers -107/-108 to every request until the next SELECTOR/RULE push. Reproduced by this PR's own CI: gateway log 13:28:59 update PLUGIN config (the e2e enabling loggingRocketMQ), no SELECTOR/RULE update afterwards, then 13:29:20 GET /http/order/findById -> {"code": -107, "message": "divide:Can not find selector"}. The websocket-mode batch of the same e2e passed because websocket PluginDataHandler.doUpdate only calls onSubscribe without any clear. Please drop this cascade (keep version++) and leave cross-map cleanup to the per-entity delete paths, which this PR already implements correctly in removePluginDataByPluginName.
There was a problem hiding this comment.
Addressed on the current head (13fb0a1). cleanPluginData() now clears only pluginMap and advances the generation; plugin-owned selector/rule cleanup remains in removePluginDataByPluginName(). The full PR CI, including HTTP-sync E2E, is now green. Please re-review when convenient.
| public void cleanSelectorData() { | ||
| public synchronized void cleanSelectorData() { | ||
| selectorMap.clear(); | ||
| ruleMap.clear(); |
There was a problem hiding this comment.
[blocking] Same protocol issue one level down: cleanSelectorData() is invoked by refreshSelectorDataAll() (CommonPluginDataSubscriber.java:212-216), which SelectorDataRefresh.refresh() (shenyu-sync-data-http/.../refresh/SelectorDataRefresh.java:70-78) calls on any SELECTOR-group change — it then re-caches only the selectors. With ruleMap.clear() here, every selector change over HTTP sync drops all rules until the next RULE push. In a full refresh it is worse: DataRefreshFactory.executor iterates its EnumMap in key ordinal order, and ConfigGroupEnum declares PLUGIN(1), RULE(2), SELECTOR(3) (shenyu-common/.../ConfigGroupEnum.java:33-48), so the sequence is PLUGIN -> RULE -> SELECTOR: the RULE step refills ruleMap, then the SELECTOR step clears it again and only selectors are refilled — ruleMap stays empty until an unrelated RULE change arrives. Please restrict this method to selectorMap (master semantics) and keep the rule cleanup in removeSelectData, which this PR already wires correctly (ruleMap.remove(selectorId)).
There was a problem hiding this comment.
Addressed on the current head (13fb0a1). cleanSelectorData() now clears only selectorMap and advances the generation; associated rules are removed in the per-selector removal path. The full PR CI, including HTTP-sync E2E, is now green. Please re-review when convenient.
Aias00
left a comment
There was a problem hiding this comment.
Request changes. The core idea — generation-scoped match cache, rewritten-path keys, URI-only caching gate, finally-block cache cleanup — is well designed, but two cascades added to the clean* primitives break the sync layer's per-group refresh protocol. The e2e failure on this PR is that regression, not infrastructure flake; I traced it end to end below.
Blocking 1 — cleanPluginData() now clears selectors and rules (caused the e2e failure)
cleanPluginData() is not only a delete-all primitive. The sync layer uses it as the "clear" half of a per-group refresh:
- admin pushes a PLUGIN-group change (e.g. enabling a plugin) -> gateway
PluginDataRefresh.refresh()(shenyu-sync-data-center/shenyu-sync-data-http/src/main/java/org/apache/shenyu/sync/data/http/refresh/PluginDataRefresh.java:70-78) callsrefreshPluginDataAll()and then re-caches only the plugins. refreshPluginDataAll()->cleanPluginData()-> with this PR alsocleanSelectorData()->selectorMap.clear()+ruleMap.clear().- On master,
cleanPluginData()clearedpluginMaponly, so selectors and rules survived a plugin-group refresh. With the cascade, any plugin enable/disable/update over HTTP sync leaves the gateway with zero selectors and zero rules until the next SELECTOR/RULE push — every request answers-107 Can not find selector/-108 Can not find rule.
Hard evidence from this PR's own failing e2e job (e2e-case-logging-rocketmq, run 38904376053, job 116989492155):
13:26:18 websocket-mode batch: "start loggingRocketMQ plugin" -> tests PASS at 13:27:03
13:27:22 compose switches gateway to http sync mode
13:28:04 gateway: update SELECTOR config (x3), update RULE config (x21) <- re-registration
13:28:59 gateway: update PLUGIN config (x1) <- e2e enables loggingRocketMQ
(no SELECTOR/RULE update after this point)
13:29:20 GET /http/order/findById?id=123 -> {"code": -107, "message": "divide:Can not find selector, please check your configuration!"}
-> DividePluginTest.before:80 fails, 2 test cases fail
The websocket batch passed because websocket PluginDataHandler.doUpdate only calls onSubscribe per item — no clear — so only the HTTP-sync path exposes the bug. e2e-case (e2e-http-sync-compose, shenyu-e2e-case-http) would very likely have caught it too, but it was cancelled by the matrix fail-fast after the rocketmq job failed.
Blocking 2 — cleanSelectorData() now clears ruleMap
Same protocol issue one level down. refreshSelectorDataAll() -> cleanSelectorData() runs on any SELECTOR-group change (SelectorDataRefresh.java:70-78), and only selectors are re-cached — so every selector change over HTTP sync drops all rules until the next RULE push. In a full refresh it is worse: DataRefreshFactory.executor iterates its EnumMap in key ordinal order, and ConfigGroupEnum declares PLUGIN, RULE, SELECTOR (shenyu-common/.../ConfigGroupEnum.java:33-48), so the sequence is PLUGIN -> RULE -> SELECTOR: the RULE step refills ruleMap, the SELECTOR step then clears it again and nobody refills it — rules stay empty until an unrelated RULE change arrives.
Suggested fix
Keep the cascades in the per-entity delete paths only — this PR already implements them correctly there:
removePluginDataByPluginNameremoves the plugin, its selectors and its rules (correct for a plugin delete).removeSelectData(pluginName, selectorId)removes the selector and doesruleMap.remove(selectorId)(correct for a selector delete).removeRuleDataalready handles dangling rules.
What needs to change:
cleanPluginData(): drop thecleanSelectorData()call, keeppluginMap.clear()+version++(master semantics).cleanSelectorData(): dropruleMap.clear(), keepselectorMap.clear()+version++(master semantics).
Note the same root cause also affects the websocket full-refresh path: onPluginRefresh -> refreshPluginDataSelf -> cleanPluginDataSelf -> removePluginData per item, which with the cascade also drops each refreshed plugin's selectors and rules. Fixing the two clean* methods addresses that path as well.
What is good (and should be kept)
- The generation-scoped match key (
version+ rewritten path, rule key scoped to the selected selector) is the right shape for the reported issue, and gating caching on "every enabled candidate uses stable URI conditions" plus skippingmatchRestfulis conservative in the right direction. - The per-entity cascades in
removePluginDataByPluginName/removeSelectData/removeRuleData(including the dangling-selector/dangling-rule handling) are correct and are where the cascade belongs. MatchCacheLifecycleTestcovers the interesting lifecycles (priority replacement, dynamic competitors, cross-selector scoping, old-generation refill, handler exceptions). What is missing is coverage of the refresh protocol itself: please add a test that simulatesrefreshPluginDataAll()followed by plugin-only re-cache and asserts thatobtainSelectorData/obtainRuleDatastill return their data (i.e. the exact scenario the e2e caught). A small test aroundDataRefreshFactory/PluginDataRefreshordering would be even better.
CI note
The two red jobs are this PR's regression, not infrastructure: master's e2e is green (runs 38909074537 / 38924255915 both success, including the rocketmq case), and the failure mechanism is reproducible from the code path above. Once the cascades are removed, please rerun the e2e workflow — I would expect the rocketmq case to go green again.
8866f74 to
f2a21c9
Compare
Aias00
left a comment
There was a problem hiding this comment.
The cache-generation approach is sound. Putting BaseDataCache.version into the match key (version:path for selectors, selectorId.length:selectorId + version:path for rules) means an in-flight request can only populate the generation it started on, so a stale match result can never become visible to a newer configuration — that is the correct invariant, and it is better than trying to invalidate the trie/LRU on every change.
Also correct:
- Caching is now restricted to
!matchRestful && onlyUriConditions(...), which is the same safety rule as #8106: only a key that is fully determined by the URI path may be cached. Restful and header/query/time-predicate rules are recomputed every request. getRawPathmoving toRequestUrlUtils.getRewrittenRawPathkeeps the cache key consistent with the rewritten path actually used for matching.removePluginDataByPluginNamenow cascades into selectors and rules, andremoveRuleDatahandles a blankselectorIdby sweeping all buckets — both plug real leak paths.
One thing to confirm before merge: version-keyed entries and cache growth. Every configuration change mints a new key prefix, and the old-generation entries become unreachable. If the L1 LRU is bounded that is fine (they age out), but if the L2 trie retains them indefinitely this becomes a slow memory leak proportional to config-change rate. Please confirm the L2 store is bounded or is cleared on version bump — otherwise the fix trades a correctness bug for an unbounded-growth bug.
Minor: removePluginData calls removePluginDataByPluginName (which already increments version) and then increments again; cleanPluginDataSelf no longer cascades into selectors/rules. Both are harmless given version only needs to be monotonic, but the double increment is worth a comment so it does not look accidental.
LGTM once the cache-growth question is answered.
f2a21c9 to
b19f360
Compare
|
Rebased on the latest upstream/master (92d16c2) and reran the focused plugin-base reactor. The cache-growth concern is addressed by the current implementation: MatchDataCache has one per-plugin selector map and one per-plugin rule map, each backed directly by WindowTinyLFUMap; there is no L2 trie. AbstractShenyuPlugin passes the configured maximumSize and TTL to each cache, and WindowTinyLFUMap applies Caffeine maximumSize(maximumSize) plus expireAfterWrite. Old generation keys therefore remain bounded by both limits and expire. The targeted reactor passed: MatchCacheLifecycleTest, BaseDataCacheTest, and CommonPluginDataSubscriberTest (116 tests total, 0 failures/errors), with Checkstyle passing. The rebased branch preserves the refresh-protocol fix and contains no Markdown files. |
|
The full PR workflow exposed four cache-test assertions that still queried the old unversioned Validation passed locally: focused positive/negative eligibility tests (27 tests) and the full 11-module plugin-base reactor (386 plugin-base tests, no failures; Checkstyle clean). Pushed as |
Aias00
left a comment
There was a problem hiding this comment.
Stream Agent analysis
PR #7681 attempts to isolate match cache across config updates by scoping rule matches to selected selector and BaseDataCache generation. Bounded excerpts and e2e checks show success but full review pending.
Evidence
- PR head SHA 13fb0a1; e2e checks success
- Comment: rebased on 92d16c2; MatchDataCache uses per-plugin maps
- Resource excerpt shenyu-source @13fb0a1b truncated AbstractShenyuPlugin
Recommended next steps
- Review full patch for AbstractShenyuPlugin and MatchDataCache at head 13fb0a1
- Confirm unit tests added for per-selector cache isolation
- Validate with maintainers that finally-block cache clears are correct
Scope: bounded repository evidence; this is not a full code review or a claim that tests were run.
Task: c7b27e72-1cc2-4e7c-8dc7-74ca9b248f42 · Commit: 13fb0a1b5e8d83e9f5713967c7d5ca2d7ec6c54c
Aias00
left a comment
There was a problem hiding this comment.
Stream Agent analysis
PR #7681 (head 13fb0a1) isolates match cache across config updates with version-scoped keys and selector-scoped negative caching. All partition reviews PASS, complete diff reviewed, CI green (68 checks). No HIGH/CRITICAL defects.
Evidence
- Frozen review: all_parts_passed true, complete_diff true, CI passed 68 checks.
- Two synthesis nodes reduce-1-0 and reduce-1-1 both PASS with no blocking findings.
Recommended next steps
- Merge after standard approvals; monitor cache behavior in staging.
Scope: captured diff and GitHub check metadata only; no local tests were executed.
Task: eed0addd-80d2-4a92-8534-b2208a3c87e6 · Commit: 13fb0a1b5e8d83e9f5713967c7d5ca2d7ec6c54c
Review verdict: PASS
FINAL synthesis: all child partitions PASS, complete_diff true, CI green (68 checks). Cross-file cache isolation changes consistent, thread-safe, with regression tests. No HIGH/CRITICAL defects.
Publication: APPROVE (EXPLICIT_PASS)
13fb0a1 to
a05e6f2
Compare
Follow-up to #7651.
The match cache can retain valid entries across configuration updates even when the selector/rule that produced them is no longer the winner. This change scopes rule matches to the selected selector and a BaseDataCache generation, so an in-flight request cannot repopulate a cache entry used after a later update. It only caches results when every enabled candidate uses stable URI conditions, uses the rewritten path as the key, and clears match entries in
finallyblocks if a plugin data handler throws. BaseDataCache serializes batch snapshot publication with incremental writes/deletes, cascades selector/plugin removal to child rules, and removes IDs that move between groups.The generation is global to BaseDataCache: each config mutation makes earlier derived entries unreachable, and their bounded TTL reclaims them later. Config writes are infrequent, so they are serialized on the cache writer monitor; request reads remain unsynchronized. Dynamic header/query conditions skip match-result caching. The existing configurable TTL from #7664 remains the upper bound for unused old generations.
Validation:
shenyu-plugin-base -amreactor, including deterministic refresh/delete/update interleavings and regressions for selector/rule priority, dynamic request conditions, selector scoping, rewritten paths, cascaded deletion, and handler exceptions.git diff --checkpasses. A full repository clean install was not run.