Skip to content

fix(pd): keep KvClient watches alive after reconnect failures - #3157

Open
contrueCT wants to merge 2 commits into
apache:masterfrom
contrueCT:fix/3152-pd-kv-watch-reconnect
Open

fix(pd): keep KvClient watches alive after reconnect failures#3157
contrueCT wants to merge 2 commits into
apache:masterfrom
contrueCT:fix/3152-pd-kv-watch-reconnect

Conversation

@contrueCT

Copy link
Copy Markdown
Contributor

Purpose of the PR

KvClient previously retried a failed watch only once. If that reconnect also failed, the
watch stopped permanently, and unexpected stream completion did not trigger recovery.

The reconnect path also exposed a lifecycle issue: AbstractClient.resetStub() invoked the
virtual close() method, so transport initialization on a KvClient dispatched to
KvClient.close() and marked the whole client closed.

Main Changes

  • Separate AbstractClient transport cleanup from the overridable client lifecycle close.
  • Track each exact-key or prefix watch as an independent subscription with its current observer.
  • Retry onError, Leader_Changed, and unexpected onCompleted with a fixed delay until the
    watch recovers or the client closes.
  • Deduplicate reconnect scheduling for the same observer and ignore stale observer callbacks.
  • Stop the reconnect executor and invalidate active observers when KvClient.close() is called.
  • Add deterministic regression coverage for repeated failures followed by recovery, leader
    changes, completion, deduplication, stale observers, prefix semantics, and close behavior.

This PR does not add event replay, protocol revisions, PD Server changes, or the Server-side
reconciliation tracked by #3151. Events emitted while a watch is disconnected are still not
replayed.

Verifying these changes

  • Trivial rework / code cleanup without any test coverage. (No Need)
  • Already covered by existing tests, such as (please modify tests here).
  • Need tests and can be verified as follows:
    • mvn -q -o test -pl hugegraph-pd/hg-pd-test -am -Dtest=KvClientTest -DfailIfNoTests=false -Drat.skip=true -Djacoco.skip=true (12 tests, 0 failures/errors)
    • mvn -q -o package -pl hugegraph-pd -am -DskipTests -Dmaven.javadoc.skip=true -Drat.skip=true -Djacoco.skip=true
    • mvn -q -o -f hugegraph-pd/hg-pd-client/pom.xml apache-rat:check
    • mvn -q -o -f hugegraph-pd/hg-pd-test/pom.xml apache-rat:check

The full local PD suite was also attempted. The common suite passed 83/83 and the core suite
passed 89/91 with 2 skipped. The client suite could not complete without a local PD service at
127.0.0.1:8686 (4 failures and 5 errors reported connection refused), which prevented the rest
suite from executing in that Maven run.

Repository-wide RAT is not a valid signal in this checkout because pre-existing ignored
.upgrade-artifacts files produce 68 unrelated unapproved-license entries; RAT passes for both
changed modules.

Does this PR potentially affect the following parts?

Documentation Status

  • Doc - TODO
  • Doc - Done
  • Doc - No Need

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. bug Something isn't working pd PD module tests Add or improve test cases labels Aug 15, 2026

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: yes. Summary: The reconnect implementation has critical identity and failure-propagation risks that can silently stop watches and invalidate existing locks; permanent callback errors also retry without termination. Evidence: static review of KvClient.java and AbstractClient.java at head 772b3a5, with exact server lock/watch handling cross-checked.

@codecov

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 138 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.34%. Comparing base (c9a646d) to head (3c2f4df).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../java/org/apache/hugegraph/pd/client/KvClient.java 0.00% 132 Missing ⚠️
...org/apache/hugegraph/pd/client/AbstractClient.java 0.00% 6 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3157      +/-   ##
============================================
- Coverage     39.23%   38.34%   -0.89%     
- Complexity      264      424     +160     
============================================
  Files           771      771              
  Lines         65938    66024      +86     
  Branches       8759     8778      +19     
============================================
- Hits          25872    25320     -552     
- Misses        37310    37970     +660     
+ Partials       2756     2734      -22     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@imbajin imbajin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: yes. Summary: The current head addresses the previously reported watch reconnect, lock identity, terminal-error retry, and stale-observer issues; focused validation passes, but coverage checks remain failed and independent review evidence is incomplete. Evidence: mvn -pl hugegraph-pd/hg-pd-test -am -Dtest=KvClientTest -DfailIfNoTests=false test (18/18 passed), git diff --check passed; codecov/project and codecov/patch failed.

