Fixes #7316: Resynchronize the gateway after failed websocket config updates - #7344
BobSong-dev wants to merge 8 commits into
Conversation
Aias00
left a comment
There was a problem hiding this comment.
The core of this is right and I want it: silently swallowing a failed config update is exactly what leaves a gateway running stale rules until someone restarts it, and close() → health-check reconnect → alreadySync = false → MYSELF is the correct way to force a full resynchronization. I verified that chain in the head version: onOpen:224-227 only sends MYSELF when alreadySync is false, and close():267 resets it, so the reconnect really does re-pull everything.
Requesting changes on one thing: the terminal state at MAX_CONSECUTIVE_SYNC_FAILURES has no recovery path.
handleSyncFailure:404 calls nowClose(), and nowClose():277-287 does two things beyond closing the socket:
this.manuallyClosed.set(true);
if (Objects.nonNull(timerTask)) { timerTask.cancel(); }timerTask is the 10-second wheel-timer task whose doRun is healthCheck() (:196-199), and healthCheck():291-297 is the only thing that ever triggers a reconnect (RECONNECT_EXECUTOR.submit(this::doReconnect)). manuallyClosed is set to true here and never set back to false anywhere in the class.
So after three consecutive failures this client is permanently dead: no reconnect, no ping, no health check — and that includes the case the operator actually wants, which is "the bad config was fixed on the admin side". Nothing will ever pick the fix up; the only remedy is restarting the gateway. On master the same situation logged a warning, dropped one message, and kept the connection — so a later, good update still applied. I am not asking you to go back to that, but the recovery loop has to survive the give-up.
Concretely, one of these would work:
- Keep the timer alive. At the cap, stop closing on failure and go back to log-and-ignore, but leave
healthCheckrunning. The connection stays up, and the next successful reconnect/MYSELF (or the next good message) re-syncs and resetsconsecutiveSyncFailures. This bounds the reconnect storm without giving up. - Keep reconnecting, just stop re-syncing on failure. Set a
resyncDisabledflag consulted byhandleSyncFailure, lethealthCheck/doReconnectcontinue with its existing capped backoff (calculateBackoff, up to 60s). A fixed admin config then recovers on its own. - If you genuinely want a terminal state, it needs to be observable rather than a log line — but I would rather not have one at all here, because the failure it detects is "config the gateway cannot apply", which is precisely the state where you most want it to keep trying.
Note that testPoisonDataGivesUpAfterBoundedFailures asserts nowClose() is called, so whichever option you pick, that test changes with it.
Two other things worth considering while you are in here
- Blast radius of the first failure. The counter is reset by any successful
executorcall (:56), which is good, but the very first failure now drops the connection and forces a full re-pull of every config group. Under a bad push, every gateway in the cluster does that at the same time. That is the intended fix for #7316 so I am not blocking on it — just be sure the log atWARNis enough for operators to correlate a reconnect storm with a bad config, since it is now a normal-ish event rather than an exceptional one. - The outer catch in
onMessagenow covers envelope parsing.JsonUtils.jsonToMap(result)and theRUNNING_MODEhandling are inside the sametry, so a frame that is not parseable JSON — or any other shape the client does not recognise — is now treated as a sync failure and closes the connection, where master ignored it. If admin can emit any non-JSON frame (heartbeat, error text), that is a new disconnect source.
Thanks for the careful review — the terminal state was indeed a dead end, and option 1 is now
Verification: |
Aias00
left a comment
There was a problem hiding this comment.
Two recovery-path issues still block this change. First, a syntactically valid config frame with an unknown eventType reaches AbstractDataHandler, throws from DataEventTypeEnum.acquireByName, and is counted as an apply failure; older gateways will close/resync on newer event types instead of preserving the documented log-and-ignore compatibility behavior. Please classify unsupported event types separately and assert they do not close the socket or consume the failure budget. Second, once MAX_CONSECUTIVE_SYNC_FAILURES is reached, handleSyncFailure keeps the socket open but neither closes nor sends MYSELF; health checks only ping/report instance info, while only MYSELF triggers a full admin snapshot. The skipped config can therefore remain stale indefinitely. Please add a bounded/backoff resync path at the cap and test automatic recovery without a manual restart.
|
Re-check: the recovery-path issue I raised is still open — a syntactically valid config frame with an unknown |
Aias00
left a comment
There was a problem hiding this comment.
Re-reviewed — both recovery-path issues I raised are fixed.
The unknown-eventType case is now handled before it can do damage: handleResult resolves ConfigGroupEnum.acquireByName and DataEventTypeEnum.acquireByName(eventType) up front and, on any RuntimeException, logs and ignores the frame instead of letting the throw propagate into a counted apply failure. Unparseable frames and non-object payloads are handled the same way, and onMessage no longer drops the connection for a protocol-shape mismatch.
On top of that, handleSyncFailure gives real recovery: bounded consecutive-failure counting, a backoff window, and a scheduled full resync — which is the actual point of the PR. ShenyuWebsocketClientTest covers it. CI is green. Approving.
sunnysabor
left a comment
There was a problem hiding this comment.
One recovery-loop case remains unbounded. handleResult resets consecutiveSyncFailures after any successful websocketDataHandler.executor call (line 464). Initial synchronization is delivered as multiple WebsocketSyncFrame payloads and InitialSyncState.accept applies each payload in sequence. If every full snapshot contains a valid payload before the same poison payload, the valid payload resets the counter to zero, then the poison payload increments it to one and closes the connection. The next reconnect replays the same sequence, so the counter never reaches MAX_CONSECUTIVE_SYNC_FAILURES, nextSyncRetryAt is never set, and the 60-second cooldown/full-resync path is bypassed. This can still produce an endless reconnect storm for a deterministic bad item later in each snapshot.
testHealthCheckRetriesFullSyncAfterFailureCap repeats only the poison payload, so it does not exercise this ordering. Please preserve the failure count across individual successful frames within a full snapshot and reset it only when the full snapshot completes successfully (or otherwise track repeated failure of the same item); add a regression test that applies a good payload followed by the same failing payload on each replay and verifies the bounded retry path is reached.
|
I also checked the current merge conflict against |
|
Additional reproduction for the failure-budget concern at this head ( ran 31 tests with 30 passing and the temporary assertion failing: |
Aias00
left a comment
There was a problem hiding this comment.
The sync failure counter is reset after any successful payload. A full snapshot that always contains a valid payload followed by a poison payload therefore resets the budget on every reconnect and can loop indefinitely. Please track failure at the full-sync attempt level, or reset only after the complete snapshot succeeds, and add a valid-plus-poison regression test.
…resync-on-failure # Conflicts: # shenyu-sync-data-center/shenyu-sync-data-websocket/src/main/java/org/apache/shenyu/plugin/sync/data/websocket/client/ShenyuWebsocketClient.java
I will fix it.Thanks for your review. |
Aias00
left a comment
There was a problem hiding this comment.
The updated head still resets consecutiveSyncFailures after any successful payload. A full snapshot that always applies a valid payload before a poison payload resets the budget on every reconnect, so recovery can loop indefinitely. Please reset only after the complete sync cycle succeeds, and add a valid-plus-poison snapshot regression test.
sorry,I missed it.Now it is healthy. |
Aias00
left a comment
There was a problem hiding this comment.
Reviewed the updated head. Failure accounting is now reset only after the complete initial-sync attempt succeeds, so a valid payload followed by a poison payload can no longer reset the retry budget indefinitely. The previous blocker is resolved.
Fixes #7316
Background
Failed WebSocket configuration application can leave gateway caches stale. A full snapshot with a valid payload followed by the same poison payload must not reset the retry budget on every replay and cause an unbounded reconnect loop.
Changes
d89fc0fe0removes the remaining per-payload reset completely, including legacy MYSELF payloads, marked standalone snapshots, and increments. Only the complete framed-sync callback resets the budget. Legacy peers have no end-of-cycle acknowledgement: their budget stays conservative and failures continue to use cooldown/reconnect rather than treating one successful payload as full recovery. Config updates still apply; no manual restart is required to pick up repaired data. A legacy-only success does not clear the accumulated budget for future failures.c0c53df12: both failed because a good payload reset the expected count of 1 to 0. Both pass with this fix. No assertions or retry limits are weakened.7925422b1) without rewriting published history. Resolve the client conflict by retaining master's namespace-scoped snapshot dispatch and this PR's protocol classification/recovery logic. The independent storage e2e infrastructure branch is not included.Verification
./mvnw.cmd -pl shenyu-sync-data-center/shenyu-sync-data-websocket -am test -Dtest=ShenyuWebsocketClientTest,InitialSyncStateTest,InitialSyncConnectionTest,*DataHandlerTest -Dsurefire.failIfNoSpecifiedTests=false -Dmaven.javadoc.skip=true: BUILD SUCCESS; WebSocket 108 tests and plugin-base 7 tests, all zero failures/errors/skips; Checkstyle passed../mvnw.cmd -pl shenyu-sync-data-center/shenyu-sync-data-websocket -am apache-rat:check: BUILD SUCCESS; RAT passed across the selected reactor.git diff --check origin/master...HEADpassed; master is included; working tree is clean.d89fc0fe0is pushed; its checks are not yet verified green. Earlier checks do not validate this new revision.