|
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