|
9 | 9 |
|
10 | 10 | import org.apache.lucene.search.TotalHits; |
11 | 11 | import org.elasticsearch.action.ActionListener; |
| 12 | +import org.elasticsearch.action.LatchedActionListener; |
12 | 13 | import org.elasticsearch.action.admin.indices.refresh.RefreshResponse; |
13 | 14 | import org.elasticsearch.action.bulk.BulkItemResponse; |
14 | 15 | import org.elasticsearch.action.bulk.BulkRequest; |
|
53 | 54 | import java.util.Map; |
54 | 55 | import java.util.concurrent.CountDownLatch; |
55 | 56 | import java.util.concurrent.TimeUnit; |
| 57 | +import java.util.concurrent.atomic.AtomicBoolean; |
56 | 58 | import java.util.concurrent.atomic.AtomicReference; |
57 | 59 | import java.util.function.Consumer; |
58 | 60 |
|
@@ -98,6 +100,9 @@ class MockedTransformIndexer extends TransformIndexer { |
98 | 100 | // used for synchronizing with the test |
99 | 101 | private CountDownLatch searchLatch; |
100 | 102 | private CountDownLatch doProcessLatch; |
| 103 | + private CountDownLatch doSaveStateLatch; |
| 104 | + |
| 105 | + private AtomicBoolean saveStateInProgress = new AtomicBoolean(false); |
101 | 106 |
|
102 | 107 | // how many loops to execute until reporting done |
103 | 108 | private int numberOfLoops; |
@@ -146,6 +151,10 @@ public CountDownLatch createCountDownOnResponseLatch(int count) { |
146 | 151 | return doProcessLatch = new CountDownLatch(count); |
147 | 152 | } |
148 | 153 |
|
| 154 | + public CountDownLatch createAwaitForDoSaveStateLatch(int count) { |
| 155 | + return doSaveStateLatch = new CountDownLatch(count); |
| 156 | + } |
| 157 | + |
149 | 158 | @Override |
150 | 159 | void doGetInitialProgress(SearchRequest request, ActionListener<SearchResponse> responseListener) { |
151 | 160 | responseListener.onResponse(ONE_HIT_SEARCH_RESPONSE); |
@@ -214,7 +223,21 @@ protected void doNextBulk(BulkRequest request, ActionListener<BulkResponse> next |
214 | 223 |
|
215 | 224 | @Override |
216 | 225 | protected void doSaveState(IndexerState state, TransformIndexerPosition position, Runnable next) { |
| 226 | + // assert that the indexer does not call doSaveState again, while it is still saving state |
| 227 | + // this is only useful together with the doSaveStateLatch |
| 228 | + assertTrue("doSaveState called again while still in progress", saveStateInProgress.compareAndSet(false, true)); |
| 229 | + if (doSaveStateLatch != null) { |
| 230 | + try { |
| 231 | + doSaveStateLatch.await(); |
| 232 | + |
| 233 | + } catch (InterruptedException e) { |
| 234 | + throw new IllegalStateException(e); |
| 235 | + } |
| 236 | + } |
| 237 | + |
217 | 238 | assert state == IndexerState.STARTED || state == IndexerState.INDEXING || state == IndexerState.STOPPED; |
| 239 | + |
| 240 | + assertTrue(saveStateInProgress.compareAndSet(true, false)); |
218 | 241 | next.run(); |
219 | 242 | } |
220 | 243 |
|
@@ -288,7 +311,7 @@ public void testRetentionPolicyExecution() throws Exception { |
288 | 311 | assertTrue(indexer.maybeTriggerAsyncJob(System.currentTimeMillis())); |
289 | 312 | assertThat(indexer.getState(), oneOf(IndexerState.INDEXING, IndexerState.STARTED)); |
290 | 313 |
|
291 | | - assertBusy(() -> assertEquals(1L, indexer.getLastCheckpoint().getCheckpoint()), 5, TimeUnit.HOURS); |
| 314 | + assertBusy(() -> assertEquals(1L, indexer.getLastCheckpoint().getCheckpoint()), 5, TimeUnit.SECONDS); |
292 | 315 |
|
293 | 316 | // delete by query has been executed |
294 | 317 | assertEquals(1, indexer.getDeleteByQueryCallCount()); |
@@ -340,6 +363,63 @@ public void testRetentionPolicyExecution() throws Exception { |
340 | 363 | } |
341 | 364 | } |
342 | 365 |
|
| 366 | + /** |
| 367 | + * This test ensures correct handling of async behavior during indexer shutdown |
| 368 | + * |
| 369 | + * Indexer shutdown is not atomic: 1st the state is set back to e.g. STARTED, afterwards state is stored. |
| 370 | + * State is stored async and is IO based, therefore it can take time until this is done. |
| 371 | + * |
| 372 | + * Between setting the state and storing it, some race condition occurred, this test acts |
| 373 | + * as regression test. |
| 374 | + */ |
| 375 | + public void testInterActionWhileIndexerShutsdown() throws Exception { |
| 376 | + TransformConfig config = new TransformConfig( |
| 377 | + randomAlphaOfLength(10), |
| 378 | + randomSourceConfig(), |
| 379 | + randomDestConfig(), |
| 380 | + null, |
| 381 | + new TimeSyncConfig("timestamp", TimeValue.timeValueSeconds(1)), |
| 382 | + null, |
| 383 | + randomPivotConfig(), |
| 384 | + null, |
| 385 | + randomBoolean() ? null : randomAlphaOfLengthBetween(1, 1000), |
| 386 | + null, |
| 387 | + null, |
| 388 | + null, |
| 389 | + null |
| 390 | + ); |
| 391 | + AtomicReference<IndexerState> state = new AtomicReference<>(IndexerState.STARTED); |
| 392 | + |
| 393 | + TransformContext context = new TransformContext(TransformTaskState.STARTED, "", 0, mock(TransformContext.Listener.class)); |
| 394 | + final MockedTransformIndexer indexer = createMockIndexer( |
| 395 | + 5, |
| 396 | + config, |
| 397 | + state, |
| 398 | + null, |
| 399 | + threadPool, |
| 400 | + auditor, |
| 401 | + new TransformIndexerStats(), |
| 402 | + context |
| 403 | + ); |
| 404 | + |
| 405 | + // add a latch at doSaveState |
| 406 | + CountDownLatch saveStateLatch = indexer.createAwaitForDoSaveStateLatch(1); |
| 407 | + |
| 408 | + indexer.start(); |
| 409 | + assertTrue(indexer.maybeTriggerAsyncJob(System.currentTimeMillis())); |
| 410 | + assertEquals(indexer.getState(), IndexerState.INDEXING); |
| 411 | + |
| 412 | + assertBusy(() -> assertEquals(IndexerState.STARTED, indexer.getState()), 5, TimeUnit.SECONDS); |
| 413 | + |
| 414 | + // the indexer thread is shutting down, the trigger should be ignored |
| 415 | + assertFalse(indexer.maybeTriggerAsyncJob(System.currentTimeMillis())); |
| 416 | + this.<Void>assertAsync(listener -> setStopAtCheckpoint(indexer, true, listener), v -> {}); |
| 417 | + saveStateLatch.countDown(); |
| 418 | + |
| 419 | + // after the indexer has shutdown, it should check for stop at checkpoint and shutdown |
| 420 | + assertBusy(() -> assertEquals(IndexerState.STOPPED, indexer.getState()), 5, TimeUnit.SECONDS); |
| 421 | + } |
| 422 | + |
343 | 423 | private MockedTransformIndexer createMockIndexer( |
344 | 424 | int numberOfLoops, |
345 | 425 | TransformConfig config, |
@@ -370,4 +450,36 @@ private MockedTransformIndexer createMockIndexer( |
370 | 450 | indexer.initialize(); |
371 | 451 | return indexer; |
372 | 452 | } |
| 453 | + |
| 454 | + private void setStopAtCheckpoint( |
| 455 | + TransformIndexer indexer, |
| 456 | + boolean shouldStopAtCheckpoint, |
| 457 | + ActionListener<Void> shouldStopAtCheckpointListener |
| 458 | + ) { |
| 459 | + // we need to simulate that this is called from the task, which offloads it to the generic threadpool |
| 460 | + CountDownLatch latch = new CountDownLatch(1); |
| 461 | + threadPool.executor(ThreadPool.Names.GENERIC).execute(() -> { |
| 462 | + indexer.setStopAtCheckpoint(shouldStopAtCheckpoint, shouldStopAtCheckpointListener); |
| 463 | + latch.countDown(); |
| 464 | + }); |
| 465 | + try { |
| 466 | + assertTrue("timed out after 5s", latch.await(5, TimeUnit.SECONDS)); |
| 467 | + } catch (InterruptedException e) { |
| 468 | + fail("timed out after 5s"); |
| 469 | + } |
| 470 | + } |
| 471 | + |
| 472 | + private <T> void assertAsync(Consumer<ActionListener<T>> function, Consumer<T> furtherTests) throws InterruptedException { |
| 473 | + CountDownLatch latch = new CountDownLatch(1); |
| 474 | + AtomicBoolean listenerCalled = new AtomicBoolean(false); |
| 475 | + |
| 476 | + LatchedActionListener<T> listener = new LatchedActionListener<>(ActionListener.wrap(r -> { |
| 477 | + assertTrue("listener called more than once", listenerCalled.compareAndSet(false, true)); |
| 478 | + furtherTests.accept(r); |
| 479 | + }, e -> { fail("got unexpected exception: " + e); }), latch); |
| 480 | + |
| 481 | + function.accept(listener); |
| 482 | + assertTrue("timed out after 5s", latch.await(5, TimeUnit.SECONDS)); |
| 483 | + } |
| 484 | + |
373 | 485 | } |
0 commit comments