1818
1919import com .google .cloud .grpc .proto .AffinityConfig ;
2020import com .google .common .base .MoreObjects ;
21+ import com .google .common .base .Preconditions ;
2122import io .grpc .Attributes ;
2223import io .grpc .CallOptions ;
2324import io .grpc .ClientCall ;
3031import java .util .Collections ;
3132import java .util .List ;
3233import java .util .Queue ;
33- import java .util .concurrent .atomic .AtomicBoolean ;
34+ import java .util .concurrent .atomic .AtomicInteger ;
3435import javax .annotation .Nullable ;
3536import javax .annotation .concurrent .GuardedBy ;
3637
@@ -52,7 +53,8 @@ public class GcpClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
5253 private ClientCall <ReqT , RespT > delegateCall = null ;
5354 private List <String > keys = null ;
5455 private boolean received = false ;
55- private final AtomicBoolean decremented = new AtomicBoolean (false );
56+ // 0 = not counted, 1 = counted, 2 = finished.
57+ private final AtomicInteger countState = new AtomicInteger ();
5658
5759 @ GuardedBy ("this" )
5860 private final Queue <Runnable > calls = new ArrayDeque <>();
@@ -123,16 +125,23 @@ public void sendMessage(ReqT message) {
123125 delegateChannelRef = delegateChannel .getChannelRef (key );
124126 }
125127 delegateChannelRef .activeStreamsCountIncr ();
128+ Preconditions .checkState (countState .compareAndSet (0 , 1 ));
126129
127130 // Create the client call and do the previous operations.
128131 CallOptions callOptionsWithChannelId =
129132 callOptions .withOption (GcpManagedChannel .CHANNEL_ID_KEY , delegateChannelRef .getId ());
130- delegateCall =
131- delegateChannelRef .getChannel ().newCall (methodDescriptor , callOptionsWithChannelId );
132- for (Runnable call : calls ) {
133- call .run ();
133+ try {
134+ delegateCall =
135+ delegateChannelRef .getChannel ().newCall (methodDescriptor , callOptionsWithChannelId );
136+ for (Runnable call : calls ) {
137+ call .run ();
138+ }
139+ } catch (RuntimeException | Error failure ) {
140+ finishCount (Status .fromThrowable (failure ), true );
141+ throw failure ;
142+ } finally {
143+ calls .clear ();
134144 }
135- calls .clear ();
136145 started = true ;
137146 }
138147 }
@@ -165,12 +174,16 @@ public String toString() {
165174 }
166175
167176 private void checkedCancel (@ Nullable String message , @ Nullable Throwable cause ) {
168- if (!decremented .getAndSet (true )) {
169- delegateChannelRef .activeStreamsCountDecr (startNanos , Status .CANCELLED , true );
170- }
177+ finishCount (Status .CANCELLED , true );
171178 delegateCall .cancel (message , cause );
172179 }
173180
181+ private void finishCount (Status status , boolean cancelled ) {
182+ if (countState .compareAndSet (1 , 2 )) {
183+ delegateChannelRef .activeStreamsCountDecr (startNanos , status , cancelled );
184+ }
185+ }
186+
174187 private void checkSendMessage (Runnable call ) {
175188 synchronized (this ) {
176189 if (started ) {
@@ -188,9 +201,7 @@ private Listener<RespT> getListener(final Listener<RespT> responseListener) {
188201 // Decrement the stream number by one when the call is closed.
189202 @ Override
190203 public void onClose (Status status , Metadata trailers ) {
191- if (!decremented .getAndSet (true )) {
192- delegateChannelRef .activeStreamsCountDecr (startNanos , status , false );
193- }
204+ finishCount (status , false );
194205 // If the operation completed successfully, bind/unbind the affinity key.
195206 if (keys != null && status .getCode () == Status .Code .OK ) {
196207 if (affinity .getCommand () == AffinityConfig .Command .UNBIND ) {
@@ -231,9 +242,10 @@ public static class SimpleGcpClientCall<ReqT, RespT> extends ForwardingClientCal
231242 private final ClientCall <ReqT , RespT > delegateCall ;
232243 @ Nullable private final String affinityKey ;
233244 private final boolean unbindOnComplete ;
234- private long startNanos = 0 ;
245+ private long startNanos ;
235246
236- private final AtomicBoolean decremented = new AtomicBoolean (false );
247+ // 0 = not counted, 1 = counted, 2 = finished.
248+ private final AtomicInteger countState = new AtomicInteger ();
237249
238250 protected SimpleGcpClientCall (
239251 GcpManagedChannel delegateChannel ,
@@ -247,8 +259,16 @@ protected SimpleGcpClientCall(
247259 // Set the actual channel ID in callOptions so downstream interceptors can access it.
248260 CallOptions callOptionsWithChannelId =
249261 callOptions .withOption (GcpManagedChannel .CHANNEL_ID_KEY , channelRef .getId ());
250- this .delegateCall =
251- channelRef .getChannel ().newCall (methodDescriptor , callOptionsWithChannelId );
262+ startNanos = System .nanoTime ();
263+ channelRef .activeStreamsCountIncr ();
264+ Preconditions .checkState (countState .compareAndSet (0 , 1 ));
265+ try {
266+ this .delegateCall =
267+ channelRef .getChannel ().newCall (methodDescriptor , callOptionsWithChannelId );
268+ } catch (RuntimeException | Error failure ) {
269+ finishCount (Status .fromThrowable (failure ), true );
270+ throw failure ;
271+ }
252272 }
253273
254274 @ Override
@@ -258,16 +278,12 @@ protected ClientCall<ReqT, RespT> delegate() {
258278
259279 @ Override
260280 public void start (Listener <RespT > responseListener , Metadata headers ) {
261- startNanos = System .nanoTime ();
262-
263281 Listener <RespT > listener =
264282 new ForwardingClientCallListener .SimpleForwardingClientCallListener <RespT >(
265283 responseListener ) {
266284 @ Override
267285 public void onClose (Status status , Metadata trailers ) {
268- if (!decremented .getAndSet (true )) {
269- channelRef .activeStreamsCountDecr (startNanos , status , false );
270- }
286+ finishCount (status , false );
271287 // Unbind the affinity key when the caller explicitly requests it
272288 // (e.g., on terminal RPCs like Commit or Rollback) to prevent
273289 // unbounded growth of the affinity map.
@@ -284,20 +300,28 @@ public void onMessage(RespT message) {
284300 }
285301 };
286302
287- channelRef .activeStreamsCountIncr ();
288- delegateCall .start (listener , headers );
303+ try {
304+ delegateCall .start (listener , headers );
305+ } catch (RuntimeException | Error failure ) {
306+ finishCount (Status .fromThrowable (failure ), true );
307+ throw failure ;
308+ }
289309 }
290310
291311 @ Override
292312 public void cancel (String message , Throwable cause ) {
293- if (!decremented .getAndSet (true )) {
294- channelRef .activeStreamsCountDecr (startNanos , Status .CANCELLED , true );
295- }
313+ finishCount (Status .CANCELLED , true );
296314 // Always unbind on cancel — the transaction is being abandoned.
297315 if (affinityKey != null ) {
298316 delegateChannel .unbind (Collections .singletonList (affinityKey ));
299317 }
300318 delegateCall .cancel (message , cause );
301319 }
320+
321+ private void finishCount (Status status , boolean cancelled ) {
322+ if (countState .compareAndSet (1 , 2 )) {
323+ channelRef .activeStreamsCountDecr (startNanos , status , cancelled );
324+ }
325+ }
302326 }
303327}
0 commit comments