Skip to content

Commit cbdc64e

Browse files
committed
feat(batch): make sure to read all Results before closing the context
1 parent c540cac commit cbdc64e

2 files changed

Lines changed: 88 additions & 75 deletions

File tree

src/main/java/io/weaviate/client6/v1/api/collections/batch/BatchContext.java

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.concurrent.locks.Condition;
2424
import java.util.concurrent.locks.Lock;
2525
import java.util.concurrent.locks.ReentrantLock;
26-
import java.util.function.BiConsumer;
2726

2827
import javax.annotation.concurrent.GuardedBy;
2928

@@ -276,7 +275,7 @@ void start() {
276275
throw new IllegalStateException("context is closed");
277276
}
278277

279-
messages = streamFactory.createStream(recv = new Recv());
278+
messages = streamFactory.createStream(recv = new Recv(this));
280279
messages.onNext(Message.start(collectionHandleDefaults.consistencyLevel()));
281280

282281
// "send" routine must start after the nextState has been set.
@@ -301,7 +300,7 @@ void reconnect() throws InterruptedException, ExecutionException {
301300
// after the server half of the stream is closed (EOF or hangup).
302301
recv.get();
303302

304-
messages = streamFactory.createStream(recv = new Recv());
303+
messages = streamFactory.createStream(recv = new Recv(this));
305304
messages.onNext(Message.start(collectionHandleDefaults.consistencyLevel()));
306305
}
307306

@@ -345,7 +344,7 @@ public void close() throws IOException {
345344
}
346345
throw new IOException(e.getCause());
347346
} finally {
348-
shutdownExecutionServices();
347+
shutdownExecutors();
349348
}
350349
}
351350

@@ -393,22 +392,10 @@ private void shutdownNow(Exception ex) {
393392
}
394393
}
395394

396-
private void shutdownExecutionServices() {
397-
BiConsumer<String, List<Runnable>> assertEmpty = (name, pending) -> {
398-
assert pending.isEmpty() : "'%s' service had %d tasks awaiting execution"
399-
.formatted(pending.size(), name);
400-
};
401-
402-
List<Runnable> pending;
403-
404-
pending = sendService.shutdownNow();
405-
assertEmpty.accept("send", pending);
406-
407-
pending = scheduledService.shutdownNow();
408-
assertEmpty.accept("oom", pending);
409-
410-
pending = shutdownService.shutdownNow();
411-
assertEmpty.accept("shutdown", pending);
395+
private void shutdownExecutors() {
396+
sendService.shutdown();
397+
scheduledService.shutdown();
398+
shutdownService.shutdown();
412399
}
413400

414401
/** Set the new state and notify awaiting threads. */
@@ -523,13 +510,30 @@ private void trySend() {
523510
System.out.println("took POISON");
524511
drain();
525512

526-
// FIXME(dyma): we should wait until the WIP is empty
527-
// and only then exit, in case the server restarts
528-
// after ack'ing the last batch.
529-
513+
// Close our end of the stream and exit.
530514
messages.onNext(Message.stop());
531515
messages.onCompleted();
532-
return;
516+
517+
// The SSB protocol requires the client to continue reading the stream
518+
// until EOF. In the happy case, the server will close its half having
519+
// processed all previous requests; the WIP buffer is empty in that case.
520+
//
521+
// It is possible that the server will be restarted or the stream will be
522+
// hungup before client receives all Results, in which case we might need
523+
// to re-submit the items remaining in the WIP buffer.
524+
recv.get();
525+
526+
assert closed : "queue poisoned when context not closed";
527+
if (wip.isEmpty()) {
528+
return;
529+
}
530+
531+
// Server closed the stream before reporting all expected Results.
532+
// Return the poison to the queue and go another round -- if the
533+
// client tried to reconnect the batch may've been filled again.
534+
assert queue.isEmpty() : "queue must be empty after poison";
535+
queue.add(task);
536+
continue;
533537
}
534538

535539
Data data = task.data();
@@ -618,11 +622,20 @@ private void awaitCanPrepareNext() throws InterruptedException {
618622
}
619623
}
620624

