Skip to content

Commit 0c73efb

Browse files
SNOW-2159000 Allow thread pool to scale properly (#2231)
Co-authored-by: Mikołaj Kubik <mikolaj.kubik@snowflake.com>
1 parent 9acf849 commit 0c73efb

3 files changed

Lines changed: 107 additions & 6 deletions

File tree

src/main/java/net/snowflake/client/jdbc/telemetry/TelemetryClient.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import java.util.concurrent.Future;
1313
import net.snowflake.client.core.HttpUtil;
1414
import net.snowflake.client.core.ObjectMapperFactory;
15-
import net.snowflake.client.core.SFBaseSession;
1615
import net.snowflake.client.core.SFSession;
1716
import net.snowflake.client.jdbc.SnowflakeConnectionV1;
1817
import net.snowflake.client.jdbc.SnowflakeSQLException;
@@ -27,7 +26,7 @@
2726

2827
/** Telemetry Service Interface */
2928
public class TelemetryClient implements Telemetry {
30-
private static final SFLogger logger = SFLoggerFactory.getLogger(SFBaseSession.class);
29+
private static final SFLogger logger = SFLoggerFactory.getLogger(TelemetryClient.class);
3130

3231
private static final String SF_PATH_TELEMETRY = "/telemetry/send";
3332
private static final String SF_PATH_TELEMETRY_SESSIONLESS = "/telemetry/send/sessionless";

src/main/java/net/snowflake/client/jdbc/telemetryOOB/TelemetryThreadPool.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
* asynchronously to server
1313
*/
1414
public class TelemetryThreadPool {
15-
private ExecutorService uploader;
15+
private static final int CORE_POOL_SIZE = 10;
16+
private final ExecutorService uploader;
1617

1718
private static TelemetryThreadPool instance;
1819

@@ -27,15 +28,27 @@ public static TelemetryThreadPool getInstance() {
2728
return instance;
2829
}
2930

31+
/**
32+
* Private constructor to initialize the singleton instance.
33+
*
34+
* <p>Configures a thread pool that scales dynamically based on workload. The pool starts with
35+
* zero threads and will create new threads on demand up to a maximum of 10. If all 10 threads are
36+
* active, new tasks are placed in an unbounded queue to await execution.
37+
*
38+
* <p>To conserve resources, threads that are idle for more than 30 seconds are terminated,
39+
* allowing the pool to shrink back to zero during periods of inactivity.
40+
*/
3041
private TelemetryThreadPool() {
3142
uploader =
3243
new ThreadPoolExecutor(
33-
0, // core size
34-
10, // max size
35-
1, // keep alive time
44+
CORE_POOL_SIZE, // core size
45+
CORE_POOL_SIZE, // max size
46+
30L, // keep alive time
3647
TimeUnit.SECONDS,
3748
new LinkedBlockingQueue<>() // work queue
3849
);
50+
// Allow core threads to time out and be terminated when idle.
51+
((ThreadPoolExecutor) uploader).allowCoreThreadTimeOut(true);
3952
}
4053

4154
public void execute(Runnable task) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package net.snowflake.client.jdbc.telemetryOOB;
2+
3+
import static org.awaitility.Awaitility.await;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
6+
import java.lang.reflect.Field;
7+
import java.util.Collections;
8+
import java.util.Set;
9+
import java.util.concurrent.ConcurrentHashMap;
10+
import java.util.concurrent.CountDownLatch;
11+
import java.util.concurrent.ThreadPoolExecutor;
12+
import java.util.concurrent.TimeUnit;
13+
import net.snowflake.client.category.TestTags;
14+
import org.junit.jupiter.api.Tag;
15+
import org.junit.jupiter.api.Test;
16+
17+
@Tag(TestTags.OTHERS)
18+
class TelemetryThreadPoolTest {
19+
20+
/**
21+
* This test confirms fixing the issue where the TelemetryThreadPool did not scale up to its
22+
* maximum pool size due to its configuration with a core pool size of 0 and an unbounded work
23+
* queue (LinkedBlockingQueue). https://github.com/snowflakedb/snowflake-jdbc/issues/2219
24+
*
25+
* <p>This test submits 100 tasks that each sleep for a short duration. If the pool were scaling,
26+
* it would create up to 10 threads to handle the load concurrently. Previously only a single
27+
* thread was ever created to process all tasks sequentially from the queue.
28+
*
29+
* @throws InterruptedException if the waiting thread is interrupted.
30+
*/
31+
@Test
32+
void testThreadPoolScaling() throws InterruptedException {
33+
TelemetryThreadPool telemetryPool = TelemetryThreadPool.getInstance();
34+
int taskCount = 100; // Submit more tasks than the max pool size (10)
35+
ThreadPoolExecutor executor = getThreadPoolExecutor(telemetryPool);
36+
37+
final Set<Long> uniqueThreadIds = Collections.newSetFromMap(new ConcurrentHashMap<>());
38+
final CountDownLatch completionLatch = new CountDownLatch(taskCount);
39+
40+
// Submit a burst of tasks. These tasks will be queued up and the pool should scale up to handle
41+
// them
42+
for (int i = 0; i < taskCount; i++) {
43+
telemetryPool.execute(
44+
() -> {
45+
uniqueThreadIds.add(Thread.currentThread().getId());
46+
try {
47+
Thread.sleep(10);
48+
} catch (InterruptedException e) {
49+
Thread.currentThread().interrupt();
50+
} finally {
51+
completionLatch.countDown();
52+
}
53+
});
54+
}
55+
56+
// Wait for all tasks to finish processing.
57+
completionLatch.await(5, TimeUnit.SECONDS);
58+
59+
assertEquals(
60+
10,
61+
uniqueThreadIds.size(),
62+
"The thread pool should have used 10 threads, but it used " + uniqueThreadIds.size());
63+
assertEquals(10, executor.getPoolSize());
64+
65+
executor.setKeepAliveTime(
66+
1L, TimeUnit.MILLISECONDS); // Reset keep-alive time to 0 to allow threads to terminate.
67+
68+
await()
69+
.atMost(5, TimeUnit.SECONDS) // Max time to wait for the condition
70+
.pollInterval(100, TimeUnit.MILLISECONDS) // Check every 100ms
71+
.untilAsserted(
72+
() -> {
73+
assertEquals(
74+
0,
75+
executor.getPoolSize(),
76+
"The thread pool should have scaled down to 0 idle threads.");
77+
});
78+
}
79+
80+
private ThreadPoolExecutor getThreadPoolExecutor(TelemetryThreadPool telemetryThreadPool) {
81+
try {
82+
Field uploaderField = TelemetryThreadPool.class.getDeclaredField("uploader");
83+
uploaderField.setAccessible(true);
84+
return (ThreadPoolExecutor) uploaderField.get(telemetryThreadPool);
85+
} catch (NoSuchFieldException | IllegalAccessException e) {
86+
throw new RuntimeException("Failed to access the uploader field in TelemetryThreadPool", e);
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)