Skip to content

Commit 6d61480

Browse files
authored
feat(bigquery-jdbc): implement automated MDC executor context propagation and decouple daemon threads (googleapis#13374)
b/519201498 This PR implements automated MDC logging context propagation across threads spawned by execution services in the BigQuery JDBC driver and decouples JVM-wide background garbage collection monitoring from statement executors. It also cleans up test execution by routing unit-test logging files into a dedicated sub-directory. ## Detailed Changes ### 1. MDC Propagation Infrastructure (`BigQueryJdbcMdc.java`) - Added `MdcThreadPoolExecutor` and `MdcFutureTask` implementation that automatically intercepts calls to `execute` and `newTaskFor` to capture the connection's MDC context (`connectionId`) and restore it on worker threads. - Added static helper factory methods like `newFixedThreadPool(...)` to allow creation of MDC-propagating executor pools. ### 2. GC Daemon Decoupling (`BigQueryDaemonPollingTask.java`) - Decoupled `BigQueryDaemonPollingTask` tasks (which monitor Arrow and Json references for JVM-wide cleanup) from the connection statement executor pool. - Changed task initialization to spawn dedicated daemon threads (`BigQuery-GC-Daemon-Arrow` and `BigQuery-GC-Daemon-Json`) directly, ensuring GC work does not consume execution pool resources. ### 3. Unit Testing (`BigQueryJdbcMdcTest.java`) - Added a full test suite validating transparent MDC connection context propagation and verifying that contexts are properly cleared from threads on completion/re-use.
1 parent 4159d2d commit 6d61480

4 files changed

Lines changed: 239 additions & 2 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class BigQueryDaemonPollingTask extends Thread {
4545
private BigQueryDaemonPollingTask(
4646
List<BigQueryResultSetFinalizers.ArrowResultSetFinalizer> arrowRsFinalizers,
4747
ReferenceQueue<BigQueryArrowResultSet> referenceQueueArrowRs) {
48+
super("BigQuery-GC-Daemon-Arrow");
4849
BigQueryDaemonPollingTask.referenceQueueArrowRs = referenceQueueArrowRs;
4950
BigQueryDaemonPollingTask.arrowRsFinalizers = arrowRsFinalizers;
5051
setDaemon(true);
@@ -53,6 +54,7 @@ private BigQueryDaemonPollingTask(
5354
private BigQueryDaemonPollingTask(
5455
ReferenceQueue<BigQueryJsonResultSet> referenceQueueJsonRs,
5556
List<BigQueryResultSetFinalizers.JsonResultSetFinalizer> jsonRsFinalizers) {
57+
super("BigQuery-GC-Daemon-Json");
5658
BigQueryDaemonPollingTask.referenceQueueJsonRs = referenceQueueJsonRs;
5759
BigQueryDaemonPollingTask.jsonRsFinalizers = jsonRsFinalizers;
5860
setDaemon(true);

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcMdc.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,17 @@
1616

1717
package com.google.cloud.bigquery.jdbc;
1818

19+
import java.util.concurrent.BlockingQueue;
20+
import java.util.concurrent.Callable;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.concurrent.Executors;
23+
import java.util.concurrent.FutureTask;
24+
import java.util.concurrent.LinkedBlockingQueue;
25+
import java.util.concurrent.RunnableFuture;
26+
import java.util.concurrent.ThreadFactory;
27+
import java.util.concurrent.ThreadPoolExecutor;
28+
import java.util.concurrent.TimeUnit;
29+
1930
/** Lightweight MDC implementation for the BigQuery JDBC driver using InheritableThreadLocal. */
2031
class BigQueryJdbcMdc {
2132
private static final InheritableThreadLocal<String> currentConnectionId =
@@ -40,6 +51,110 @@ static void clear() {
4051
currentConnectionId.remove();
4152
}
4253

54+
/**
55+
* Creates a new fixed thread pool ExecutorService that automatically propagates MDC connection
56+
* context from the submitting thread to the executing thread.
57+
*/
58+
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
59+
return new MdcThreadPoolExecutor(
60+
nThreads,
61+
nThreads,
62+
0L,
63+
TimeUnit.MILLISECONDS,
64+
new LinkedBlockingQueue<>(),
65+
new MdcThreadFactory(threadFactory));
66+
}
67+
68+
/**
69+
* Creates a new fixed thread pool ExecutorService that automatically propagates MDC connection
70+
* context from the submitting thread to the executing thread.
71+
*/
72+
static ExecutorService newFixedThreadPool(int nThreads) {
73+
return newFixedThreadPool(nThreads, Executors.defaultThreadFactory());
74+
}
75+
76+
private static class MdcThreadFactory implements ThreadFactory {
77+
private final ThreadFactory delegate;
78+
79+
public MdcThreadFactory(ThreadFactory delegate) {
80+
this.delegate = delegate;
81+
}
82+
83+
@Override
84+
public Thread newThread(Runnable r) {
85+
return delegate.newThread(
86+
() -> {
87+
clear();
88+
r.run();
89+
});
90+
}
91+
}
92+
93+
private static class MdcThreadPoolExecutor extends ThreadPoolExecutor {
94+
95+
public MdcThreadPoolExecutor(
96+
int corePoolSize,
97+
int maximumPoolSize,
98+
long keepAliveTime,
99+
TimeUnit unit,
100+
BlockingQueue<Runnable> workQueue,
101+
ThreadFactory threadFactory) {
102+
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
103+
}
104+
105+
@Override
106+
public void execute(Runnable command) {
107+
if (command == null) {
108+
throw new NullPointerException();
109+
}
110+
if (command instanceof MdcFutureTask) {
111+
super.execute(command);
112+
} else {
113+
super.execute(wrap(command));
114+
}
115+
}
116+
117+
@Override
118+
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
119+
return new MdcFutureTask<>(runnable, value, getConnectionId());
120+
}
121+
122+
@Override
123+
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
124+
return new MdcFutureTask<>(callable, getConnectionId());
125+
}
126+
127+
private static Runnable wrap(Runnable runnable) {
128+
String connectionId = getConnectionId();
129+
return () -> {
130+
try (MdcCloseable mdc = registerInstance(connectionId)) {
131+
runnable.run();
132+
}
133+
};
134+
}
135+
}
136+
137+
private static class MdcFutureTask<V> extends FutureTask<V> {
138+
private final String connectionId;
139+
140+
public MdcFutureTask(Runnable runnable, V result, String connectionId) {
141+
super(runnable, result);
142+
this.connectionId = connectionId;
143+
}
144+
145+
public MdcFutureTask(Callable<V> callable, String connectionId) {
146+
super(callable);
147+
this.connectionId = connectionId;
148+
}
149+
150+
@Override
151+
public void run() {
152+
try (MdcCloseable mdc = registerInstance(connectionId)) {
153+
super.run();
154+
}
155+
}
156+
}
157+
43158
/**
44159
* Functional interface that extends AutoCloseable to avoid throwing checked exceptions in
45160
* try-with-resources.

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDriverTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public void testUnknownPropertyWarningIsLogged() throws SQLException {
112112
Connection connection =
113113
bigQueryDriver.connect(
114114
"jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"
115-
+ "OAuthType=2;OAuthAccessToken=redactedToken;ProjectId=t;LogLevel=3;"
115+
+ "OAuthType=2;OAuthAccessToken=redactedToken;ProjectId=t;LogLevel=3;LogPath=logs/;"
116116
+ "MyUnknownSetting=Value",
117117
new Properties());
118118
assertThat(connection.isClosed()).isFalse();
@@ -134,7 +134,7 @@ public void testMalformedUrlExceptionIsLogged() {
134134
() ->
135135
bigQueryDriver.connect(
136136
"jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;"
137-
+ "OAuthType=2;OAuthAccessToken=redactedToken;ProjectId=t;LogLevel=3;"
137+
+ "OAuthType=2;OAuthAccessToken=redactedToken;ProjectId=t;LogLevel=3;LogPath=logs/;"
138138
+ "MalformedPropertyWithoutEquals",
139139
new Properties()));
140140

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcMdcTest.java

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@
1818

1919
import static org.junit.jupiter.api.Assertions.assertEquals;
2020
import static org.junit.jupiter.api.Assertions.assertNull;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
2122
import static org.junit.jupiter.api.Assertions.assertTrue;
2223

2324
import java.util.concurrent.CountDownLatch;
25+
import java.util.concurrent.ExecutorService;
26+
import java.util.concurrent.Future;
27+
import java.util.concurrent.FutureTask;
28+
import java.util.concurrent.RunnableFuture;
2429
import java.util.concurrent.TimeUnit;
2530
import java.util.concurrent.atomic.AtomicReference;
2631
import org.junit.jupiter.api.AfterEach;
@@ -131,4 +136,119 @@ public void testMdcCloseableClearsContext() {
131136
}
132137
assertNull(BigQueryJdbcMdc.getConnectionId());
133138
}
139+
140+
@Test
141+
public void testExecutorPropagatesMdc() throws Exception {
142+
BigQueryJdbcMdc.registerInstance("JdbcConnection-Executor-Test");
143+
ExecutorService executor = BigQueryJdbcMdc.newFixedThreadPool(2);
144+
145+
try {
146+
// Test Runnable submission
147+
CountDownLatch runnableLatch = new CountDownLatch(1);
148+
AtomicReference<String> runnableMdcVal = new AtomicReference<>();
149+
executor.execute(
150+
() -> {
151+
runnableMdcVal.set(BigQueryJdbcMdc.getConnectionId());
152+
runnableLatch.countDown();
153+
});
154+
assertTrue(runnableLatch.await(5, TimeUnit.SECONDS));
155+
assertEquals("JdbcConnection-Executor-Test", runnableMdcVal.get());
156+
157+
// Test Callable submission
158+
Future<String> callableFuture =
159+
executor.submit(
160+
() -> {
161+
return BigQueryJdbcMdc.getConnectionId();
162+
});
163+
assertEquals("JdbcConnection-Executor-Test", callableFuture.get(5, TimeUnit.SECONDS));
164+
165+
// Test context is cleared on worker thread after task completion
166+
CountDownLatch cleanupLatch = new CountDownLatch(1);
167+
AtomicReference<String> postTaskMdcVal = new AtomicReference<>("initial-non-null");
168+
executor.execute(
169+
() -> {
170+
// Run a task on a potentially reused thread
171+
postTaskMdcVal.set(BigQueryJdbcMdc.getConnectionId());
172+
cleanupLatch.countDown();
173+
});
174+
assertTrue(cleanupLatch.await(5, TimeUnit.SECONDS));
175+
// Since the main thread has context set, the second task will also capture and set it.
176+
// Let's clear the context on the main thread, and submit a task to check if the thread-local
177+
// is clean for a thread that has previously executed tasks.
178+
BigQueryJdbcMdc.clear();
179+
CountDownLatch cleanMainLatch = new CountDownLatch(1);
180+
AtomicReference<String> cleanThreadMdcVal = new AtomicReference<>("initial-non-null");
181+
executor.execute(
182+
() -> {
183+
cleanThreadMdcVal.set(BigQueryJdbcMdc.getConnectionId());
184+
cleanMainLatch.countDown();
185+
});
186+
assertTrue(cleanMainLatch.await(5, TimeUnit.SECONDS));
187+
// It should be null because the submitting thread had no context set,
188+
// and the previous task's close() call cleaned the ThreadLocal.
189+
assertNull(cleanThreadMdcVal.get());
190+
191+
} finally {
192+
executor.shutdownNow();
193+
}
194+
}
195+
196+
@Test
197+
public void testExecutorThrowsNpeOnNullCommand() {
198+
ExecutorService executor = BigQueryJdbcMdc.newFixedThreadPool(2);
199+
try {
200+
assertThrows(NullPointerException.class, () -> executor.execute(null));
201+
} finally {
202+
executor.shutdownNow();
203+
}
204+
}
205+
206+
@Test
207+
public void testExecutorWrapsCustomRunnableFuture() throws Exception {
208+
BigQueryJdbcMdc.registerInstance("JdbcConnection-CustomFuture-Test");
209+
ExecutorService executor = BigQueryJdbcMdc.newFixedThreadPool(2);
210+
try {
211+
CountDownLatch latch = new CountDownLatch(1);
212+
AtomicReference<String> mdcVal = new AtomicReference<>();
213+
RunnableFuture<Void> customFuture =
214+
new FutureTask<>(
215+
() -> {
216+
mdcVal.set(BigQueryJdbcMdc.getConnectionId());
217+
latch.countDown();
218+
},
219+
null);
220+
221+
executor.execute(customFuture);
222+
assertTrue(latch.await(5, TimeUnit.SECONDS));
223+
assertEquals("JdbcConnection-CustomFuture-Test", mdcVal.get());
224+
} finally {
225+
executor.shutdownNow();
226+
}
227+
}
228+
229+
@Test
230+
public void testPoolThreadInheritanceSevered() throws Exception {
231+
BigQueryJdbcMdc.registerInstance("JdbcConnection-ParentContext");
232+
ExecutorService executor = BigQueryJdbcMdc.newFixedThreadPool(1);
233+
try {
234+
CountDownLatch initLatch = new CountDownLatch(1);
235+
executor.execute(initLatch::countDown);
236+
assertTrue(initLatch.await(5, TimeUnit.SECONDS));
237+
238+
BigQueryJdbcMdc.clear();
239+
240+
CountDownLatch taskLatch = new CountDownLatch(1);
241+
AtomicReference<String> workerMdcVal = new AtomicReference<>("initial-non-null");
242+
executor.execute(
243+
() -> {
244+
workerMdcVal.set(BigQueryJdbcMdc.getConnectionId());
245+
taskLatch.countDown();
246+
});
247+
248+
assertTrue(taskLatch.await(5, TimeUnit.SECONDS));
249+
assertNull(workerMdcVal.get());
250+
} finally {
251+
executor.shutdownNow();
252+
}
253+
}
134254
}

0 commit comments

Comments
 (0)