621-
private final class Recv extends CompletableFuture<Void> implements StreamObserver<Event> {
625+
private static final class Recv extends CompletableFuture<Void> implements StreamObserver<Event> {
626+
private final BatchContext<?> context;
627+
628+
private Recv(BatchContext<?> context) {
629+
this.context = context;
630+
}
622631

623632
@Override
624633
public void onNext(Event event) {
625-
onEvent(event);
634+
try {
635+
context.onEvent(event);
636+
} catch (Exception e) {
637+
context.onEvent(Event.StreamHangup.fromThrowable(e));
638+
}
626639
}
627640

628641
/**
@@ -633,7 +646,7 @@ public void onNext(Event event) {
633646
@Override
634647
public void onCompleted() {
635648
try {
636-
onEvent(Event.EOF);
649+
context.onEvent(Event.EOF);
637650
} finally {
638651
complete(null);
639652
}
@@ -643,7 +656,7 @@ public void onCompleted() {
643656
@Override
644657
public void onError(Throwable t) {
645658
try {
646-
onEvent(Event.StreamHangup.fromThrowable(t));
659+
context.onEvent(Event.StreamHangup.fromThrowable(t));
647660
} finally {
648661
complete(null);
649662
}

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

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.io.IOException;
44
import java.util.ArrayList;
5-
import java.util.Collection;
5+
import java.util.Arrays;
66
import java.util.Collections;
77
import java.util.List;
88
import java.util.Map;
@@ -150,13 +150,12 @@ public void test_sendOneBatch() throws Exception {
150150
Assertions.assertThat(tasks)
151151
.extracting(TaskHandle::isAcked).allMatch(CompletableFuture::isDone);
152152

153-
Future<?> results = out.emitEvent(new Event.Results(received, Collections.emptyMap()));
153+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
154154

155155
// Since MockServer runs in the same thread as this test,
156156
// the context will be updated before the last emitEvent returns.
157157
context.close();
158158

159-
results.get(); // Wait until the Results event has been processed.
160159
Assertions.assertThat(tasks).extracting(TaskHandle::result)
161160
.allMatch(CompletableFuture::isDone)
162161
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
@@ -179,18 +178,19 @@ public void test_drainOnClose() throws Exception {
179178
BACKGROUND.submit(() -> {
180179
try {
181180
List<String> received = recvDataAndAck();
182-
Assertions.assertThat(tasks)
183-
.extracting(TaskHandle::id).containsExactlyInAnyOrderElementsOf(received);
184-
Assertions.assertThat(tasks)
185-
.extracting(TaskHandle::isAcked).allMatch(CompletableFuture::isDone);
186-
out.emitEvent(new Event.Results(received, Collections.emptyMap()));
181+
Assertions.assertThat(tasks).extracting(TaskHandle::id)
182+
.containsExactlyInAnyOrderElementsOf(received);
183+
Assertions.assertThat(tasks).extracting(TaskHandle::isAcked)
184+
.allMatch(CompletableFuture::isDone);
187185
} catch (Exception e) {
188186
throw new RuntimeException(e);
189187
}
190188
});
191189

190+
List<String> submitted = tasks.stream().map(TaskHandle::id).toList();
191+
out.beforeEof(new Event.Results(submitted, Collections.emptyMap()));
192+
192193
context.close();
193-
awaitResults(tasks);
194194

195195
Assertions.assertThat(tasks).extracting(TaskHandle::result)
196196
.allMatch(CompletableFuture::isDone)
@@ -220,15 +220,15 @@ public void test_backoff() throws Exception {
220220
// set by the Backoff message, i.e. BATCH_SIZE / 2.
221221
List<String> received = recvDataAndAck();
222222
Assertions.assertThat(received).hasSize(BATCH_SIZE / 2);
223-
out.emitEvent(new Event.Results(received, Collections.emptyMap()));
223+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
224224

225225
backgroundAdd.get(); // Finish populating batch context.
226226

227227
// Since testUser will try and add BATCH_SIZE no. objects,
228228
// we should expect there to be exactly 2 batches.
229229
received = recvDataAndAck();
230230
Assertions.assertThat(received).hasSize(BATCH_SIZE / 2);
231-
out.emitEvent(new Event.Results(received, Collections.emptyMap()));
231+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
232232

233233
context.close();
234234

@@ -262,20 +262,13 @@ public void test_backoffBacklog() throws Exception {
262262
tasks.add(context.add(WeaviateObject.of()));
263263

264264
Assertions.assertThat(received = recvDataAndAck()).hasSize(batchSizeNew);
265-
out.emitEvent(new Event.Results(received, Collections.emptyMap()));
265+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
266266

267267
Assertions.assertThat(received = recvDataAndAck()).hasSize(batchSizeNew);
268-
out.emitEvent(new Event.Results(received, Collections.emptyMap()));
268+
out.beforeEof(new Event.Results(received, Collections.emptyMap()));
269269

270270
context.close();
271271

272-
// Wait until the Results event's been processed to guarantee
273-
// that the tasks' futures are completed before asserting.
274-
CompletableFuture.allOf(
275-
tasks.stream().map(TaskHandle::result)
276-
.toArray(CompletableFuture[]::new))
277-
.get(100, TimeUnit.MILLISECONDS);
278-
279272
Assertions.assertThat(tasks).extracting(TaskHandle::result)
280273
.allMatch(CompletableFuture::isDone)
281274
.extracting(CompletableFuture::get).extracting(TaskHandle.Result::error)
@@ -301,9 +294,11 @@ public void test_reconnect_onOom() throws Exception {
301294
in.expectMessage(START);
302295
out.emitEvent(Event.STARTED);
303296

297+
List<TaskHandle> tasks = new ArrayList<>();
298+
304299
// OOM is the opposite of Ack -- trigger a flush first.
305300
for (int i = 0; i < BATCH_SIZE; i++) {
306-
context.add(WeaviateObject.of());
301+
tasks.add(context.add(WeaviateObject.of()));
307302
}
308303

309304
// Respond with OOM and wait for the client to close its end of the stream.
@@ -317,16 +312,20 @@ public void test_reconnect_onOom() throws Exception {
317312
in.expectMessage(START);
318313
out.emitEvent(Event.STARTED);
319314
recvDataAndAck();
315+
316+
List<String> submitted = tasks.stream().map(TaskHandle::id).toList();
317+
out.beforeEof(new Event.Results(submitted, Collections.emptyMap()));
320318
}
321319

322320
@Test
323321
public void test_reconnect_onStreamHangup() throws Exception {
324322
in.expectMessage(START);
325323
out.emitEvent(Event.STARTED);
326324

325+
List<TaskHandle> tasks = new ArrayList<>();
327326
// Trigger a flush.
328327
for (int i = 0; i < BATCH_SIZE; i++) {
329-
context.add(WeaviateObject.of());
328+
tasks.add(context.add(WeaviateObject.of()));
330329
}
331330

332331
// Expect a new batch to arrive. Hangup the stream before sending the Acks.
@@ -348,14 +347,17 @@ public void test_reconnect_onStreamHangup() throws Exception {
348347
out.hangup();
349348
in.expectMessage(START);
350349
out.emitEvent(Event.STARTED);
351-
context.add(WeaviateObject.of());
350+
tasks.add(context.add(WeaviateObject.of()));
352351
recvDataAndAck();
353352

354353
// Now fill up the rest of the batch to trigger a flush. Ack the incoming batch.
355354
for (int i = 0; i < BATCH_SIZE - 1; i++) {
356-
context.add(WeaviateObject.of());
355+
tasks.add(context.add(WeaviateObject.of()));
357356
}
358357
recvDataAndAck();
358+
359+
List<String> submitted = tasks.stream().map(TaskHandle::id).toList();
360+
out.beforeEof(new Event.Results(submitted, Collections.emptyMap()));
359361
}
360362

361363
@Test
@@ -391,20 +393,17 @@ public void test_reconnect_DrainAfterStreamHangup() throws Exception {
391393
out.emitEvent(Event.STARTED);
392394
Future<?> backgroundAcks = BACKGROUND.submit(() -> {
393395
try {
394-
List<String> ids = recvDataAndAck();
395-
out.emitEvent(new Event.Results(ids, Collections.emptyMap()));
396-
397-
ids = recvDataAndAck();
398-
out.emitEvent(new Event.Results(ids, Collections.emptyMap()));
399-
400-
ids = recvDataAndAck();
401-
Future<?> lastEvent = out.emitEvent(new Event.Results(ids, Collections.emptyMap()));
402-
lastEvent.get();
396+
recvDataAndAck();
397+
recvDataAndAck();
398+
recvDataAndAck();
403399
} catch (Exception e) {
404400
throw new RuntimeException(e);
405401
}
406402
});
407403

404+
List<String> submitted = tasks.stream().map(TaskHandle::id).toList();
405+
out.beforeEof(new Event.Results(submitted, Collections.emptyMap()));
406+
408407
context.close();
409408
backgroundAcks.get();
410409

@@ -480,22 +479,14 @@ private List<String> recvData() throws InterruptedException {
480479
.toList();
481480
}
482481

483-
private void awaitResults(Collection<TaskHandle> tasks) throws Exception {
484-
// Wait until the Results event's been processed to guarantee
485-
// that the tasks' futures are completed before asserting.
486-
CompletableFuture.allOf(
487-
tasks.stream().map(TaskHandle::result)
488-
.toArray(CompletableFuture[]::new))
489-
.get(100, TimeUnit.MILLISECONDS);
490-
}
491-
492482
static String getBeacon(WeaviateProtoBatch.BatchReference reference) {
493483
return "weaviate://localhost/" + reference.getToCollection() + "/" + reference.getToUuid();
494484
}
495485

496486
/** OutboundStream is a mock which dispatches server-side events. */
497487
private static final class OutboundStream {
498488
private final StreamObserver<Event> stream;
489+
private List<Event> beforeEof = new ArrayList<>();
499490

500491
OutboundStream(StreamObserver<Event> stream) {
501492
this.stream = stream;
@@ -508,13 +499,22 @@ Future<?> emitEvent(Event event) {
508499
return EVENT_THREAD.submit(() -> stream.onNext(event));
509500
}
510501

511-
/** Terminate the server-side of the stream abruptly. */
502+
/** Terminate the server half of the stream abruptly. */
503+
512504
Future<?> hangup() {
513505
return EVENT_THREAD.submit(() -> stream.onError(new RuntimeException("whaam!")));
514506
}
515507

516-
void eof() {
508+
/** Emit events before closing the server half of the stream. */
509+
void beforeEof(Event... events) {
510+
this.beforeEof.addAll(Arrays.asList(events));
511+
}
512+
513+
void eof(boolean ok) {
517514
assert Thread.currentThread() != TEST_THREAD : "test MUST NOT close/terminate the the stream";
515+
if (ok) {
516+
beforeEof.forEach(this::emitEvent);
517+
}
518518
EVENT_THREAD.submit(stream::onCompleted);
519519
}
520520
}
@@ -548,13 +548,13 @@ WeaviateProtoBatch.BatchStreamRequest expectMessage(
548548
@Override
549549
public void onCompleted() {
550550
done.complete(null);
551-
outbound.eof();
551+
outbound.eof(true);
552552
}
553553

554554
@Override
555555
public void onError(Throwable t) {
556556
done.completeExceptionally(t);
557-
outbound.eof();
557+
outbound.eof(false);
558558
}
559559

560560
@Override

0 commit comments

Comments
 (0)