Skip to content

Commit bb3fd91

Browse files
chore: replace OpExecutor backing with an inline-capable queue; tighten Client.close
OpExecutor switches from a SequentialExecutor backing to an internal ArrayDeque + drain loop, and gains runInline() — runs the task synchronously on the caller thread when the executor is idle, otherwise queues it. VOperationImpl uses runInline for chain.start so the start dispatch skips the queue+drain round-trip. Drain rejections (e.g. during shutdown) reset drainScheduled so subsequent submissions can retry. Client.close drains userCallbackExecutor first via a 5s-bounded shutdownAndAwait so pool.close's cancelWithResult onClose notifications complete before the executor is torn down. Resource.close becomes idempotent. Drive-by cleanups: replace fully-qualified type names with imports across touched files, and swap Guava Objects.hashCode(id) for Long.hashCode(id) in ChannelPoolDpImpl.AfeId to avoid per-call boxing and array allocation.
1 parent 62ba853 commit bb3fd91

10 files changed

Lines changed: 163 additions & 47 deletions

File tree

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/api/AuthorizedViewAsync.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.grpc.Deadline;
3434
import java.io.Closeable;
3535
import java.util.concurrent.CompletableFuture;
36+
import java.util.concurrent.Executor;
3637

3738
public class AuthorizedViewAsync implements AutoCloseable, Closeable {
3839

@@ -49,7 +50,7 @@ static AuthorizedViewAsync createAndStart(
4950
Permission permission,
5051
Metrics metrics,
5152
BigtableTimer timer,
52-
java.util.concurrent.Executor userCallbackExecutor) {
53+
Executor userCallbackExecutor) {
5354

5455
AuthorizedViewName viewName =
5556
AuthorizedViewName.builder()

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/api/Client.java

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public static Client create(ClientSettings settings) throws IOException {
171171
Resource.createOwned(metrics, metrics::close),
172172
Resource.createOwned(configManager, configManager::close),
173173
Resource.createOwned(backgroundExecutor, backgroundExecutor::shutdown),
174-
Resource.createOwned(userCallbackExecutor, userCallbackExecutor::shutdown));
174+
Resource.createOwned(userCallbackExecutor, () -> shutdownAndAwait(userCallbackExecutor)));
175175
}
176176

177177
public Client(
@@ -225,13 +225,17 @@ public void close() {
225225
.setReason(CloseSessionReason.CLOSE_SESSION_REASON_USER)
226226
.setDescription("Client closing")
227227
.build()));
228+
// Drain user-callback first so pool.close's cancelWithResult listener notifications complete
229+
// before we tear down the surrounding executors and timer. Without this, the late onClose
230+
// submissions race the shutdown and get RejectedExecutionException, silently dropping the
231+
// user's terminal onClose.
232+
userCallbackExecutor.close();
228233
metrics.close();
229234
channelPool.close();
230235
configManager.close();
231236
// Stop the timer before tearing down backgroundExecutor (the timer's dispatcher).
232237
sessionTimer.stop();
233238
backgroundExecutor.close();
234-
userCallbackExecutor.close();
235239
}
236240

