Repository navigation
fix(discovery): remove stale gateway cache on upstream deletion - #7289
Conversation
Aias00
left a comment
There was a problem hiding this comment.
Thanks for taking on #6479 — stale discovery upstreams surviving a selector deletion is one of those bugs that is invisible in tests and painful in production, and this PR wires the whole chain end to end rather than patching one hop:
- admin side resolves (and now requires) the plugin name before publishing a removal (
AbstractPathDataChangedListener,SelectorServiceImpl), which is the root cause of the malformed.../discoveryUpstream//<selectorId>paths; - gateway side finally acts on a removal, per protocol: divide/websocket drop their
UpstreamCacheManagerentry, gRPC invalidatesApplicationConfigCache/GrpcClientCache, and TCP gets the selector-id → selector-name mapping it needed (UpstreamProvider#registerSelector/getSelectorName) because the upstream cache there is keyed by name while the sync key carries the id; - the three sync channels are covered differently and correctly for each: ZK now uses
oldDataforNODE_DELETEDwhen the payload is gone, the websocket handler mapsdoDeletetoDiscoveryUpstreamKey, and the HTTP refresh diffs the previous snapshot so a selector that disappears from the config (or whose name changed) is properly removed.
The DiscoveryUpstreamKey record is the right abstraction — it captures exactly what a removal needs (plugin + id + optional name) instead of smuggling a partially-filled DiscoverySyncData around, and it is why CommonDiscoveryUpstreamDataSubscriber#unSubscribe can finally be implemented instead of being an //ignore.
Requesting changes for two things
1. The branch has conflicts and zero CI evidence. GitHub reports no workflow runs on fix/6479-discovery-upstream-delete-cache and the merge state is dirty. This patch spans seven modules (admin listener api, admin, plugin-base, divide, grpc, tcp, websocket, protocol-tcp, sync-data-api, http, websocket, zookeeper) and changes delete semantics in production paths, so please rebase onto current master and get a green pr_build before this is merged — I cannot approve it on reading alone.
2. SelectorServiceImpl#unbindDiscovery now throws IllegalStateException (SelectorServiceImpl.java:332). The intent — "do not publish a removal we cannot address" — is right, and validating the whole batch before touching any row (the two-phase ResolvedDiscovery structure) is a nice touch. But the consequence is that a batch delete that contains one selector with unresolvable plugin metadata now fails and rolls back entirely, and the admin REST layer sees a raw IllegalStateException rather than a typed error. Given that this can happen from stale/orphan discovery rows, users could end up unable to delete selectors at all. Options:
- degrade gracefully: skip the discovery unbind for that selector, log at ERROR, and continue deleting the rest of the batch (the row-level cleanup then still happens for the healthy ones), or
- keep failing, but throw a typed admin exception (the ones already mapped to a clean error response) instead of
IllegalStateException.
Either is fine for me; please pick one and make the behaviour visible in the message shown to operators.
Worth noting, not blocking
- Public API change.
DiscoveryUpstreamDataSubscriber#unSubscribe(DiscoverySyncData)becameunSubscribe(DiscoveryUpstreamKey). I checked every occurrence in the repo:CommonDiscoveryUpstreamDataSubscriberis the only implementation and every other call site goes through the sync services you already updated, so the build is fine — but this is source-incompatible for anyone implementing the interface downstream, so it belongs in the release notes. - Coverage of the other sync channels. Discovery-upstream deletion is now propagated only over zookeeper, websocket and http. Nacos, etcd, consul, polaris and apollo still have no delete path for this group (pre-existing). A short note in the PR description or a follow-up issue would help whoever hits it next.
- The blank-
pluginNameguard inAbstractPathDataChangedListeneris the right safety net;selectorIdhas the same failure shape (buildDiscoveryUpstreamPathwould produce.../divide/null). No logging detail lost if you extend the same check to it. - In
DiscoveryUpstreamDataRefresh#refresh, an empty snapshot now triggerssubscriber.refresh()and anunSubscribeper previously known key. Correct, just slightly redundant work; also notepreviousSnapshotbeing a plainHashMapis fine only because you maderefreshsynchronized— worth a comment so nobody removes the modifier later. TcpUpstreamDataHandlerregistering the selector id → name mapping insidehandlerDiscoveryUpstreamDatais the piece that makesremoveDiscoveryUpstreamDatawork after a gateway restart with no prior selector event. Good; please keep the invariant documented onUpstreamProvider#registerSelector.
Once the branch is rebased with green CI and the IllegalStateException handling is settled, I am happy to take another look.
a11d8be to
81918c8
Compare
81918c8 to
a2ebb80
Compare
Addressed each point:
|
Aias00
left a comment
There was a problem hiding this comment.
Thanks for the follow-up — both blocking points are resolved, so flipping to approve.
What I re-checked
1. Branch state / CI. mergeable: MERGEABLE against master, and the last commit 4bdda53 is a merge of master rather than a stale branch, so the "dirty, zero runs" situation I objected to is gone. Workflows are running on that sha (ci, it, it-k8s, e2e, k8s-examples-http, CodeQL) with no failures so far.
2. SelectorServiceImpl#unbindDiscovery. Now throws ShenyuAdminException instead of IllegalStateException, and I confirmed this is actually the "typed exception mapped to a clean response" option rather than just a rename: ShenyuAdminException extends ShenyuException, and ExceptionHandlers:124-132 has @ExceptionHandler(ShenyuException.class) returning ShenyuAdminResult.error(message) — so the operator sees Cannot delete selector batch: plugin name for selector <id> ... No selectors in this batch were deleted; restore the plugin name and retry instead of a 500 with a stack trace. That is exactly the visibility I asked for. Keeping the batch atomic is fine now that the failure is explicit and actionable, and testDeleteValidatesWholeBatchBeforeUnbindingDiscovery proves no rows move for the healthy selectors either.
Two observations worth recording, not requests:
- The resolution chain is wider than before, in a good way.
pluginMap->pluginMapper.selectById->discoveryDO.getPluginName()means the case that motivated my concern (orphaneddiscoveryrows whose plugin row is gone) mostly resolves instead of failing; the exception is now a last resort rather than a common path. - The
pluginDOSmutation closes a latent NPE.SelectorEventPublisher:98-99doespluginMap.get(...).equals(...)unguarded, so a selector whose plugin id was absent from the batch list used to NPE on delete. Injecting the syntheticPluginDO(id, name)at:338-343fixes that too. This is also whypluginMapper.selectByIds(...)is now copied into a mutableArrayListat:302— worth keeping in mind for whoever later tries to "simplify" that copy.
3. Non-blocking items. The blank-selectorId guard (AbstractPathDataChangedListener:101) is in, with namespaceId added to the warning, and the 5 focused tests cover DELETE and UPDATE for each blank field. previousSnapshot now carries the // Only the synchronized refresh method accesses this shared mutable snapshot comment. UpstreamProvider#registerSelector keeps the id -> name mapping; its javadoc says what it does but not why the invariant matters (deletion after a restart with no prior selector event) — one more line there would help the next reader. Release notes carry the unSubscribe signature change.
Behavioural notes worth having in the record
DiscoveryUpstreamDataRefresh#refreshis nowsynchronizedand diffspreviousSnapshotagainst the incoming snapshot, so a selector that disappears or is renamed emitsunSubscribewith the previous key. The rename case matters because TCP keys its upstream cache by selector name — seeTcpUpstreamDataHandler#removeDiscoveryUpstreamData, which prefers the locally registeredgetSelectorName(selectorId)and falls back tokey.selectorName().TcpBootstrapFactorynow drops upstream state in three more places (thecreateBootstrapServerIfAbsentfailure path,removeAndShutdown,clearCache), which keepsUpstreamProviderfrom outliving itsBootstrapServer.AbstractPathDataSyncService#discoveryUpstreamHandlerEventhandlesDELETEexplicitly by rebuilding the key from the path segments ([length-2]plugin,[length-1]selector) instead of waiting for a payload that is already gone — that is the piece that makes the path-based channels work on delete.CommonDiscoveryUpstreamDataSubscriber#onSubscribe/unSubscribenow guard null plugin names, so a malformed event degrades to a no-op instead of ahandlerMap.get(null)lookup.
Coverage stays as before: zookeeper, websocket and http have explicit delete paths, the rest propagate through the shared handlers. Nothing here blocks merge.
|
The RocketMQ logging e2e failed because Could a maintainer rerun the failed e2e workflow? I won't push an unrelated change just to retrigger CI. If it fails again, I'll investigate the reproducible failure before requesting a merge. |
|
Thanks for working on this issue. After comparing this PR with #7172, I do not think the new PR #7172 establishes discovery upstream synchronization as snapshot reconciliation: This means a registry DELETED event is an internal Admin-side event. It should not be propagated to the gateway as an For example: Previous upstreams: [A, B] The gateway removes A by comparing [A, B] with [B]. Deleting the last instance should still publish an empty UPDATE snapshot: Previous upstreams: [A] The gateway then clears the selector's upstream cache through the same reconciliation path. Therefore, the following chain introduced by this PR represents the wrong abstraction for upstream instance deletion: DISCOVER_UPSTREAM DELETE There are two different concepts that should not be mixed:
The actual remaining issue is full-snapshot reconciliation, especially for HTTP sync: Previous selectors: [S1, S2] SelectorDataRefresh does not invoke removeSelector(S1), and DiscoveryUpstreamDataRefresh does not detect that S1 That problem should be fixed by reconciling the previous and current snapshots, rather than introducing a new I suggest:
In summary, the stale-cache problem is valid, but the DELETE chain introduced by this PR conflicts with the snapshot- |
I agree with the instance-level model in #7172: registry The The proposed HTTP I’ve clarified these separate flows in the PR description. The
PR #7172 resolves instance lifecycle within a selector, while PR #7289 resolves selector lifecycle within discovery sync.
So I agree with your requirement that |
|
|
pls resolve the conflicts. Thanks @lymerin |
I've resolved the merge conflicts and all tests are now passing. |
Aias00
left a comment
There was a problem hiding this comment.
Re-reviewed after the new commits. This closes a real gap: discovery-bound upstreams could stay cached in the gateway after the selector was deleted. Routing DISCOVER_UPSTREAM DELETE through path-based sync, handling ZooKeeper node deletions, reconciling HTTP discovery snapshots so removed selectors are unsubscribed, and adding removeDiscoveryUpstreamData(DiscoveryUpstreamKey) per plugin handler (divide / grpc / websocket / tcp) is a coherent fix, and the defensive checks for blank pluginName / selectorId are welcome. Test coverage across every touched module is solid.
One thing to keep visible: this changes the public SPI DiscoveryUpstreamDataSubscriber#unSubscribe(DiscoverySyncData) to unSubscribe(DiscoveryUpstreamKey), which breaks third-party sync-data implementations. You documented it under "API Changes" in RELEASE-NOTES — approving on the basis that this was a deliberate, documented decision for this release.
Fixes #6479
What does this PR do?
This PR fixes stale discovery upstream data remaining in the gateway after a discovery-bound selector is deleted.
The main changes are:
DISCOVER_UPSTREAM DELETEevent through path-based sync;If Admin cannot resolve the plugin name for a discovery-bound selector, it rejects the entire selector-delete batch with
ShenyuAdminExceptionbefore deleting rows or publishing removal events. The error identifies the affected selector and states that no selectors in the batch were deleted.The existing selector-level
DISCOVER_UPSTREAM DELETEevent also propagates through Nacos, etcd, Consul, Polaris, and Apollo. Etcd and Consul use the shared path-based handler; Nacos, Polaris, and Apollo use the shared node-based handler.Event semantics and relationship to #7172
Upstream instance changes and selector removal are separate flows:
ADDED,UPDATED, andDELETEDevents are published asDISCOVER_UPSTREAM UPDATE. [#7172]( fix: reconcile upstream snapshots without retaining removed instances #7172) proposes publishing the complete remaining upstream list, including an empty list after the last instance is removed. This PR does not change that producer or turn an individual instance deletion intoDISCOVER_UPSTREAM DELETE.SelectorServiceImpl#unbindDiscoveryalready callsDiscoveryProcessor#removeSelectorUpstream, which publishesDISCOVER_UPSTREAM DELETEfor that selector’s discovery-upstream record. This PR fixes the gateway’s handling of that existing event. It does not introduce a new DELETE producer or replaceSELECTOR DELETEorPROXY_SELECTOR DELETE.For HTTP full snapshots,
DiscoveryUpstreamDataRefreshcompares the previous and current selector identities and unsubscribes records that disappear or are renamed. An existing selector with an empty upstream list remains in the Admin snapshot and is processed as an update, not mistaken for a removed selector.The
DiscoveryUpstreamDataSubscriber#unSubscribe(DiscoveryUpstreamKey)API change is documented inRELEASE-NOTES.md.Why is this needed?
Before this change, the existing discovery upstream deletion event was ignored by path-based gateway sync or reached a no-op unsubscribe handler. Deleted selectors could therefore leave gateway-side upstream state behind and continue to affect routing.
HTTP full-snapshot synchronization also needed to detect discovery-upstream selector records that disappeared from the latest snapshot.
Tests
Added or updated tests for:
Make sure that:
./mvnw clean install -Dmaven.javadoc.skip=true.