Skip to content

Commit 966dabc

Browse files
committed
fix(batch): use Future for 'send' task, as it is actually cancelable
Provide descriptive helpers for repeated operations, like awaiting a state or closing/opening the stream.
1 parent 9a85950 commit 966dabc

1 file changed

Lines changed: 57 additions & 94 deletions

File tree

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

Lines changed: 57 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import java.util.concurrent.ExecutionException;
1818
import java.util.concurrent.ExecutorService;
1919
import java.util.concurrent.Executors;
20+
import java.util.concurrent.Future;
2021
import java.util.concurrent.ScheduledExecutorService;
2122
import java.util.concurrent.ScheduledFuture;
2223
import java.util.concurrent.TimeUnit;
2324
import java.util.concurrent.locks.Condition;
2425
import java.util.concurrent.locks.Lock;
2526
import java.util.concurrent.locks.ReentrantLock;
27+
import java.util.function.Predicate;
2628

2729
import javax.annotation.concurrent.GuardedBy;
2830

@@ -159,6 +161,7 @@ public final class BatchContext<PropertiesT> implements Closeable {
159161
/**
160162
* Queue publishes insert tasks from the main thread to the "sender".
161163
*
164+
* <p>
162165
* Send {@link TaskHandle#POISON} to gracefully shut down the "sender"
163166
* thread. The same queue may be re-used with a different "sender",
164167
* e.g. after {@link #reconnect}, but only when the new thread is known
@@ -170,6 +173,7 @@ public final class BatchContext<PropertiesT> implements Closeable {
170173
/**
171174
* Work-in-progress items.
172175
*
176+
* <p>
173177
* An item is added to the wip map after the "sender" successfully
174178
* adds it to the {@link #batch} and is removed once the server reports
175179
* back the result (whether success of failure).
@@ -205,16 +209,14 @@ public final class BatchContext<PropertiesT> implements Closeable {
205209
/**
206210
* Handle for the "sender" routine.
207211
* Cancel this future to interrupt the "sender".
208-
*
209-
* @see Send#cancel
210212
*/
211-
private final Send send = new Send();
213+
private volatile Future<?> send;
212214

213215
/**
214216
* Indicates completion of the "recv" routine.
215217
* Canceling this future will have no effect.
216218
*/
217-
private volatile Recv recv;
219+
private volatile CompletableFuture<?> recv;
218220

219221
/**
220222
* Maximum number of times the client will attempt to re-open the stream
@@ -246,6 +248,8 @@ public final class BatchContext<PropertiesT> implements Closeable {
246248
this.queue = new ArrayBlockingQueue<>(queueSize);
247249
this.batch = new Batch(batchSize, maxSizeBytes);
248250
this.maxReconnectRetries = maxReconnectRetries;
251+
252+
setState(AWAIT_STARTED);
249253
}
250254

251255
private BatchContext(Builder<PropertiesT> builder) {
@@ -279,13 +283,8 @@ void start() {
279283
if (closed) {
280284
throw new IllegalStateException("context is closed");
281285
}
282-
283-
messages = streamFactory.createStream(recv = new Recv(this));
284-
messages.onNext(Message.start(collectionHandleDefaults.consistencyLevel()));
285-
286-
// "send" routine must start after the nextState has been set.
287-
setState(AWAIT_STARTED);
288-
sendService.execute((Runnable) send);
286+
openStream();
287+
send = sendService.submit(new Send());
289288
}
290289

291290
/**
@@ -304,9 +303,7 @@ void reconnect() throws InterruptedException, ExecutionException {
304303
// the context can only transition into the Reconnecting state
305304
// after the server half of the stream is closed (EOF or hangup).
306305
recv.get();
307-
308-
messages = streamFactory.createStream(recv = new Recv(this));
309-
messages.onNext(Message.start(collectionHandleDefaults.consistencyLevel()));
306+
openStream();
310307
}
311308

312309
/**
@@ -365,8 +362,8 @@ private void shutdown() {
365362
// Luckily, shutdownNow resolves the `closing` future as well.
366363
queue.put(TaskHandle.POISON);
367364

368-
// Wait for both "send" and "recv" to exit.
369-
CompletableFuture.allOf(send, recv).get();
365+
// Wait for both "send" to exit; "send" will not exit until "recv" completes.
366+
send.get();
370367
closing.complete(null);
371368
} catch (Exception e) {
372369
closing.completeExceptionally(e);
@@ -380,14 +377,8 @@ private void shutdownNow(Exception ex) {
380377
closing.completeExceptionally(ex);
381378
messages.onError(Status.INTERNAL.withCause(ex).asRuntimeException());
382379

383-
// Terminate the "send" routine and wait for it to exit.
384-
// Since we're already in the error state we do not care
385-
// much if it throws or not.
380+
// Terminate the "send" routine.
386381
send.cancel(true);
387-
try {
388-
send.get();
389-
} catch (Exception e) {
390-
}
391382

392383
if (!closed) {
393384
// Since shutdownNow is never triggered by the "main" thread,
@@ -419,24 +410,32 @@ void setState(State nextState) {
419410
}
420411
}
421412

422-
/** Returns true if the next batch can be sent. */
423-
boolean canSend() {
413+
/** Blocks until a change in {@link #state} causes the predicate to be true. */
414+
void awaitState(Predicate<State> predicate) throws InterruptedException {
415+
requireNonNull(predicate, "predicate is null");
416+
424417
lock.lock();
425418
try {
426-
return state.canSend();
419+
while (!predicate.test(state)) {
420+
stateChanged.await();
421+
}
427422
} finally {
428423
lock.unlock();
429424
}
430425
}
431426

432-
/** Returns true if the next batch can be assembled from the queued items. */
433-
boolean canPrepareNext() {
434-
lock.lock();
435-
try {
436-
return state.canPrepareNext();
437-
} finally {
438-
lock.unlock();
439-
}
427+
/** Open a new batching stream. */
428+
void openStream() {
429+
Recv events = new Recv(this);
430+
recv = events;
431+
messages = streamFactory.createStream(events);
432+
messages.onNext(Message.start(collectionHandleDefaults.consistencyLevel()));
433+
}
434+
435+
/** Close the client half of the stream. */
436+
void closeStream() {
437+
messages.onNext(Message.stop());
438+
messages.onCompleted();
440439
}
441440

442441
/**
@@ -450,6 +449,8 @@ boolean canPrepareNext() {
450449
* @see #scheduledService
451450
*/
452451
private void onEvent(Event event) {
452+
requireNonNull(event, "event is null");
453+
453454
lock.lock();
454455
try {
455456
System.out.println("onEvent " + event);
@@ -463,6 +464,7 @@ private TaskHandle add(final TaskHandle taskHandle) throws InterruptedException
463464
if (closed) {
464465
throw new IllegalStateException("context is closed");
465466
}
467+
requireNonNull(taskHandle, "taskHandle is null");
466468

467469
TaskHandle existing = wip.get(taskHandle.id());
468470
if (existing != null) {
@@ -473,7 +475,7 @@ private TaskHandle add(final TaskHandle taskHandle) throws InterruptedException
473475
return taskHandle;
474476
}
475477

476-
private final class Send extends CompletableFuture<Void> implements Runnable {
478+
private final class Send implements Runnable {
477479

478480
@Override
479481
public void run() {
@@ -483,25 +485,16 @@ public void run() {
483485
trySend();
484486
} finally {
485487
Thread.currentThread().setName(threadName);
486-
complete(null);
487488
}
488489
}
489490

490-
@Override
491-
public boolean cancel(boolean mayInterruptIfRunning) {
492-
if (mayInterruptIfRunning) {
493-
Thread.currentThread().interrupt();
494-
}
495-
return mayInterruptIfRunning;
496-
}
497-
498491
/**
499492
* trySend consumes {@link #queue} tasks and sends them in batches until it
500493
* encounters a {@link TaskHandle#POISON} or is otherwise interrupted.
501494
*/
502495
private void trySend() {
503496
try {
504-
awaitCanPrepareNext();
497+
awaitState(State::canPrepareNext);
505498

506499
while (!Thread.currentThread().isInterrupted()) {
507500
if (batch.isFull()) {
@@ -515,9 +508,7 @@ private void trySend() {
515508
System.out.println("took POISON");
516509
drain();
517510

518-
// Close the client half of the stream.
519-
messages.onNext(Message.stop());
520-
messages.onCompleted();
511+
closeStream();
521512

522513
// The SSB protocol requires the client to continue reading the stream
523514
// until EOF. In the happy case, the server will close its half having
@@ -582,49 +573,27 @@ private void drain() throws InterruptedException {
582573
assert batch.isEmpty() : "batch not empty after drain";
583574
}
584575

585-
private void flush() throws InterruptedException {
586-
awaitCanSend();
587-
messages.onNext(batch.prepare());
588-
setState(IN_FLIGHT);
589-
590-
// When we get into OOM / ServerShuttingDown state, then we can be certain that
591-
// there isn't any reason to keep waiting for the ACKs. However, we should not
592-
// exit without either taking a poison pill from the queue,
593-
// or being interrupted, as this risks blocking the producer (main) thread.
594-
awaitCanPrepareNext();
595-
}
596-
597-
/** Block until the current state allows {@link State#canSend}. */
598-
private void awaitCanSend() throws InterruptedException {
599-
lock.lock();
600-
try {
601-
while (!canSend()) {
602-
stateChanged.await();
603-
}
604-
} finally {
605-
lock.unlock();
606-
}
607-
}
608-
609576
/**
577+
* Block until the current state allows {@link State#canSend},
578+
* then prepare the batch, send it, and set InFlight state.
610579
* Block until the current state allows {@link State#canPrepareNext}.
611580
*
612-
* <p>
581+
* <br>
613582
* Depending on the BatchContext lifecycle, the semantics of
614583
* "await can prepare next" can be one of "message is ACK'ed"
615584
* "the stream has started", or, more generally,
616585
* "it is safe to take a next item from the queue and add it to the batch".
586+
*
587+
* @see Batch#prepare
588+
* @see #IN_FLIGHT
617589
*/
618-
private void awaitCanPrepareNext() throws InterruptedException {
619-
lock.lock();
620-
try {
621-
while (!canPrepareNext()) {
622-
stateChanged.await();
623-
}
624-
} finally {
625-
lock.unlock();
626-
}
590+
private void flush() throws InterruptedException {
591+
awaitState(State::canSend);
592+
messages.onNext(batch.prepare());
593+
setState(IN_FLIGHT);
594+
awaitState(State::canPrepareNext);
627595
}
596+
628597
}
629598

630599
private static final class Recv extends CompletableFuture<Void> implements StreamObserver<Event> {
@@ -639,7 +608,7 @@ public void onNext(Event event) {
639608
try {
640609
context.onEvent(event);
641610
} catch (Exception e) {
642-
context.onEvent(Event.StreamHangup.fromThrowable(e));
611+
context.onEvent(new Event.ClientError(e));
643612
}
644613
}
645614

@@ -668,22 +637,20 @@ public void onError(Throwable t) {
668637
}
669638
}
670639

671-
final State AWAIT_STARTED = new BaseState("AWAIT_STARTED", BaseState.Action.PREPARE_NEXT) {
640+
private final State AWAIT_STARTED = new BaseState("AWAIT_STARTED", BaseState.Action.PREPARE_NEXT) {
672641
@Override
673642
public void onEvent(Event event) {
674-
if (requireNonNull(event, "event is null") == Event.STARTED) {
643+
if (event == Event.STARTED) {
675644
setState(ACTIVE);
676645
} else {
677646
super.onEvent(event);
678647
}
679648
}
680649
};
681-
final State ACTIVE = new BaseState("ACTIVE", BaseState.Action.PREPARE_NEXT, BaseState.Action.SEND);
682-
final State IN_FLIGHT = new BaseState("IN_FLIGHT") {
650+
private final State ACTIVE = new BaseState("ACTIVE", BaseState.Action.PREPARE_NEXT, BaseState.Action.SEND);
651+
private final State IN_FLIGHT = new BaseState("IN_FLIGHT") {
683652
@Override
684653
public void onEvent(Event event) {
685-
requireNonNull(event, "event is null");
686-
687654
if (event instanceof Event.Acks acks) {
688655
Collection<String> removed = batch.clear();
689656
if (!acks.acked().containsAll(removed)) {
@@ -763,8 +730,6 @@ public boolean canPrepareNext() {
763730
*/
764731
@Override
765732
public void onEvent(Event event) {
766-
requireNonNull(event, "event is null");
767-
768733
if (event instanceof Event.Results results) {
769734
onResults(results);
770735
} else if (event instanceof Event.Backoff backoff) {
@@ -847,7 +812,6 @@ private void initiateShutdown() {
847812

848813
@Override
849814
public void onEvent(Event event) {
850-
requireNonNull(event, "event");
851815
if (event == Event.SHUTTING_DOWN ||
852816
event instanceof StreamHangup ||
853817
event instanceof ClientError) {
@@ -894,8 +858,7 @@ public boolean canSend() {
894858

895859
@Override
896860
public void onEnter(State prev) {
897-
messages.onNext(Message.stop());
898-
messages.onCompleted();
861+
closeStream();
899862
}
900863
}
901864

0 commit comments

Comments
 (0)