From 90e5d7abfc7d7bc9cf1426c2f178047cfc09efdf Mon Sep 17 00:00:00 2001 From: Igor Bernstein Date: Fri, 13 Nov 2026 16:27:36 +0000 Subject: [PATCH 1/2] docs(bigtable): clarify session close, cancelRpc, and heartbeat invariants --- .../data/v2/internal/session/Session.java | 27 ++++++++++++++++--- .../data/v2/internal/session/SessionImpl.java | 24 +++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/Session.java b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/Session.java index e025ce71518b..e42e5f030939 100644 --- a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/Session.java +++ b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/Session.java @@ -33,7 +33,24 @@ * vRPC transport. */ public interface Session { - /** State transitions of Session, will happen in defined order */ + /** + * State transitions of Session, will happen in defined order: + * + * + */ enum SessionState { NEW(0), STARTING(1), @@ -95,12 +112,14 @@ OpenParams withIncrementedAttempts() { void start(OpenSessionRequest req, Metadata headers, Listener sessionListener); /** - * Caller instructed close of the Session. Will immediately close the session and abort all - * outstanding vRPCs. + * Initiates a graceful close of the Session. Transitions the session to {@link + * SessionState#CLOSING}, waits for any in-flight vRPC on the wire (including a client-cancelled + * one) to receive its server response, and then sends {@link CloseSessionRequest} ({@link + * SessionState#WAIT_SERVER_CLOSE}). */ void close(CloseSessionRequest req); - /** Force close a session. */ + /** Immediately aborts the session stream and fails any in-flight vRPC. */ void forceClose(CloseSessionRequest reason); /** diff --git a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java index b155ffd4d7ad..0668b8fe371c 100644 --- a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java +++ b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java @@ -405,6 +405,10 @@ public void close(CloseSessionRequest req) { closeReason = req; updateState(SessionState.CLOSING); + // If a vRPC is still in flight on the wire (even if cancelled locally via cancelRpc), + // wait in CLOSING for the server's vRPC response before sending CloseSessionRequest: + // the protocol has no wire cancel-vRPC message yet, and sending CloseSessionRequest while + // the server still has an active vRPC logs a server-side blackholed-response error. if (currentRpc == null) { startGracefulClose(); } @@ -493,6 +497,17 @@ public void startRpc(VRpcImpl rpc, VirtualRpcRequest payload) { public void cancelRpc(long rpcId, @Nullable String message, @Nullable Throwable cause) { sessionSyncContext.execute( () -> { + // The Session V3 protocol does not yet have a wire cancel-vRPC message, so the server + // continues executing the vRPC until it finishes or hits the server-enforced + // VirtualRpcRequest.deadline. We record currentCancel here while keeping currentRpc set + // until handleVRpcResponse / handleVRpcErrorResponse arrives so that: + // 1. A READY session is not returned to the pool while the server still has an active + // vRPC on it (which would violate the single-in-flight-vRPC limit on the next call). + // 2. A CLOSING session waits for the server to finish the vRPC before sending + // CloseSessionRequest (sending CloseSessionRequest while a vRPC is still active on + // the server is treated as a blackholed-response error by the server). + // Note: the caller's operation future is already completed immediately by + // RetryingVRpc.cancel(). if (currentRpc != null && rpcId == currentRpc.rpcId) { currentCancel = VRpcResult.createRejectedError( @@ -556,6 +571,15 @@ private void cancelCloseTimeout() { // Runs on sessionSyncContext (dispatched from the wheel-timer tick body). If the session is // still in WAIT_SERVER_CLOSE, the server never closed the stream after CloseSessionRequest; // force-close so the session reaches CLOSED. + // + // Why this only covers WAIT_SERVER_CLOSE and not CLOSING: + // - A session only stays in CLOSING while currentRpc != null (an active vRPC is on the wire). + // - Server invariant: the server only sends HeartbeatResponse while a vRPC is actively + // running on the server, and enforces VirtualRpcRequest.deadline on every vRPC. + // - Therefore, CLOSING is already bounded by checkHeartbeat() (if the server dies/stalls) and + // the server-enforced vRPC deadline (if the server is alive and sending heartbeats). + // - Once the vRPC finishes and the session enters WAIT_SERVER_CLOSE, no vRPC is active so + // server heartbeats stop, and closeTimeout takes over to bound waiting for stream closure. private void checkCloseTimeout() { sessionSyncContext.throwIfNotInThisSynchronizationContext(); if (state != SessionState.WAIT_SERVER_CLOSE) { From 51232602b2c7bb7ea3960553e64100b83fd58830 Mon Sep 17 00:00:00 2001 From: Igor Bernstein Date: Fri, 13 Nov 2026 16:45:12 +0000 Subject: [PATCH 2/2] docs(bigtable): remove 'V3' from Session protocol comment --- .../cloud/bigtable/data/v2/internal/session/SessionImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java index 0668b8fe371c..d07bbc348d39 100644 --- a/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java +++ b/java-bigtable/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/session/SessionImpl.java @@ -497,7 +497,7 @@ public void startRpc(VRpcImpl rpc, VirtualRpcRequest payload) { public void cancelRpc(long rpcId, @Nullable String message, @Nullable Throwable cause) { sessionSyncContext.execute( () -> { - // The Session V3 protocol does not yet have a wire cancel-vRPC message, so the server + // The Session protocol does not yet have a wire cancel-vRPC message, so the server // continues executing the vRPC until it finishes or hits the server-enforced // VirtualRpcRequest.deadline. We record currentCancel here while keeping currentRpc set // until handleVRpcResponse / handleVRpcErrorResponse arrives so that: