bitflicker64 commented on code in PR #3157:
URL: https://github.com/apache/hugegraph/pull/3157#discussion_r3901065262
##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/KvClient.java:
##########
@@ -75,6 +131,16 @@ protected AbstractBlockingStub createBlockingStub() {
return KvServiceGrpc.newBlockingStub(channel);
}
+ @Override
+ protected long stubResetTimeoutMillis() {
+ return Math.max(1L, Math.min(config.getGrpcTimeOut(),
WATCH_START_TIMEOUT_MS));
Review Comment:
⚠️ Important. The watch-start budget is applied client-wide, so it also
shrinks the PD reconnect budget for every blocking KV and lock call.
`stubResetTimeoutMillis()` is consumed by `AbstractClient.resetStub()` as
the total deadline for the whole peer loop (AbstractClient.java:156-159), and
`resetStub()` is reached from `getBlockingStub()` (AbstractClient.java:107-116)
— not just from the watch path. That method backs `put`, `get`, `delete`,
`deletePrefix`, `scanPrefix`, `putTTL`, `keepTTLAlive`, `lock`,
`lockWithoutReentrant`, `isLocked`, `unlock` and `keepAlive`. With the default
`PDConfig.grpcTimeOut = 60000` (PDConfig.java:34) this override returns 5000,
and the derived per-peer deadline becomes `min(60000, remaining /
remainingHosts)` — about 1.7 s each across three PD peers instead of 60 s.
Measured against three unreachable peers (`10.255.255.1-3:8686`), same
`PDConfig`, same `getBlockingStub()` path:
| build | `KvClient.get()` | `MetaClient.getGraphs()` |
|---|---|---|
| base `7bb624b99` | 21328 / 35403 / 6511 ms | 37053 ms |
| head `f99b6bdd` | 4984 / 5213 / 5212 ms | 25071 ms |
At this head `KvClient` pins to ~5.0 s on every run — exactly
`WATCH_START_TIMEOUT_MS` — while a sibling client on identical configuration
keeps its multi-peer budget. So a configured `pd.grpc_timeout` of 60 s is
silently honoured for `MetaClient` but capped at 5 s for `KvClient`. During a
slow PD leader election or a GC pause longer than the per-peer slice, `lock()`
and `keepAlive()` now fail with `PD_UNREACHABLE` where they previously waited.
The same cap applies to store-node startup config reads
(`PdConfigureListener.onApplicationEvent` calls `scanPrefix`/`put`) and to
`PdMetaDriver`/`SchemaDriver`.
Requested change: scope the 5 s readiness budget to the watch-start path
only — for example thread it through the async stub reset that `startWatch()`
triggers, or gate the override on the async stub — and leave
`getBlockingStub()` on the inherited `grpcTimeOut * hostCount` budget so
`pd.grpc_timeout` keeps governing lock and KV traffic.
##########
hugegraph-struct/src/main/java/org/apache/hugegraph/SchemaDriver.java:
##########
@@ -98,24 +117,73 @@ public static void init(PDConfig pdConfig) {
}
public static void init(PDConfig pdConfig, int cacheSize, long expiration)
{
- SchemaDriver instance = INSTANCE.get();
- if (instance != null) {
- throw new NotAllowException(
- "The SchemaDriver [cacheSize=%s, expiration=%s, " +
- "client=%s] has already been initialized and is not " +
- "allowed to be initialized again", instance.caches.limit(),
- instance.caches.expiration(), instance.client);
+ synchronized (LIFECYCLE_LOCK) {
+ SchemaDriver instance = INSTANCE.get();
+ if (instance != null) {
+ throw new NotAllowException(
+ "The SchemaDriver [cacheSize=%s, expiration=%s, " +
+ "client=%s] has already been initialized and is not " +
+ "allowed to be initialized again",
instance.caches.limit(),
+ instance.caches.expiration(), instance.client);
+ }
+ INSTANCE.set(new SchemaDriver(pdConfig, cacheSize, expiration));
}
- INSTANCE.compareAndSet(null, new SchemaDriver(pdConfig, cacheSize,
- expiration));
}
public static void destroy() {
- SchemaDriver instance = INSTANCE.get();
- if (instance != null) {
- instance.caches.cancelScheduleCacheClean();
- instance.caches.destroyAll();
- INSTANCE.set(null);
+ SchemaDriver instance = null;
+ CountDownLatch completion;
+ boolean closeResources = false;
+ synchronized (LIFECYCLE_LOCK) {
Review Comment:
⚠️ Important. `destroy()` keeps `INSTANCE` published while the driver's
`KvClient` is being closed, so callers can be handed a driver whose PD client
is already shut down.
`INSTANCE` is only cleared in the `finally` at line 158-161, after
`instance.closeResources()` (line 156) has already run `client.close()`. The
new `testDestroyKeepsInstanceUntilResourcesAreClosed` asserts exactly this
window. Meanwhile `SchemaGraph.schemaDriverInit()` (SchemaGraph.java:56-64)
checks `SchemaDriver.getInstance() == null`, sees the still-published instance,
skips `init()`, and returns it — and `SchemaGraph`'s constructor immediately
calls `loadConfig()` -> `graphConfig()` -> `client.get()` on that driver.
By then `KvClient.close()` has set `closed = true`, shut down both reconnect
executors and called `shutdownNow()` on the channel, so
`AbstractClient.resetStub()` breaks out on its first `isShutdown()` check
(AbstractClient.java:159) and `getBlockingStub()` throws `PD_UNREACHABLE`. On
the base commit `destroy()` never closed the client, so a driver observed in
this window still worked. `init()` is unusable in the window too: it takes
`LIFECYCLE_LOCK`, sees the non-null `INSTANCE`, and throws `NotAllowException`
(asserted by `testInitDoesNotWaitForDestroyCleanup`).
This is the mirror image of the earlier review note about clearing
`INSTANCE` too early — the fix moved the hazard rather than removing it,
because publication and usability are still two separate states.
Requested change: make the instance unreachable before its resources are
torn down while still preventing a premature re-`init()` — for example clear
`INSTANCE` inside the first `LIFECYCLE_LOCK` block and let the `destroying`
flag alone block `init()` until cleanup finishes, so `getInstance()` never
returns a driver with a closed client. Please add a test that calls
`getInstance()` during the cleanup window and asserts the returned driver is
either absent or still usable.
##########
hugegraph-pd/hg-pd-client/src/main/java/org/apache/hugegraph/pd/client/AbstractClient.java:
##########
@@ -249,39 +326,70 @@ protected <ReqT, RespT> KVPair<Boolean, RespT>
concurrentBlockingUnaryCall(
protected <ReqT, RespT> void streamingCall(MethodDescriptor<ReqT, RespT>
method, ReqT request,
StreamObserver<RespT>
responseObserver,
int retry) throws PDException {
- AbstractStub stub = getStub();
+ AbstractStub stub;
+ Channel attemptChannel;
+ synchronized (this) {
+ stub = getStub();
+ AbstractStub currentStub = proxy.getStub();
+ attemptChannel = currentStub == null ? stub.getChannel() :
currentStub.getChannel();
+ Consumer<Channel> attemptConsumer =
this.streamingAttemptConsumer.get();
+ if (attemptConsumer != null) {
+ attemptConsumer.accept(attemptChannel);
+ }
+ }
try {
ClientCall<ReqT, RespT> call = stub.getChannel().newCall(method,
stub.getCallOptions());
ClientCalls.asyncServerStreamingCall(call, request,
responseObserver);
} catch (Exception e) {
log.error("rpc call with exception :", e);
if (e instanceof StatusRuntimeException) {
if (retry < proxy.getHostCount()) {
- synchronized (this) {
- proxy.setStub(null);
- }
+ invalidateAsyncStub(attemptChannel);
streamingCall(method, request, responseObserver, ++retry);
+ return;
}
}
+ throw new PDException(Pdpb.ErrorType.PD_UNREACHABLE_VALUE,
+ "RPC streaming call failed", e);
+ }
+ }
+
+ protected <ReqT, RespT> void streamingCall(MethodDescriptor<ReqT, RespT>
method, ReqT request,
+ StreamObserver<RespT>
responseObserver,
+ int retry,
+ Consumer<Channel>
attemptConsumer)
+ throws PDException {
+ Consumer<Channel> previous = this.streamingAttemptConsumer.get();
Review Comment:
🧹 Minor. Routing `attemptConsumer` through a `ThreadLocal` makes it
invisible to the documented 4-arg override point, which silently disables
transport rotation.
This overload stores the consumer in `streamingAttemptConsumer` (line 63)
and relies on the 4-arg `streamingCall` reading it back (line 335-338). A
subclass that overrides the 4-arg method — which
`testLegacyStreamingCallOverrideRemainsDispatchTarget` (KvClientTest.java:893,
`LegacyStreamingOverrideKvClient` at KvClientTest.java:1502-1518) deliberately
establishes as supported — never invokes the consumer.
`KvClient.recordAttemptChannel()` then never runs, so
`subscription.attemptChannel` stays null. On `Leader_Changed`,
`requestReconnect(..., rotateTransport = true)` copies that null into
`reconnectChannel` (KvClient.java:429-432), `reconnect()` skips
`invalidateAttemptStub()` (KvClient.java:556-558), the cached async stub
survives, and the watch re-attaches to the same follower — the exact failure
this PR is fixing, reintroduced for any subclass that keeps the legacy override.
The coupling is also implicit: the two calls only work because they happen
on the same thread, and the value is keyed on a per-instance `ThreadLocal`
whose entry is only reclaimed when the `finally` runs or the client is
collected.
Requested change: pass the consumer explicitly rather than through thread
state — have both public overloads delegate to one private implementation that
takes `Consumer<Channel> attemptConsumer` as a parameter (null for the legacy
signature), and drop the `ThreadLocal` field. That keeps the 4-arg override
working as a dispatch target while making it obvious at the call site that an
overrider must forward the consumer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]