Skip to content

Commit 3fe8d77

Browse files
authored
Merge pull request #549 from weaviate/test/fix-ssb-deadlock
test: Await batch items to be acked
2 parents 9ab63b3 + 86f198c commit 3fe8d77

1 file changed

Lines changed: 64 additions & 20 deletions

File tree

src/test/java/io/weaviate/client6/v1/api/collections/batch/BatchContextTest.java

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
import org.assertj.core.api.Assertions;
2121
import org.junit.After;
2222
import org.junit.Before;
23+
import org.junit.Rule;
2324
import org.junit.Test;
25+
import org.junit.rules.TestName;
26+
import org.junit.rules.TestWatcher;
27+
import org.junit.runner.Description;
2428
import org.slf4j.Logger;
2529
import org.slf4j.LoggerFactory;
2630

@@ -95,13 +99,27 @@ private StreamObserver<Message> createStream(StreamObserver<Event> recv) {
9599
return in;
96100
}
97101

102+
@Rule
103+
public TestName currentTest = new TestName();
104+
105+
private boolean testFailed;
106+
107+
@Rule
108+
public TestWatcher __ = new TestWatcher() {
109+
@Override
110+
protected void failed(Throwable e, Description description) {
111+
testFailed = true;
112+
}
113+
};
114+
98115
/**
99116
* Create new unstarted context with default maxSizeBytes, collection
100117
* descriptor, and collection handle defaults.
101118
*/
102119
@Before
103120
public void startContext() throws InterruptedException {
104121
log.debug("===================startContext==================");
122+
log.debug(currentTest.getMethodName());
105123

106124
assert !Thread.currentThread().isInterrupted() : "main thread interrupted";
107125
assert REQUEST_QUEUE.isEmpty() : "stream contains incoming message " + REQUEST_QUEUE.peek();
@@ -117,12 +135,21 @@ public void startContext() throws InterruptedException {
117135
context.start();
118136

119137
in.expectMessage(START);
120-
out.emitEvent(Event.STARTED);
138+
out.emitEventAsync(Event.STARTED);
121139
}
122140

123141
@After
124142
public void reset() throws Exception {
125-
if (!contextClosed) {
143+
log.atDebug()
144+
.addKeyValue("contextClosed", contextClosed)
145+
.addKeyValue("testFailed", testFailed)
146+
.log("Begin test cleanup");
147+
148+
// Do not attempt to close the context if it has been previously closed
149+
// by the test or the test has failed. In the latter case closing the
150+
// context may lead to a deadlock if the case hasn't scheduled Results
151+
// for all submitted messages.
152+
if (!contextClosed && !testFailed) {
126153
closeContext();
127154
}
128155

@@ -159,7 +186,7 @@ private void closeContext() throws Exception {
159186

160187
try {
161188
context.close();
162-
eof.get();
189+
eof.get(5, TimeUnit.SECONDS);
163190
} finally {
164191
contextClosed = true;
165192
}
@@ -175,17 +202,21 @@ public void test_sendOneBatch() throws Exception {
175202
// BatchContext should flush the current batch once it hits its limit.
176203
// We will ack all items in the batch and send successful result for each one.
177204
List<String> received = recvDataAndAck();
205+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
206+
178207
Assertions.assertThat(tasks)
179208
.extracting(TaskHandle::id).containsExactlyInAnyOrderElementsOf(received);
180-
Assertions.assertThat(tasks)
181-
.extracting(TaskHandle::isAcked).allMatch(CompletableFuture::isDone);
182209

183-
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
210+
CompletableFuture<?>[] tasksAcked = tasks.stream()
211+
.map(TaskHandle::isAcked).toArray(CompletableFuture[]::new);
212+
Assertions.assertThat(CompletableFuture.allOf(tasksAcked))
213+
.succeedsWithin(5, TimeUnit.SECONDS);
184214

185215
// Since MockServer runs in the same thread as this test,
186216
// the context will be updated before the last emitEvent returns.
187217
closeContext();
188218

219+
// By the time context.close() returns all tasks MUST have results set.
189220
Assertions.assertThat(tasks).extracting(TaskHandle::result)
190221
.allMatch(CompletableFuture::isDone)
191222
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
@@ -207,8 +238,11 @@ public void test_drainOnClose() throws Exception {
207238
List<String> received = recvDataAndAck();
208239
Assertions.assertThat(tasks).extracting(TaskHandle::id)
209240
.containsExactlyInAnyOrderElementsOf(received);
210-
Assertions.assertThat(tasks).extracting(TaskHandle::isAcked)
211-
.allMatch(CompletableFuture::isDone);
241+
242+
CompletableFuture<?>[] tasksAcked = tasks.stream()
243+
.map(TaskHandle::isAcked).toArray(CompletableFuture[]::new);
244+
Assertions.assertThat(CompletableFuture.allOf(tasksAcked))
245+
.succeedsWithin(5, TimeUnit.SECONDS);
212246
} catch (Exception e) {
213247
throw new RuntimeException(e);
214248
}
@@ -273,6 +307,7 @@ public void test_backoffBacklog() throws Exception {
273307
int batchSizeNew = BATCH_SIZE / 2;
274308

275309
// Force the last BATCH_SIZE / 2 - 1 items to be transferred to the backlog.
310+
// Await for this event to be processed before moving forward.
276311
out.emitEvent(new Event.Backoff(batchSizeNew));
277312

278313
// The next item will go on the backlog and the trigger a flush,
@@ -298,14 +333,14 @@ public void test_backoffBacklog() throws Exception {
298333

299334
@Test
300335
public void test_reconnect_onShutdown() throws Exception {
301-
out.emitEvent(Event.SHUTTING_DOWN);
336+
out.emitEventAsync(Event.SHUTTING_DOWN);
302337
in.expectMessage(STOP);
303338
out.eof(true);
304339
in.expectMessage(START);
305340

306341
// Not strictly necessary -- we can close the context
307342
// before a new connection is established.
308-
out.emitEvent(Event.STARTED);
343+
out.emitEventAsync(Event.STARTED);
309344
}
310345

311346
@Test
@@ -319,18 +354,18 @@ public void test_reconnect_onOom() throws Exception {
319354

320355
// Respond with OOM and wait for the client to close its end of the stream.
321356
in.expectMessage(DATA);
322-
out.emitEvent(new Event.Oom(0));
357+
out.emitEventAsync(new Event.Oom(0));
323358

324359
// Close the server's end of the stream.
325360
in.expectMessage(STOP);
326361

327362
// Allow the client to reconnect to another "instance" and Ack the batch.
328363
in.expectMessage(START);
329-
out.emitEvent(Event.STARTED);
364+
out.emitEventAsync(Event.STARTED);
330365
recvDataAndAck();
331366

332367
List<String> submitted = tasks.stream().map(TaskHandle::id).toList();
333-
out.emitEvent(new Event.Results(submitted, Collections.emptyMap()));
368+
out.emitEventAsync(new Event.Results(submitted, Collections.emptyMap()));
334369
}
335370

336371
@Test
@@ -347,7 +382,7 @@ public void test_reconnect_onStreamHangup() throws Exception {
347382

348383
// The client should try to reconnect, because the context is still open.
349384
in.expectMessage(START);
350-
out.emitEvent(Event.STARTED);
385+
out.emitEventAsync(Event.STARTED);
351386

352387
// The previous batch hasn't been acked, so we should expect to receive it
353388
// again.
@@ -358,7 +393,7 @@ public void test_reconnect_onStreamHangup() throws Exception {
358393
// in the queue to wake the sender up.
359394
out.hangup();
360395
in.expectMessage(START);
361-
out.emitEvent(Event.STARTED);
396+
out.emitEventAsync(Event.STARTED);
362397
tasks.add(context.add(WeaviateObject.of()));
363398
recvDataAndAck();
364399

@@ -399,7 +434,7 @@ public void test_reconnect_DrainAfterStreamHangup() throws Exception {
399434
// When the server starts accepting connections again, the client should
400435
// drain the remaining BATCH_SIZE+1 objects as we close the context.
401436
in.expectMessage(START);
402-
out.emitEvent(Event.STARTED);
437+
out.emitEventAsync(Event.STARTED);
403438
Future<?> backgroundAcks = backgroundThread.submit(() -> {
404439
try {
405440
recvDataAndAck();
@@ -426,7 +461,7 @@ public void test_reconnect_DrainAfterStreamHangup() throws Exception {
426461
public void test_closeAfterStreamHangup() throws Exception {
427462
out.hangup();
428463
in.expectMessage(START);
429-
out.emitEvent(Event.STARTED);
464+
out.emitEventAsync(Event.STARTED);
430465
}
431466

432467
@Test
@@ -464,7 +499,7 @@ public void test_startAfterClose() throws Exception {
464499
*/
465500
private List<String> recvDataAndAck() throws InterruptedException {
466501
List<String> received = recvData();
467-
out.emitEvent(new Event.Acks(received));
502+
out.emitEventAsync(new Event.Acks(received));
468503
return received;
469504
}
470505

@@ -492,7 +527,16 @@ private static final class OutboundStream {
492527
this.eventThread = eventThread;
493528
}
494529

495-
CompletableFuture<Void> emitEvent(Event event) {
530+
/** Emit event on the current thread. */
531+
void emitEvent(Event event) {
532+
assert event != Event.EOF : "must not use synthetic EOF event";
533+
assert !(event instanceof Event.StreamHangup) : "must not use synthetic StreamHangup event";
534+
535+
stream.onNext(event);
536+
}
537+
538+
/** Emit event on the {@link #eventThread}. */
539+
CompletableFuture<Void> emitEventAsync(Event event) {
496540
assert event != Event.EOF : "must not use synthetic EOF event";
497541
assert !(event instanceof Event.StreamHangup) : "must not use synthetic StreamHangup event";
498542

@@ -522,7 +566,7 @@ CompletableFuture<Void> eof(boolean ok) {
522566
if (ok) {
523567
// These are guaranteed to finish before onCompleted,
524568
// as eventThread is just 1 thread.
525-
pendingEvents.forEach(this::emitEvent);
569+
pendingEvents.forEach(this::emitEventAsync);
526570
}
527571
return CompletableFuture.runAsync(stream::onCompleted, eventThread);
528572
}

0 commit comments

Comments
 (0)