Skip to content

Commit a7fded2

Browse files
committed
use ThreadLocal so that metrics can re-use this solution if span is turned off
# Conflicts: # java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java
1 parent 4399a85 commit a7fded2

File tree

5 files changed

+127
-22
lines changed

5 files changed

+127
-22
lines changed

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,15 @@
2727
import com.google.cloud.RetryHelper;
2828
import io.opentelemetry.api.trace.Span;
2929
import io.opentelemetry.api.trace.Tracer;
30-
import io.opentelemetry.context.Context;
31-
import io.opentelemetry.context.ContextKey;
3230
import io.opentelemetry.context.Scope;
3331
import java.io.IOException;
3432
import java.util.concurrent.Callable;
3533
import java.util.concurrent.ExecutionException;
36-
import java.util.concurrent.atomic.AtomicInteger;
3734
import java.util.logging.Level;
3835
import java.util.logging.Logger;
3936

4037
public class BigQueryRetryHelper extends RetryHelper {
4138

42-
public static final ContextKey<AtomicInteger> RETRY_ATTEMPT_KEY =
43-
ContextKey.named("bq_retry_attempt");
44-
4539
private static final Logger LOG = Logger.getLogger(BigQueryRetryHelper.class.getName());
4640

4741
public static <V> V runWithRetries(
@@ -60,11 +54,8 @@ public static <V> V runWithRetries(
6054
.spanBuilder("com.google.cloud.bigquery.BigQueryRetryHelper.runWithRetries")
6155
.startSpan();
6256
}
63-
Context retryContext = Context.current().with(RETRY_ATTEMPT_KEY, new AtomicInteger(0));
64-
if (runWithRetries != null) {
65-
retryContext = retryContext.with(runWithRetries);
66-
}
67-
try (Scope runWithRetriesScope = retryContext.makeCurrent()) {
57+
try (Scope runWithRetriesScope = runWithRetries != null ? runWithRetries.makeCurrent() : null;
58+
BigQueryRetryTracker tracker = new BigQueryRetryTracker()) {
6859
// Suppressing should be ok as a workaraund. Current and only ResultRetryAlgorithm
6960
// implementation does not use response at all, so ignoring its type is ok.
7061
@SuppressWarnings("unchecked")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigquery;
17+
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
/**
21+
* Tracks retry attempts for a client request using a ThreadLocal for telemetry data.
22+
*
23+
* <p>It implements {@link AutoCloseable} to ensure that the ThreadLocal is safely restored to its
24+
* previous state (or removed) when the try-with-resources block exits, preventing memory leaks and
25+
* state bleeding in thread-pooling environments.
26+
*/
27+
public class BigQueryRetryTracker implements AutoCloseable {
28+
29+
private static final ThreadLocal<AtomicInteger> HOLDER = new ThreadLocal<>();
30+
private final AtomicInteger previous;
31+
32+
public BigQueryRetryTracker() {
33+
this.previous = HOLDER.get();
34+
HOLDER.set(new AtomicInteger(0));
35+
}
36+
37+
public static AtomicInteger get() {
38+
return HOLDER.get();
39+
}
40+
41+
@Override
42+
public void close() {
43+
if (previous == null) {
44+
HOLDER.remove();
45+
} else {
46+
HOLDER.set(previous);
47+
}
48+
}
49+
}

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import com.google.api.client.http.*;
2020
import com.google.api.core.BetaApi;
2121
import com.google.api.core.InternalApi;
22-
import com.google.cloud.bigquery.BigQueryRetryHelper;
22+
import com.google.cloud.bigquery.BigQueryRetryTracker;
2323
import com.google.common.annotations.VisibleForTesting;
2424
import io.opentelemetry.api.common.AttributeKey;
2525
import io.opentelemetry.api.trace.Span;
@@ -86,7 +86,7 @@ public void initialize(HttpRequest request) throws IOException {
8686

8787
addInitialHttpAttributesToSpan(span, request);
8888

89-
AtomicInteger attemptTracker = Context.current().get(BigQueryRetryHelper.RETRY_ATTEMPT_KEY);
89+
AtomicInteger attemptTracker = BigQueryRetryTracker.get();
9090
if (attemptTracker != null) {
9191
int attempt = attemptTracker.getAndIncrement();
9292
if (attempt > 0) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigquery;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertNull;
21+
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.ExecutionException;
24+
import java.util.concurrent.atomic.AtomicInteger;
25+
import org.junit.jupiter.api.Test;
26+
27+
public class BigQueryRetryTrackerTest {
28+
29+
@Test
30+
public void testCreateAndClose() {
31+
assertNull(BigQueryRetryTracker.get(), "Tracker should be null before create");
32+
33+
try (BigQueryRetryTracker tracker = new BigQueryRetryTracker()) {
34+
AtomicInteger counter = BigQueryRetryTracker.get();
35+
assertNotNull(counter);
36+
assertEquals(0, counter.get());
37+
}
38+
39+
assertNull(BigQueryRetryTracker.get(), "Tracker should be cleaned up after close");
40+
}
41+
42+
@Test
43+
public void testThreadIsolation() throws ExecutionException, InterruptedException {
44+
CompletableFuture<Void> future1 =
45+
CompletableFuture.runAsync(
46+
() -> {
47+
try (BigQueryRetryTracker tracker = new BigQueryRetryTracker()) {
48+
BigQueryRetryTracker.get().set(10);
49+
try {
50+
Thread.sleep(100);
51+
} catch (InterruptedException e) {
52+
Thread.currentThread().interrupt();
53+
}
54+
assertEquals(10, BigQueryRetryTracker.get().get());
55+
}
56+
});
57+
58+
CompletableFuture<Void> future2 =
59+
CompletableFuture.runAsync(
60+
() -> {
61+
try (BigQueryRetryTracker tracker = new BigQueryRetryTracker()) {
62+
BigQueryRetryTracker.get().set(20);
63+
assertEquals(20, BigQueryRetryTracker.get().get());
64+
}
65+
});
66+
67+
future1.get();
68+
future2.get();
69+
}
70+
}

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,10 @@
4444
import com.google.api.client.testing.http.MockHttpTransport;
4545
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
4646
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
47-
import com.google.cloud.bigquery.BigQueryRetryHelper;
47+
import com.google.cloud.bigquery.BigQueryRetryTracker;
4848
import io.opentelemetry.api.common.AttributeKey;
4949
import io.opentelemetry.api.trace.Span;
5050
import io.opentelemetry.api.trace.Tracer;
51-
import io.opentelemetry.context.Context;
5251
import io.opentelemetry.context.Scope;
5352
import io.opentelemetry.sdk.OpenTelemetrySdk;
5453
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
@@ -57,7 +56,6 @@
5756
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
5857
import java.io.IOException;
5958
import java.util.List;
60-
import java.util.concurrent.atomic.AtomicInteger;
6159
import org.junit.jupiter.api.BeforeEach;
6260
import org.junit.jupiter.api.Test;
6361

@@ -321,15 +319,13 @@ public void testAddRequestBodySizeToSpan_WithEncoding() throws IOException {
321319
@Test
322320
public void testRetryCountFromContext() throws IOException {
323321
HttpTransport transport = createTransport();
324-
AtomicInteger counter = new AtomicInteger(2);
325-
Context context =
326-
io.opentelemetry.context.Context.current()
327-
.with(BigQueryRetryHelper.RETRY_ATTEMPT_KEY, counter);
328322

329-
try (io.opentelemetry.context.Scope scope = context.makeCurrent()) {
323+
try (BigQueryRetryTracker tracker = new BigQueryRetryTracker()) {
324+
BigQueryRetryTracker.get().set(2);
330325
HttpRequest request = buildGetRequest(transport, initializer, BASE_URL);
331326
HttpResponse response = request.execute();
332327
response.disconnect();
328+
assertEquals(3, BigQueryRetryTracker.get().get());
333329
}
334330

335331
spanScope.close();
@@ -340,7 +336,6 @@ public void testRetryCountFromContext() throws IOException {
340336
SpanData span = spans.get(0);
341337
assertEquals(
342338
2L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_RESEND_COUNT));
343-
assertEquals(3, counter.get());
344339
}
345340

346341
@Test

0 commit comments

Comments
 (0)