Skip to content

Commit 67750c6

Browse files
committed
fix: avoid race condition where the BatchSearchRunner would return before receiving the cancel instruction
1 parent 5c34e42 commit 67750c6

File tree

1 file changed

+32
-6
lines changed

1 file changed

+32
-6
lines changed

datashare-app/src/test/java/org/icij/datashare/tasks/BatchSearchRunnerTest.java

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.icij.datashare.asynctasks.CancelException;
55
import org.icij.datashare.asynctasks.Task;
66
import org.icij.datashare.batch.BatchSearch;
7+
import org.icij.datashare.batch.BatchSearchRecord;
78
import org.icij.datashare.batch.BatchSearchRepository;
89
import org.icij.datashare.batch.SearchException;
910
import org.icij.datashare.test.DatashareTimeRule;
@@ -36,8 +37,7 @@
3637
import static org.mockito.ArgumentMatchers.anyBoolean;
3738
import static org.mockito.ArgumentMatchers.anyList;
3839
import static org.mockito.ArgumentMatchers.anyString;
39-
import static org.mockito.Mockito.verify;
40-
import static org.mockito.Mockito.when;
40+
import static org.mockito.Mockito.*;
4141
import static org.mockito.MockitoAnnotations.initMocks;
4242

4343

@@ -123,20 +123,46 @@ public void test_run_batch_search_with_throttle_should_not_last_more_than_max_ti
123123
assertThat(timeRule.now().getTime() - beforeBatch.getTime()).isEqualTo(1000);
124124
}
125125

126+
// To avoid race conditions, this test relies heavily on synchronization.
127+
// The goal is to avoid having the BatchSearchRunner execute itself before receiving the cancel request,
128+
// which happened sometimes on the CI.
126129
@Test
127130
public void test_cancel_current_batch_search() throws Exception {
128-
CountDownLatch countDownLatch = new CountDownLatch(1);
131+
// Given
129132
Document[] documents = {createDoc("doc1").build(), createDoc("doc2").build()};
130133
mockSearch.willReturn(1, documents);
134+
CountDownLatch runnerStarted = new CountDownLatch(1);
131135
BatchSearch search = new BatchSearch("uuid1", singletonList(project("test-datashare")), "name1", "desc1", asSet("query1", "query2"), new Date(), BatchSearch.State.QUEUED, User.local());
132136
when(repository.get(local(), search.uuid)).thenReturn(search);
133-
BatchSearchRunner batchSearchRunner = new BatchSearchRunner(indexer, new PropertiesProvider(), repository, taskView(search), progressCb, countDownLatch);
134137

138+
// The following block will halt the execution of the BatchSearchRunner until it receives the cancel request
139+
CountDownLatch cancelRequestedInRunner = new CountDownLatch(1);
140+
doAnswer(inv -> {
141+
//This will block the execution of the runner
142+
cancelRequestedInRunner.await();
143+
return null;
144+
}).when(repository).setState(any(), eq(BatchSearchRecord.State.RUNNING));
145+
146+
// WHEN
147+
BatchSearchRunner batchSearchRunner = new BatchSearchRunner(indexer, new PropertiesProvider(), repository, taskView(search), progressCb, runnerStarted);
135148
Future<BatchSearchRunnerResult> result = executor.submit(batchSearchRunner);
149+
runnerStarted.await();
136150
executor.shutdown();
137-
countDownLatch.await();
138-
batchSearchRunner.cancel(false);
139151

152+
// BatchSearchRunner.cancel is blocking until the thread returns,
153+
// so it must be started in a new thread to avoid interlocking
154+
Thread cancelThread = new Thread(() -> batchSearchRunner.cancel(false));
155+
cancelThread.start();
156+
157+
//Wait for the batchSearchRunner to receive the instruction that it must be canceled
158+
while (!batchSearchRunner.cancelAsked) { Thread.yield(); }
159+
160+
//BatchSearchRunner received the instruction that it must be canceled : its execution can resume
161+
cancelRequestedInRunner.countDown();
162+
163+
cancelThread.join();
164+
165+
// THEN
140166
assertThat(executor.awaitTermination(2, TimeUnit.SECONDS)).isTrue();
141167
assertThat(assertThrows(ExecutionException.class, result::get).getCause()).isInstanceOf(CancelException.class);
142168
}

0 commit comments

Comments
 (0)