@bitflicker64 bitflicker64 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: no. Summary: Splitting the lock and watch client IDs, tracking each watch as an independent subscription, and propagating the terminal streaming failure are all solid, and the regression suite is genuinely deterministic; two gaps remain in the reconnect loop itself, plus one visibility nit. Evidence: read of KvClient.java, AbstractClient.java and KvClientTest.java at 3c2f4df; repo greps for streamingCall/listen callers; PD server watch path in KvWatchSubject.notifyClientChangeLeader and KvServiceGrpcImpl.clientWatch; gh -R apache/hugegraph pr checks 3157 (17 pass, only codecov/patch and codecov/project fail).

}
try {
startWatch(subscription);
} catch (PDException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ reconnect() catches only PDException, so an unchecked failure ends the retry chain permanently.

reconnect() clears subscription.reconnectScheduled at the top and then calls startWatch guarded only by catch (PDException e). Any RuntimeException escapes into the ScheduledExecutorService, where it is swallowed into the (discarded) ScheduledFuture. At that point the flag is already false, subscription.observer is null, and nothing reschedules — the subscription is left permanently dead, which is the exact failure mode this PR removes.

This is reachable through the production stub path: startWatchstreamingCallAbstractClient.getStub()resetStub(). resetStub() assigns leaderHost from the members response (AbstractClient.java:150) before it assigns proxy.setStub(...) (AbstractClient.java:156). If stub creation throws for every host, resetStub() returns a non-empty leaderHost while proxy.getStub() is still null, and getStub() then evaluates setAsyncParams(null, config) → NPE, which is unchecked and outside the try in streamingCall.

No test covers it either: TestKvClient.streamingCall only ever throws PDException (KvClientTest.java:752).

Requested change: catch Throwable (or at least RuntimeException) here and route it through the same scheduleReconnect(subscription) path — or move the rescheduling into a finally — and add a test whose stubbed streamingCall throws an unchecked exception, asserting a further reconnect is still scheduled.

return false;
}

acquire(watchClientId, watchSemaphore);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ All reconnects share one thread that can block here indefinitely, so one stalled stream stops every watch from recovering.

acquire(watchClientId, watchSemaphore) performs an untimed semaphore.acquire() whenever watchClientId == 0 (KvClient.java:340-352). The permit only comes back when some watch receives Starting (KvClient.java:185), fails (KvClient.java:286), or is stopped (KvClient.java:301). Reconnects all run on the single-thread executor created at KvClient.java:85-89.

That combination is exercised by an ordinary leader change: KvWatchSubject.notifyClientChangeLeader() sends Leader_Changed to every observer and then calls removeClient(...), which completes each stream, so all subscriptions call requestReconnect (resetting watchClientId to 0) and queue on that one thread. The first task re-issues its stream and returns; the second parks in acquire until the first stream answers. Since resetStub() builds channels with ManagedChannelBuilder.forTarget(host).usePlaintext() and no keepalive (AbstractClient.java:142), a half-open connection that never delivers Starting and never errors parks that thread for good, and no subscription in the client can reconnect.

Requested change: replace the untimed acquire with tryAcquire(timeout, unit) and treat a timeout as a failed attempt that reschedules, and/or give the reconnect executor more than one thread so a stalled subscription cannot block the others.

}
release(watchSemaphore);
subscriptions.remove(subscription);
log.error("Watch for key {} stopped after a non-retryable error: {}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 A permanently stopped watch is invisible to the caller.

stopWatch() drops the subscription from subscriptions and records a log line, but the consumer registered through listen/listenPrefix is never told. Both production callers — PdMetaDriver.listen/listenPrefix (hugegraph-server/hugegraph-core/.../meta/PdMetaDriver.java:110-126) and SchemaDriver.listen (hugegraph-struct/.../SchemaDriver.java:199-205) — only see the initial PDException, so after a CANCELLED, UNAUTHENTICATED or PERMISSION_DENIED stream error the application keeps running as if it were still subscribed and the only signal is a log line.

Requested change: give the caller a way to observe termination — for example an optional error/termination callback on listen, or a handle returned from listen whose state can be queried — so a permanently stopped watch is detectable without scraping logs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes PD KvClient watch reliability by ensuring watches continue to reconnect after repeated failures, leader changes, and unexpected stream completion, while also addressing a lifecycle bug where transport resets could inadvertently invoke subclass close() behavior.

Changes:

  • Refactors AbstractClient to separate transport cleanup (closeConnections()) from the overridable close() lifecycle.
  • Reworks KvClient watch handling to track independent watch subscriptions and keep reconnecting with deduped scheduled retries until recovery or client close.
  • Adds regression tests covering repeated failures/recovery, leader changes, completion recovery, deduplication, stale observers, prefix semantics, and close behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/AbstractClient.java Avoids invoking subclass lifecycle close() during transport reset; improves streaming-call retry failure signaling.
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java Introduces per-watch subscription tracking and scheduled reconnect loop to prevent watches permanently stopping.
hugegraph-pd/hg-pd-test/src/main/java/org/apache/hugegraph/pd/client/KvClientTest.java Adds deterministic coverage for reconnect behaviors and the transport initialization lifecycle regression.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working pd PD module size:XL This PR changes 500-999 lines, ignoring generated files. tests Add or improve test cases

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

[Bug] PD KvClient watch can permanently stop after reconnect failure

4 participants