From ca1517734541d8c41adc9d33fc9ea0a4ee47acb1 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Mon, 13 Jul 2026 17:29:46 -0400 Subject: [PATCH 1/4] feat(bigquery-jdbc):implement clearcut transport layer --- .../jdbc/telemetry/v1/ClearcutTransport.java | 112 +++++++++++++++ .../telemetry/v1/ClearcutTransportTest.java | 127 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java create mode 100644 java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java new file mode 100644 index 000000000000..3e738ceaaa29 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc.telemetry.v1; + +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +class ClearcutTransport { + private static final Logger logger = Logger.getLogger(ClearcutTransport.class.getName()); + private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf"; + + private final HttpTransport httpTransport; + private final TelemetryConfiguration config; + private final HttpRequestFactory requestFactory; + + ClearcutTransport(TelemetryConfiguration config) { + this(new NetHttpTransport(), config); + } + + // Package-private constructor for testing overrides + ClearcutTransport(HttpTransport httpTransport, TelemetryConfiguration config) { + this.httpTransport = httpTransport; + this.config = config; + this.requestFactory = this.httpTransport.createRequestFactory(); + } + + boolean send(TelemetryPayload payload) { + if (!config.isEnabled()) { + return false; + } + + long now = System.currentTimeMillis(); + LogRequest logRequest = + LogRequest.newBuilder() + .setLogSource(config.getLogSource()) + .setRequestTimeMs(now) + .addLogEvents( + LogEvent.newBuilder() + .setEventTimeMs(now) + .setSourceExtension(payload.toByteString()) + .build()) + .build(); + + byte[] requestBytes = logRequest.toByteArray(); + HttpContent content = new ByteArrayContent(CONTENT_TYPE_PROTOBUF, requestBytes); + GenericUrl url = new GenericUrl(config.getEndpointUrl()); + + int attempt = 0; + int maxAttempts = 3; + long backoffMs = 1000; + + while (attempt < maxAttempts) { + attempt++; + try { + HttpRequest request = requestFactory.buildPostRequest(url, content); + HttpResponse response = request.execute(); + + try { + int statusCode = response.getStatusCode(); + if (statusCode >= 200 && statusCode < 300) { + logger.log(Level.FINE, "Successfully uploaded telemetry payload to Clearcut"); + return true; + } else { + logger.log(Level.WARNING, "Clearcut upload returned status code: " + statusCode); + } + } finally { + response.disconnect(); + } + } catch (IOException e) { + logger.log(Level.WARNING, "Error sending telemetry payload on attempt " + attempt, e); + if (attempt >= maxAttempts) { + break; + } + try { + Thread.sleep(backoffMs); + backoffMs *= 2; + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + logger.log(Level.WARNING, "Backoff thread interrupted", ie); + break; + } + } + } + logger.log( + Level.SEVERE, + "Failed to upload telemetry payload to Clearcut after " + maxAttempts + " attempts"); + return false; + } +} diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java new file mode 100644 index 000000000000..65f0db3ff38b --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc.telemetry.v1; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.api.client.http.LowLevelHttpRequest; +import com.google.api.client.http.LowLevelHttpResponse; +import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.client.testing.http.MockLowLevelHttpRequest; +import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +public class ClearcutTransportTest { + + @Test + public void testSendWhenDisabled() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(); + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(false).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + boolean result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result); + assertEquals(0, requestCount.get()); + } + + @Test + public void testSendSuccess() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(200); + return response; + } + }; + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + boolean result = transport.send(TelemetryPayload.getDefaultInstance()); + assertTrue(result); + assertEquals(1, requestCount.get()); + } + + @Test + public void testSendServerErrorStatusCodeRetries() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(500); + return response; + } + }; + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + boolean result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result); + assertEquals(3, requestCount.get()); + } + + @Test + public void testSendIOExceptionRetries() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + requestCount.incrementAndGet(); + throw new IOException("Network unreachable"); + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + boolean result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result); + assertEquals(3, requestCount.get()); + } +} From 1a0baa7e9c86a6766832f0597dc58848417ec77d Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Mon, 13 Jul 2026 17:58:57 -0400 Subject: [PATCH 2/4] fix transport method --- .../jdbc/telemetry/v1/ClearcutTransport.java | 46 +++++++++++++++---- .../telemetry/v1/ClearcutTransportTest.java | 27 +++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java index 3e738ceaaa29..fb306a325fa5 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java @@ -24,12 +24,14 @@ import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; class ClearcutTransport { - private static final Logger logger = Logger.getLogger(ClearcutTransport.class.getName()); + private static final Logger logger = + new BigQueryJdbcCustomLogger(ClearcutTransport.class.getName()); private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf"; private final HttpTransport httpTransport; @@ -74,39 +76,65 @@ boolean send(TelemetryPayload payload) { while (attempt < maxAttempts) { attempt++; + boolean retryable = false; + try { HttpRequest request = requestFactory.buildPostRequest(url, content); - HttpResponse response = request.execute(); + request.setThrowExceptionOnExecuteError(false); + HttpResponse response = null; try { + response = request.execute(); int statusCode = response.getStatusCode(); if (statusCode >= 200 && statusCode < 300) { logger.log(Level.FINE, "Successfully uploaded telemetry payload to Clearcut"); return true; + } else if (isRetryableStatusCode(statusCode)) { + logger.log( + Level.WARNING, + "Clearcut upload returned retryable status code: " + + statusCode + + " on attempt " + + attempt); + retryable = true; } else { - logger.log(Level.WARNING, "Clearcut upload returned status code: " + statusCode); + logger.log( + Level.WARNING, + "Clearcut upload failed with non-retryable status code: " + statusCode); + return false; } } finally { - response.disconnect(); + if (response != null) { + response.disconnect(); + } } } catch (IOException e) { - logger.log(Level.WARNING, "Error sending telemetry payload on attempt " + attempt, e); - if (attempt >= maxAttempts) { - break; - } + logger.log( + Level.WARNING, + "IOException sending telemetry payload to Clearcut on attempt " + attempt, + e); + retryable = true; + } + + if (retryable && attempt < maxAttempts) { try { Thread.sleep(backoffMs); backoffMs *= 2; } catch (InterruptedException ie) { Thread.currentThread().interrupt(); logger.log(Level.WARNING, "Backoff thread interrupted", ie); - break; + return false; } } } + logger.log( Level.SEVERE, "Failed to upload telemetry payload to Clearcut after " + maxAttempts + " attempts"); return false; } + + private static boolean isRetryableStatusCode(int statusCode) { + return statusCode == 429 || (statusCode >= 500 && statusCode < 600); + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java index 65f0db3ff38b..ac48e7628d2f 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java @@ -105,6 +105,33 @@ public LowLevelHttpResponse execute() { assertEquals(3, requestCount.get()); } + @Test + public void testSendNonRetryableStatusCodeFailsImmediately() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(400); + return response; + } + }; + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + boolean result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result); + assertEquals(1, requestCount.get()); + } + @Test public void testSendIOExceptionRetries() { AtomicInteger requestCount = new AtomicInteger(0); From 77888b944f81a5b80b96162ec148d81d6cd80d94 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Mon, 13 Jul 2026 18:26:28 -0400 Subject: [PATCH 3/4] String format --- .../jdbc/telemetry/v1/ClearcutTransport.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java index fb306a325fa5..0161240d12a0 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java @@ -92,15 +92,15 @@ boolean send(TelemetryPayload payload) { } else if (isRetryableStatusCode(statusCode)) { logger.log( Level.WARNING, - "Clearcut upload returned retryable status code: " - + statusCode - + " on attempt " - + attempt); + String.format( + "Clearcut upload returned retryable status code: %d on attempt %d", + statusCode, attempt)); retryable = true; } else { logger.log( Level.WARNING, - "Clearcut upload failed with non-retryable status code: " + statusCode); + String.format( + "Clearcut upload failed with non-retryable status code: %d", statusCode)); return false; } } finally { @@ -111,7 +111,8 @@ boolean send(TelemetryPayload payload) { } catch (IOException e) { logger.log( Level.WARNING, - "IOException sending telemetry payload to Clearcut on attempt " + attempt, + String.format( + "IOException sending telemetry payload to Clearcut on attempt %d", attempt), e); retryable = true; } @@ -130,7 +131,8 @@ boolean send(TelemetryPayload payload) { logger.log( Level.SEVERE, - "Failed to upload telemetry payload to Clearcut after " + maxAttempts + " attempts"); + String.format( + "Failed to upload telemetry payload to Clearcut after %d attempts", maxAttempts)); return false; } From 197da300894b86e61c3f62d4d4e54b5fed15adf8 Mon Sep 17 00:00:00 2001 From: Neenu1995 Date: Tue, 14 Jul 2026 12:55:11 -0400 Subject: [PATCH 4/4] respect nextRequestWaitmillis --- .../jdbc/telemetry/v1/ClearcutTransport.java | 108 ++++++++---------- .../jdbc/telemetry/v1/TransportResult.java | 71 ++++++++++++ .../telemetry/v1/ClearcutTransportTest.java | 87 +++++++++++--- 3 files changed, 189 insertions(+), 77 deletions(-) create mode 100644 java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TransportResult.java diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java index 0161240d12a0..5680516cab64 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransport.java @@ -26,13 +26,16 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger; import java.io.IOException; +import java.io.InputStream; import java.util.logging.Level; import java.util.logging.Logger; -class ClearcutTransport { +final class ClearcutTransport { private static final Logger logger = new BigQueryJdbcCustomLogger(ClearcutTransport.class.getName()); private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf"; + private static final int DEFAULT_CONNECT_TIMEOUT_MS = 10_000; + private static final int DEFAULT_READ_TIMEOUT_MS = 10_000; private final HttpTransport httpTransport; private final TelemetryConfiguration config; @@ -49,9 +52,13 @@ class ClearcutTransport { this.requestFactory = this.httpTransport.createRequestFactory(); } - boolean send(TelemetryPayload payload) { + TransportResult send(TelemetryPayload payload) { if (!config.isEnabled()) { - return false; + return TransportResult.disabled(); + } + if (payload == null) { + logger.log(Level.WARNING, "Cannot send null telemetry payload to Clearcut"); + return TransportResult.disabled(); } long now = System.currentTimeMillis(); @@ -70,73 +77,50 @@ boolean send(TelemetryPayload payload) { HttpContent content = new ByteArrayContent(CONTENT_TYPE_PROTOBUF, requestBytes); GenericUrl url = new GenericUrl(config.getEndpointUrl()); - int attempt = 0; - int maxAttempts = 3; - long backoffMs = 1000; + long nextRequestWaitMillis = -1; - while (attempt < maxAttempts) { - attempt++; - boolean retryable = false; + try { + HttpRequest request = requestFactory.buildPostRequest(url, content); + request.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT_MS); + request.setReadTimeout(DEFAULT_READ_TIMEOUT_MS); + request.setThrowExceptionOnExecuteError(false); + HttpResponse response = null; try { - HttpRequest request = requestFactory.buildPostRequest(url, content); - request.setThrowExceptionOnExecuteError(false); - - HttpResponse response = null; - try { - response = request.execute(); - int statusCode = response.getStatusCode(); - if (statusCode >= 200 && statusCode < 300) { - logger.log(Level.FINE, "Successfully uploaded telemetry payload to Clearcut"); - return true; - } else if (isRetryableStatusCode(statusCode)) { - logger.log( - Level.WARNING, - String.format( - "Clearcut upload returned retryable status code: %d on attempt %d", - statusCode, attempt)); - retryable = true; - } else { - logger.log( - Level.WARNING, - String.format( - "Clearcut upload failed with non-retryable status code: %d", statusCode)); - return false; - } - } finally { - if (response != null) { - response.disconnect(); + response = request.execute(); + int statusCode = response.getStatusCode(); + + if (response.getContent() != null) { + try (InputStream is = response.getContent()) { + LogResponse logResponse = LogResponse.parseFrom(is); + if (logResponse.getNextRequestWaitMillis() > 0) { + nextRequestWaitMillis = logResponse.getNextRequestWaitMillis(); + } + } catch (IOException ignored) { + // Ignore non-protobuf content from error bodies } } - } catch (IOException e) { - logger.log( - Level.WARNING, - String.format( - "IOException sending telemetry payload to Clearcut on attempt %d", attempt), - e); - retryable = true; - } - if (retryable && attempt < maxAttempts) { - try { - Thread.sleep(backoffMs); - backoffMs *= 2; - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - logger.log(Level.WARNING, "Backoff thread interrupted", ie); - return false; + boolean success = statusCode >= 200 && statusCode < 300; + if (success) { + logger.log(Level.FINE, "Successfully uploaded telemetry payload to Clearcut"); + } else { + logger.log( + Level.WARNING, + String.format("Clearcut upload failed with status code: %d", statusCode)); + } + return new TransportResult(success, nextRequestWaitMillis); + } finally { + if (response != null) { + response.disconnect(); } } + } catch (IOException e) { + logger.log(Level.WARNING, "IOException sending telemetry payload to Clearcut", e); + return new TransportResult(false, nextRequestWaitMillis); + } catch (Throwable t) { + logger.log(Level.WARNING, "Unexpected error sending telemetry payload to Clearcut", t); + return new TransportResult(false, nextRequestWaitMillis); } - - logger.log( - Level.SEVERE, - String.format( - "Failed to upload telemetry payload to Clearcut after %d attempts", maxAttempts)); - return false; - } - - private static boolean isRetryableStatusCode(int statusCode) { - return statusCode == 429 || (statusCode >= 500 && statusCode < 600); } } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TransportResult.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TransportResult.java new file mode 100644 index 000000000000..a6d28508160b --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TransportResult.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc.telemetry.v1; + +import java.util.Objects; + +/** Represents the immutable result of a Clearcut telemetry payload transmission attempt. */ +final class TransportResult { + private static final TransportResult DISABLED = new TransportResult(false, -1); + + private final boolean success; + private final long nextRequestWaitMillis; + + TransportResult(boolean success, long nextRequestWaitMillis) { + this.success = success; + this.nextRequestWaitMillis = nextRequestWaitMillis; + } + + static TransportResult disabled() { + return DISABLED; + } + + boolean isSuccess() { + return success; + } + + long getNextRequestWaitMillis() { + return nextRequestWaitMillis; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TransportResult that = (TransportResult) o; + return success == that.success && nextRequestWaitMillis == that.nextRequestWaitMillis; + } + + @Override + public int hashCode() { + return Objects.hash(success, nextRequestWaitMillis); + } + + @Override + public String toString() { + return "TransportResult{" + + "success=" + + success + + ", nextRequestWaitMillis=" + + nextRequestWaitMillis + + '}'; + } +} diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java index ac48e7628d2f..26b078c91a8e 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/ClearcutTransportTest.java @@ -46,8 +46,30 @@ public LowLevelHttpRequest buildRequest(String method, String url) { TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(false).build(); ClearcutTransport transport = new ClearcutTransport(mockTransport, config); - boolean result = transport.send(TelemetryPayload.getDefaultInstance()); - assertFalse(result); + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); + assertEquals(0, requestCount.get()); + } + + @Test + public void testSendNullPayloadFailsSilently() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(); + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + TransportResult result = transport.send(null); + assertFalse(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); assertEquals(0, requestCount.get()); } @@ -73,13 +95,14 @@ public LowLevelHttpResponse execute() { TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); ClearcutTransport transport = new ClearcutTransport(mockTransport, config); - boolean result = transport.send(TelemetryPayload.getDefaultInstance()); - assertTrue(result); + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertTrue(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); assertEquals(1, requestCount.get()); } @Test - public void testSendServerErrorStatusCodeRetries() { + public void testSendServerErrorStatusCodeReturnsFalse() { AtomicInteger requestCount = new AtomicInteger(0); MockHttpTransport mockTransport = new MockHttpTransport() { @@ -100,13 +123,14 @@ public LowLevelHttpResponse execute() { TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); ClearcutTransport transport = new ClearcutTransport(mockTransport, config); - boolean result = transport.send(TelemetryPayload.getDefaultInstance()); - assertFalse(result); - assertEquals(3, requestCount.get()); + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); + assertEquals(1, requestCount.get()); } @Test - public void testSendNonRetryableStatusCodeFailsImmediately() { + public void testSendClientErrorStatusCodeReturnsFalse() { AtomicInteger requestCount = new AtomicInteger(0); MockHttpTransport mockTransport = new MockHttpTransport() { @@ -127,13 +151,14 @@ public LowLevelHttpResponse execute() { TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); ClearcutTransport transport = new ClearcutTransport(mockTransport, config); - boolean result = transport.send(TelemetryPayload.getDefaultInstance()); - assertFalse(result); + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); assertEquals(1, requestCount.get()); } @Test - public void testSendIOExceptionRetries() { + public void testSendIOExceptionReturnsFalse() { AtomicInteger requestCount = new AtomicInteger(0); MockHttpTransport mockTransport = new MockHttpTransport() { @@ -147,8 +172,40 @@ public LowLevelHttpRequest buildRequest(String method, String url) throws IOExce TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); ClearcutTransport transport = new ClearcutTransport(mockTransport, config); - boolean result = transport.send(TelemetryPayload.getDefaultInstance()); - assertFalse(result); - assertEquals(3, requestCount.get()); + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result.isSuccess()); + assertEquals(-1, result.getNextRequestWaitMillis()); + assertEquals(1, requestCount.get()); + } + + @Test + public void testSendServerErrorWithNextRequestWaitMillis() { + AtomicInteger requestCount = new AtomicInteger(0); + MockHttpTransport mockTransport = + new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + requestCount.incrementAndGet(); + return new MockLowLevelHttpRequest(url) { + @Override + public LowLevelHttpResponse execute() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(429); + LogResponse logResponse = + LogResponse.newBuilder().setNextRequestWaitMillis(10).build(); + response.setContent(logResponse.toByteArray()); + return response; + } + }; + } + }; + + TelemetryConfiguration config = TelemetryConfiguration.newBuilder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(mockTransport, config); + + TransportResult result = transport.send(TelemetryPayload.getDefaultInstance()); + assertFalse(result.isSuccess()); + assertEquals(1, requestCount.get()); + assertEquals(10, result.getNextRequestWaitMillis()); } }