237241
public TableAsync openTableAsync(String tableId, Permission permission) {
@@ -289,8 +293,10 @@ public MaterializedViewAsync openMaterializedViewAsync(
289293
}
290294

291295
public static class Resource<T> {
292-
private T value;
293-
private Runnable closer;
296+
private final T value;
297+
private final Runnable closer;
298+
private final java.util.concurrent.atomic.AtomicBoolean closed =
299+
new java.util.concurrent.atomic.AtomicBoolean(false);
294300

295301
public static <T> Resource<T> createOwned(T value, Runnable closer) {
296302
return new Resource<>(value, closer);
@@ -305,12 +311,29 @@ private Resource(T value, Runnable closer) {
305311
this.closer = closer;
306312
}
307313

314+
/** Idempotent. Repeat calls are no-ops. */
308315
public void close() {
309-
this.closer.run();
316+
if (closed.compareAndSet(false, true)) {
317+
this.closer.run();
318+
}
310319
}
311320

312321
public T get() {
313322
return value;
314323
}
315324
}
325+
326+
// Drain in-flight listener.onClose tasks before the executor is shut down; bound the wait at 5s
327+
// so close() doesn't hang the caller on a pathological listener.
328+
private static void shutdownAndAwait(ExecutorService exec) {
329+
exec.shutdown();
330+
try {
331+
if (!exec.awaitTermination(5, TimeUnit.SECONDS)) {
332+
exec.shutdownNow();
333+
}
334+
} catch (InterruptedException e) {
335+
Thread.currentThread().interrupt();
336+
exec.shutdownNow();
337+
}
338+
}
316339
}

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/api/MaterializedViewAsync.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import io.grpc.Deadline;
3131
import java.io.Closeable;
3232
import java.util.concurrent.CompletableFuture;
33+
import java.util.concurrent.Executor;
3334

3435
public class MaterializedViewAsync implements AutoCloseable, Closeable {
3536

@@ -45,7 +46,7 @@ public static MaterializedViewAsync createAndStart(
4546
OpenMaterializedViewRequest.Permission permission,
4647
Metrics metrics,
4748
BigtableTimer timer,
48-
java.util.concurrent.Executor userCallbackExecutor) {
49+
Executor userCallbackExecutor) {
4950

5051
MaterializedViewName viewName =
5152
MaterializedViewName.builder()

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/api/TableAsync.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.grpc.Deadline;
3535
import java.io.Closeable;
3636
import java.util.concurrent.CompletableFuture;
37+
import java.util.concurrent.Executor;
3738

3839
public class TableAsync implements AutoCloseable, Closeable {
3940
private final TableBase base;
@@ -48,7 +49,7 @@ public static TableAsync createAndStart(
4849
Permission permission,
4950
Metrics metrics,
5051
BigtableTimer timer,
51-
java.util.concurrent.Executor userCallbackExecutor) {
52+
Executor userCallbackExecutor) {
5253

5354
TableName tableName =
5455
TableName.builder()

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/channels/ChannelPoolDpImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ public boolean equals(Object o) {
573573

574574
@Override
575575
public int hashCode() {
576-
return com.google.common.base.Objects.hashCode(id);
576+
return Long.hashCode(id);
577577
}
578578

579579
@Override

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/compat/ShimImpl.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,16 @@
4747
import com.google.cloud.bigtable.data.v2.stub.MetadataExtractorInterceptor;
4848
import com.google.common.base.Strings;
4949
import com.google.common.collect.ImmutableMap;
50+
import com.google.common.util.concurrent.ThreadFactoryBuilder;
5051
import io.grpc.ClientInterceptor;
5152
import io.grpc.ManagedChannel;
5253
import io.grpc.Metadata;
5354
import io.grpc.stub.MetadataUtils;
5455
import java.io.IOException;
5556
import java.time.Duration;
5657
import java.util.Optional;
58+
import java.util.concurrent.ExecutorService;
59+
import java.util.concurrent.Executors;
5760
import java.util.concurrent.ScheduledExecutorService;
5861
import java.util.concurrent.TimeUnit;
5962
import java.util.logging.Level;
@@ -160,9 +163,9 @@ public static Shim create(
160163
featureFlags = featureFlags.toBuilder().setSessionsRequired(true).build();
161164
}
162165

163-
java.util.concurrent.ExecutorService userCallbackExecutor =
164-
java.util.concurrent.Executors.newCachedThreadPool(
165-
new com.google.common.util.concurrent.ThreadFactoryBuilder()
166+
ExecutorService userCallbackExecutor =
167+
Executors.newCachedThreadPool(
168+
new ThreadFactoryBuilder()
166169
.setNameFormat("bigtable-callback-shim-%d")
167170
.setDaemon(true)
168171
.build());

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/middleware/OpExecutor.java

Lines changed: 96 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,26 @@
1616

1717
package com.google.cloud.bigtable.data.v2.internal.middleware;
1818

19+
import com.google.common.util.concurrent.MoreExecutors;
20+
import java.util.ArrayDeque;
1921
import java.util.concurrent.Executor;
2022

2123
/**
22-
* Per-op serializing executor. Wraps a delegate {@link Executor} (typically a per-call {@code
23-
* SerializingExecutor} over the user-callback pool) and tracks which thread is currently running a
24-
* task, so callers can assert they are on the executor (analogous to {@code
24+
* Per-op serializing executor that tracks which thread is currently draining it, so callers can
25+
* assert they are running inside the executor (analogous to {@code
2526
* SynchronizationContext#throwIfNotInThisSynchronizationContext}).
2627
*
27-
* <p>If a task throws, the registered {@link UncaughtExceptionHandler} is invoked — this is the
28-
* last-resort recovery point for the chain. Without it, a throw from a user-installed tracer or a
29-
* listener callback would silently drop and the caller's future would never complete.
28+
* <p>{@link #runInline} executes synchronously on the caller thread when the executor is idle, or
29+
* falls back to a queued submission when busy. Use it to avoid the queue+drain round-trip for the
30+
* first task of a fresh op executor (e.g. the start() dispatch).
31+
*
32+
* <p>If a task throws, the registered {@link UncaughtExceptionHandler} is invoked on the same task
33+
* thread (still inside the drain wrapper, so {@link #throwIfNotInThisExecutor} still passes). This
34+
* is the last-resort recovery point for the op chain — without it, an exception inside a callback
35+
* is silently dropped and the caller's listener never sees a terminal close. The handler's own
36+
* throws propagate (caught by the backing executor in production; surfaced to the calling thread
37+
* when the backing is {@link MoreExecutors#directExecutor()}, which is what makes fail-fast test
38+
* handlers like {@code t -> throw new AssertionError(t)} work).
3039
*/
3140
public final class OpExecutor implements Executor {
3241

@@ -36,6 +45,11 @@ public interface UncaughtExceptionHandler {
3645

3746
private final Executor backing;
3847
private final UncaughtExceptionHandler handler;
48+
49+
// Guards queue and drainScheduled. runningThread is volatile so throwIfNotInThisExecutor() can
50+
// read it lock-free; writes happen inside the lock to piggy-back the memory barrier.
51+
private final ArrayDeque<Runnable> queue = new ArrayDeque<>();
52+
private boolean drainScheduled = false;
3953
private volatile Thread runningThread;
4054

4155
public OpExecutor(Executor backing, UncaughtExceptionHandler handler) {
@@ -45,18 +59,82 @@ public OpExecutor(Executor backing, UncaughtExceptionHandler handler) {
4559

4660
@Override
4761
public void execute(Runnable r) {
48-
backing.execute(
49-
() -> {
50-
Thread prev = runningThread;
51-
runningThread = Thread.currentThread();
52-
try {
53-
r.run();
54-
} catch (Throwable t) {
55-
handler.uncaught(t);
56-
} finally {
57-
runningThread = prev;
58-
}
59-
});
62+
synchronized (queue) {
63+
queue.add(r);
64+
if (!drainScheduled && runningThread == null) {
65+
scheduleDrainLocked();
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Runs {@code r} synchronously on the caller thread if this executor is idle, otherwise queues
72+
* it for later drain on the backing executor. Either way, FIFO ordering with other tasks is
73+
* preserved.
74+
*/
75+
public void runInline(Runnable r) {
76+
synchronized (queue) {
77+
if (drainScheduled || runningThread != null || !queue.isEmpty()) {
78+
queue.add(r);
79+
if (!drainScheduled) {
80+
scheduleDrainLocked();
81+
}
82+
return;
83+
}
84+
runningThread = Thread.currentThread();
85+
}
86+
try {
87+
try {
88+
r.run();
89+
} catch (Throwable t) {
90+
handler.uncaught(t);
91+
}
92+
} finally {
93+
synchronized (queue) {
94+
runningThread = null;
95+
if (!queue.isEmpty() && !drainScheduled) {
96+
scheduleDrainLocked();
97+
}
98+
}
99+
}
100+
}
101+
102+
// Schedule a drain on the backing executor. If the backing throws (e.g. RejectedExecutionException
103+
// during shutdown), reset drainScheduled before propagating so the next execute() can retry
104+
// instead of wedging the executor with no drainer.
105+
private void scheduleDrainLocked() {
106+
drainScheduled = true;
107+
try {
108+
backing.execute(this::drain);
109+
} catch (Throwable t) {
110+
drainScheduled = false;
111+
throw t;
112+
}
113+
}
114+
115+
private void drain() {
116+
while (true) {
117+
Runnable r;
118+
synchronized (queue) {
119+
r = queue.poll();
120+
if (r == null) {
121+
drainScheduled = false;
122+
return;
123+
}
124+
runningThread = Thread.currentThread();
125+
}
126+
try {
127+
try {
128+
r.run();
129+
} catch (Throwable t) {
130+
handler.uncaught(t);
131+
}
132+
} finally {
133+
synchronized (queue) {
134+
runningThread = null;
135+
}
136+
}
137+
}
60138
}
61139

62140
public void throwIfNotInThisExecutor() {

java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/middleware/VOperationImpl.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,24 @@ public void start(ReqT req, VRpcListener<RespT> listener) {
8686
// single Done.
8787
OpExecutor exec =
8888
new OpExecutor(
89-
MoreExecutors.newSequentialExecutor(userCallbackExecutor),
90-
t -> chain.cancel("Uncaught exception in op executor task", t));
89+
userCallbackExecutor, t -> chain.cancel("Uncaught exception in op executor task", t));
9190
this.opExecutor = exec;
9291
VRpcCallContext ctx = VRpcCallContext.create(deadline, idempotent, tracer, exec);
9392
CleanupListener<RespT> wrapped =
9493
new CleanupListener<>(listener, grpcContext, cancellationListener);
95-
// Register the gRPC context listener BEFORE submitting chain.start. The submit queues the
96-
// task on the op executor; chain.cancel from this listener also queues. SequentialExecutor
97-
// preserves submission order, so a context-cancel fired during/before chain.start will be
98-
// processed after it.
99-
grpcContext.addListener(cancellationListener, MoreExecutors.directExecutor());
100-
exec.execute(() -> chain.start(req, ctx, wrapped));
94+
exec.runInline(() -> chain.start(req, ctx, wrapped));
95+
// Register AFTER chain.start so a context-cancel that fires immediately is sequenced behind
96+
// start. runInline runs chain.start synchronously, so it has fully completed by the time the
97+
// listener is registered. Matches ClientCallImpl's ordering (grpc-java issue #1343).
98+
//
99+
// If the chain reached a terminal onClose synchronously inside runInline (uncaught-handler
100+
// recovery, immediate failure), CleanupListener already tried to remove a listener that was
101+
// never registered (no-op). Skip the addListener in that case — otherwise we'd register a
102+
// listener on grpcContext that nothing will ever remove, pinning the entire chain for the
103+
// lifetime of the (potentially long-lived) gRPC Context.
104+
if (!wrapped.closed) {
105+
grpcContext.addListener(cancellationListener, MoreExecutors.directExecutor());
106+
}
101107
}
102108

103109
@Override

java-bigtable/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImplTest.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@
7070
import java.io.IOException;
7171
import java.time.Duration;
7272
import java.time.Instant;
73+
import java.util.concurrent.CountDownLatch;
7374
import java.util.concurrent.ExecutionException;
7475
import java.util.concurrent.Executors;
7576
import java.util.concurrent.ScheduledExecutorService;
7677
import java.util.concurrent.TimeUnit;
7778
import java.util.concurrent.atomic.AtomicInteger;
79+
import java.util.concurrent.atomic.AtomicReference;
7880
import org.junit.jupiter.api.AfterEach;
7981
import org.junit.jupiter.api.BeforeEach;
8082
import org.junit.jupiter.api.Test;
@@ -679,9 +681,9 @@ void abortFiresWhenListenerOnReadyThrows() throws Exception {
679681
SessionImpl session =
680682
new SessionImpl(metrics, poolInfo, 0, sessionFactory.createNew(), timer);
681683

682-
java.util.concurrent.CountDownLatch onCloseLatch = new java.util.concurrent.CountDownLatch(1);
683-
java.util.concurrent.atomic.AtomicReference<Status> capturedStatus =
684-
new java.util.concurrent.atomic.AtomicReference<>();
684+
CountDownLatch onCloseLatch = new CountDownLatch(1);
685+
AtomicReference<Status> capturedStatus =
686+
new AtomicReference<>();
685687

686688
Session.Listener throwingListener =
687689
new Session.Listener() {
@@ -721,8 +723,8 @@ void abortDoesNotHangWhenListenerOnCloseThrows() throws Exception {
721723
SessionImpl session =
722724
new SessionImpl(metrics, poolInfo, 0, sessionFactory.createNew(), timer);
723725

724-
java.util.concurrent.CountDownLatch onReadyLatch = new java.util.concurrent.CountDownLatch(1);
725-
java.util.concurrent.CountDownLatch onCloseLatch = new java.util.concurrent.CountDownLatch(1);
726+
CountDownLatch onReadyLatch = new CountDownLatch(1);
727+
CountDownLatch onCloseLatch = new CountDownLatch(1);
726728

727729
Session.Listener throwingListener =
728730
new Session.Listener() {
@@ -775,8 +777,8 @@ void abortDoesNotInfiniteLoopWhenRecoveryListenerAlsoThrows() throws Exception {
775777
SessionImpl session =
776778
new SessionImpl(metrics, poolInfo, 0, sessionFactory.createNew(), timer);
777779

778-
java.util.concurrent.CountDownLatch onCloseInvoked =
779-
new java.util.concurrent.CountDownLatch(1);
780+
CountDownLatch onCloseInvoked =
781+
new CountDownLatch(1);
780782

781783
Session.Listener doublyThrowingListener =
782784
new Session.Listener() {

0 commit comments

Comments
 (0)