|
| 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