|
17 | 17 | import org.elasticsearch.test.ESTestCase; |
18 | 18 | import org.elasticsearch.threadpool.ThreadPool; |
19 | 19 |
|
| 20 | +import java.time.Duration; |
20 | 21 | import java.util.List; |
21 | 22 | import java.util.concurrent.CountDownLatch; |
22 | 23 | import java.util.concurrent.CyclicBarrier; |
@@ -89,18 +90,90 @@ public void testExecutionEWMACalculation() throws Exception { |
89 | 90 | assertThat(executor.getTotalTaskExecutionTime(), equalTo(500L)); |
90 | 91 | }); |
91 | 92 | assertThat(executor.getOngoingTasks().toString(), executor.getOngoingTasks().size(), equalTo(0)); |
92 | | - executor.shutdown(); |
93 | | - executor.awaitTermination(10, TimeUnit.SECONDS); |
| 93 | + ThreadPool.terminate(executor, 10, TimeUnit.SECONDS); |
94 | 94 | } |
95 | 95 |
|
96 | | - public void testMaxQueueLatency() throws Exception { |
| 96 | + /** |
| 97 | + * Verifies that we can peek at the task in front of the task queue to fetch the duration that the oldest task has been queued. |
| 98 | + * Tests {@link TaskExecutionTimeTrackingEsThreadPoolExecutor#peekMaxQueueLatencyInQueue}. |
| 99 | + */ |
| 100 | + public void testFrontOfQueueLatency() throws Exception { |
97 | 101 | ThreadContext context = new ThreadContext(Settings.EMPTY); |
98 | | - RecordingMeterRegistry meterRegistry = new RecordingMeterRegistry(); |
99 | | - final var threadPoolName = randomIdentifier(); |
100 | 102 | final var barrier = new CyclicBarrier(2); |
| 103 | + // Replace all tasks submitted to the thread pool with a configurable task that supports configuring queue latency durations and |
| 104 | + // waiting for task execution to begin via the supplied barrier. |
101 | 105 | var adjustableTimedRunnable = new AdjustableQueueTimeWithExecutionBarrierTimedRunnable( |
102 | 106 | barrier, |
103 | | - TimeUnit.NANOSECONDS.toNanos(1000000) |
| 107 | + // This won't actually be used, because it is reported after a task is taken off the queue. This test peeks at the still queued |
| 108 | + // tasks. |
| 109 | + TimeUnit.MILLISECONDS.toNanos(1) |
| 110 | + ); |
| 111 | + TaskExecutionTimeTrackingEsThreadPoolExecutor executor = new TaskExecutionTimeTrackingEsThreadPoolExecutor( |
| 112 | + "test-threadpool", |
| 113 | + 1, |
| 114 | + 1, |
| 115 | + 1_000, |
| 116 | + TimeUnit.MILLISECONDS, |
| 117 | + ConcurrentCollections.newBlockingQueue(), |
| 118 | + (runnable) -> adjustableTimedRunnable, |
| 119 | + EsExecutors.daemonThreadFactory("queue-latency-test"), |
| 120 | + new EsAbortPolicy(), |
| 121 | + context, |
| 122 | + randomBoolean() |
| 123 | + ? EsExecutors.TaskTrackingConfig.builder() |
| 124 | + .trackOngoingTasks() |
| 125 | + .trackMaxQueueLatency() |
| 126 | + .trackExecutionTime(DEFAULT_EXECUTION_TIME_EWMA_ALPHA_FOR_TEST) |
| 127 | + .build() |
| 128 | + : EsExecutors.TaskTrackingConfig.builder() |
| 129 | + .trackMaxQueueLatency() |
| 130 | + .trackExecutionTime(DEFAULT_EXECUTION_TIME_EWMA_ALPHA_FOR_TEST) |
| 131 | + .build() |
| 132 | + ); |
| 133 | + try { |
| 134 | + executor.prestartAllCoreThreads(); |
| 135 | + logger.info("--> executor: {}", executor); |
| 136 | + |
| 137 | + // Check that the peeking at a non-existence queue returns zero. |
| 138 | + assertEquals("Zero should be returned when there is no queue", 0, executor.peekMaxQueueLatencyInQueue()); |
| 139 | + |
| 140 | + // Submit two tasks, into the thread pool with a single worker thread. The second one will be queued (because the pool only has |
| 141 | + // one thread) and can be peeked at. |
| 142 | + executor.execute(() -> {}); |
| 143 | + executor.execute(() -> {}); |
| 144 | + |
| 145 | + waitForTimeToElapse(); |
| 146 | + var frontOfQueueDuration = executor.peekMaxQueueLatencyInQueue(); |
| 147 | + assertThat("Expected a task to be queued", frontOfQueueDuration, greaterThan(0L)); |
| 148 | + waitForTimeToElapse(); |
| 149 | + var updatedFrontOfQueueDuration = executor.peekMaxQueueLatencyInQueue(); |
| 150 | + assertThat( |
| 151 | + "Expected a second peek to report a longer duration", |
| 152 | + updatedFrontOfQueueDuration, |
| 153 | + greaterThan(frontOfQueueDuration) |
| 154 | + ); |
| 155 | + |
| 156 | + // Release the first task that's running, and wait for the second to start -- then it is ensured that the queue will be empty. |
| 157 | + safeAwait(barrier); |
| 158 | + safeAwait(barrier); |
| 159 | + assertEquals("Queue should be emptied", 0, executor.peekMaxQueueLatencyInQueue()); |
| 160 | + } finally { |
| 161 | + ThreadPool.terminate(executor, 10, TimeUnit.SECONDS); |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * Verifies that tracking of the max queue latency (captured on task dequeue) is maintained. |
| 167 | + * Tests {@link TaskExecutionTimeTrackingEsThreadPoolExecutor#getMaxQueueLatencyMillisSinceLastPollAndReset()}. |
| 168 | + */ |
| 169 | + public void testMaxDequeuedQueueLatency() throws Exception { |
| 170 | + ThreadContext context = new ThreadContext(Settings.EMPTY); |
| 171 | + final var barrier = new CyclicBarrier(2); |
| 172 | + // Replace all tasks submitted to the thread pool with a configurable task that supports configuring queue latency durations and |
| 173 | + // waiting for task execution to begin via the supplied barrier. |
| 174 | + var adjustableTimedRunnable = new AdjustableQueueTimeWithExecutionBarrierTimedRunnable( |
| 175 | + barrier, |
| 176 | + TimeUnit.NANOSECONDS.toNanos(1000000) // Until changed, queue latencies will always be 1 millisecond. |
104 | 177 | ); |
105 | 178 | TaskExecutionTimeTrackingEsThreadPoolExecutor executor = new TaskExecutionTimeTrackingEsThreadPoolExecutor( |
106 | 179 | "test-threadpool", |
@@ -145,9 +218,7 @@ public void testMaxQueueLatency() throws Exception { |
145 | 218 | assertEquals("Max should not be the last task", 5, executor.getMaxQueueLatencyMillisSinceLastPollAndReset()); |
146 | 219 | assertEquals("The max was just reset, should be zero", 0, executor.getMaxQueueLatencyMillisSinceLastPollAndReset()); |
147 | 220 | } finally { |
148 | | - // Clean up. |
149 | | - executor.shutdown(); |
150 | | - executor.awaitTermination(10, TimeUnit.SECONDS); |
| 221 | + ThreadPool.terminate(executor, 10, TimeUnit.SECONDS); |
151 | 222 | } |
152 | 223 | } |
153 | 224 |
|
@@ -184,8 +255,7 @@ public void testExceptionThrowingTask() throws Exception { |
184 | 255 | assertThat(executor.getTotalTaskExecutionTime(), equalTo(0L)); |
185 | 256 | assertThat(executor.getActiveCount(), equalTo(0)); |
186 | 257 | assertThat(executor.getOngoingTasks().toString(), executor.getOngoingTasks().size(), equalTo(0)); |
187 | | - executor.shutdown(); |
188 | | - executor.awaitTermination(10, TimeUnit.SECONDS); |
| 258 | + ThreadPool.terminate(executor, 10, TimeUnit.SECONDS); |
189 | 259 | } |
190 | 260 |
|
191 | 261 | public void testGetOngoingTasks() throws Exception { |
@@ -222,8 +292,7 @@ public void testGetOngoingTasks() throws Exception { |
222 | 292 | exitTaskLatch.countDown(); |
223 | 293 | assertBusy(() -> assertThat(executor.getOngoingTasks().toString(), executor.getOngoingTasks().size(), equalTo(0))); |
224 | 294 | assertThat(executor.getTotalTaskExecutionTime(), greaterThan(0L)); |
225 | | - executor.shutdown(); |
226 | | - executor.awaitTermination(10, TimeUnit.SECONDS); |
| 295 | + ThreadPool.terminate(executor, 10, TimeUnit.SECONDS); |
227 | 296 | } |
228 | 297 |
|
229 | 298 | public void testQueueLatencyHistogramMetrics() { |
@@ -387,4 +456,15 @@ long getQueueTimeNanos() { |
387 | 456 | return queuedTimeTakenNanos; |
388 | 457 | } |
389 | 458 | } |
| 459 | + |
| 460 | + /** |
| 461 | + * Ensures that the time reported by {@code System.nanoTime()} has advanced. It is otherwise feasible for the clock to report no time |
| 462 | + * passing between operations. Call this method if time passing must be guaranteed. |
| 463 | + */ |
| 464 | + private static void waitForTimeToElapse() throws InterruptedException { |
| 465 | + final var startNanoTime = System.nanoTime(); |
| 466 | + while ((System.nanoTime() - startNanoTime) < 1) { |
| 467 | + Thread.sleep(Duration.ofNanos(1)); |
| 468 | + } |
| 469 | + } |
390 | 470 | } |
0 commit comments