Skip to content

Commit af624d7

Browse files
committed
refactor(batch): make TaskHandle retriable
This commit introduces a RetriableTask, which, at its core, manages a lifecycle of a CompletableFuture such that it is only completed after retrying this task is not possible.
1 parent 3e64a2d commit af624d7

7 files changed

Lines changed: 275 additions & 248 deletions

File tree

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

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,12 @@ public final class BatchContext<PropertiesT> implements Closeable {
223223
*/
224224
private volatile CompletableFuture<?> recv;
225225

226+
/**
227+
* Retry policy controls if and how many times
228+
* a {@link RetriableTask} can be retried.
229+
*/
230+
private final RetryPolicy retryPolicy;
231+
226232
/**
227233
* Maximum number of times the client will attempt to re-open the stream
228234
* before terminating the context.
@@ -243,12 +249,14 @@ public final class BatchContext<PropertiesT> implements Closeable {
243249
int maxSizeBytes,
244250
CollectionDescriptor<PropertiesT> collectionDescriptor,
245251
CollectionHandleDefaults collectionHandleDefaults,
252+
RetryPolicy retryPolicy,
246253
int batchSize,
247254
int queueSize,
248255
int maxReconnectRetries) {
249-
this.streamFactory = requireNonNull(streamFactory, "streamFactory is null");
250256
this.collectionDescriptor = requireNonNull(collectionDescriptor, "collectionDescriptor is null");
251257
this.collectionHandleDefaults = requireNonNull(collectionHandleDefaults, "collectionHandleDefaults is null");
258+
this.retryPolicy = requireNonNull(retryPolicy, "retryPolicy is null");
259+
this.streamFactory = requireNonNull(streamFactory, "streamFactory is null");
252260

253261
this.queue = new ArrayBlockingQueue<>(queueSize);
254262
this.batch = new Batch(batchSize, maxSizeBytes);
@@ -263,6 +271,7 @@ private BatchContext(Builder<PropertiesT> builder) {
263271
builder.maxSizeBytes,
264272
builder.collectionDescriptor,
265273
builder.collectionHandleDefaults,
274+
builder.retryPolicy,
266275
builder.batchSize,
267276
builder.queueSize,
268277
builder.maxReconnectRetries);
@@ -272,18 +281,35 @@ private BatchContext(Builder<PropertiesT> builder) {
272281
public TaskHandle add(WeaviateObject<PropertiesT> object) throws InterruptedException {
273282
TaskHandle handle = new TaskHandle(
274283
object,
275-
InsertManyRequest.buildObject(object, collectionDescriptor, collectionHandleDefaults));
284+
InsertManyRequest.buildObject(object, collectionDescriptor, collectionHandleDefaults),
285+
retryPolicy, this::retry);
276286
return add(handle);
277287
}
278288

279289
/** Add {@link BatchReference} to the batch. */
280290
public TaskHandle add(BatchReference reference) throws InterruptedException {
281291
TaskHandle handle = new TaskHandle(
282292
reference,
283-
InsertManyRequest.buildReference(reference, collectionHandleDefaults.tenant()));
293+
InsertManyRequest.buildReference(reference, collectionHandleDefaults.tenant()),
294+
retryPolicy, this::retry);
284295
return add(handle);
285296
}
286297

298+
private TaskHandle add(final TaskHandle taskHandle) throws InterruptedException {
299+
if (closed) {
300+
throw new IllegalStateException("context is closed");
301+
}
302+
requireNonNull(taskHandle, "taskHandle is null");
303+
304+
TaskHandle existing = wip.get(taskHandle.id());
305+
if (existing != null) {
306+
throw new DuplicateTaskException(taskHandle, existing);
307+
}
308+
309+
queue.put(taskHandle);
310+
return taskHandle;
311+
}
312+
287313
void start() {
288314
if (closed) {
289315
throw new IllegalStateException("context is closed");
@@ -316,12 +342,28 @@ void reconnect() throws InterruptedException, ExecutionException {
316342
*
317343
* <p>
318344
* BatchContext does not impose any limit on the number of times a task can
319-
* be retried -- it is up to the user to implement an appropriate retry policy.
345+
* be retried -- it is up to the user to select an appropriate retry policy.
320346
*
321347
* @see TaskHandle#timesRetried
348+
* @see RetryPolicy
322349
*/
323-
public TaskHandle retry(TaskHandle taskHandle) throws InterruptedException {
324-
return add(taskHandle.retry());
350+
private void retry(String id) {
351+
try {
352+
requireNonNull(id, "id is null");
353+
354+
TaskHandle taskHandle = wip.get(id);
355+
assert taskHandle != null : taskHandle + " is not wip";
356+
357+
// Put the handle back on the queue directly, circumventing
358+
// the checks closed- and duplicate items checks we do for
359+
// public methods. The retried task is guaranteed to be present
360+
// in the WIP list and may be retried well after the context
361+
// is closed to the user.
362+
queue.put(taskHandle);
363+
} catch (InterruptedException e) {
364+
// Preserve interrupted state without throwing the exception.
365+
Thread.currentThread().interrupt();
366+
}
325367
}
326368

327369
/**
@@ -562,21 +604,6 @@ private void onEvent(Event event) {
562604
}
563605
}
564606

565-
private TaskHandle add(final TaskHandle taskHandle) throws InterruptedException {
566-
if (closed) {
567-
throw new IllegalStateException("context is closed");
568-
}
569-
requireNonNull(taskHandle, "taskHandle is null");
570-
571-
TaskHandle existing = wip.get(taskHandle.id());
572-
if (existing != null) {
573-
throw new DuplicateTaskException(taskHandle, existing);
574-
}
575-
576-
queue.put(taskHandle);
577-
return taskHandle;
578-
}
579-
580607
private final class Send implements Runnable {
581608

582609
@Override
@@ -856,10 +883,6 @@ private void onAcks(Event.Acks acks) {
856883
if (!acks.acked().containsAll(removed)) {
857884
throwInternal(ProtocolViolationException.incompleteAcks(List.copyOf(removed)));
858885
}
859-
acks.acked().stream()
860-
.map(wip::get).filter(Objects::nonNull)
861-
.forEach(TaskHandle::setAcked);
862-
863886
setState(ACTIVE);
864887
}
865888

@@ -870,13 +893,18 @@ private void onResults(Event.Results results) {
870893
.addKeyValue("wip_tasks", wip::size)
871894
.log("Received Results");
872895

896+
// Remove successfully completed tasks from the WIP list and mark them done.
873897
results.successful().stream()
874898
.map(wip::remove).filter(Objects::nonNull)
875899
.forEach(TaskHandle::setSuccess);
876900

901+
// Report errors for failed tasks. Do NOT remove them from the WIP list.
877902
results.errors().keySet().stream()
878-
.map(wip::remove).filter(Objects::nonNull)
879-
.forEach(taskHandle -> taskHandle.setError(results.errors().get(taskHandle.id())));
903+
.map(wip::get).filter(Objects::nonNull)
904+
.forEach(taskHandle -> taskHandle.setError(
905+
new ServerException(results.errors().get(taskHandle.id()))));
906+
907+
// TODO(dyma): notify receivedResults
880908
}
881909

882910
private void onBackoff(Event.Backoff backoff) {
@@ -1194,10 +1222,16 @@ public static class Builder<PropertiesT> implements ObjectBuilder<BatchContext<P
11941222
this.collectionHandleDefaults = collectionHandleDefaults;
11951223
}
11961224

1225+
private RetryPolicy retryPolicy = RetryPolicy.never();
11971226
private int batchSize = 1_000;
11981227
private int queueSize = 1_000;
11991228
private int maxReconnectRetries = 5;
12001229

1230+
public Builder<PropertiesT> retryPolicy(RetryPolicy retryPolicy) {
1231+
this.retryPolicy = retryPolicy;
1232+
return this;
1233+
}
1234+
12011235
public Builder<PropertiesT> batchSize(int batchSize) {
12021236
this.batchSize = batchSize;
12031237
return this;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.weaviate.client6.v1.api.collections.batch;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.util.concurrent.CompletableFuture;
6+
import java.util.function.Consumer;
7+
8+
abstract class RetriableTask {
9+
private final String id;
10+
private final CompletableFuture<Void> root;
11+
12+
private volatile CompletableFuture<Void> current = new CompletableFuture<>();
13+
private volatile int retries = 0;
14+
15+
protected RetriableTask(String id, RetryPolicy retryPolicy, Consumer<String> onRetry) {
16+
this.id = requireNonNull(id, "id is null");
17+
this.root = retry(current, retryPolicy, onRetry);
18+
}
19+
20+
private final CompletableFuture<Void> retry(
21+
CompletableFuture<Void> f,
22+
RetryPolicy retryPolicy,
23+
Consumer<String> onRetry) {
24+
requireNonNull(f, "f is null");
25+
requireNonNull(retryPolicy, "retryPolicy is null");
26+
requireNonNull(onRetry, "onRetry is null");
27+
28+
return f.exceptionallyCompose(t -> {
29+
if (!retryPolicy.canRetry(this, t)) {
30+
return CompletableFuture.failedFuture(t);
31+
}
32+
retries++;
33+
current = new CompletableFuture<>();
34+
onRetry.accept(id);
35+
return retry(current, retryPolicy, onRetry);
36+
});
37+
}
38+
39+
/** Number of times this task has been retried. */
40+
public final int timesRetried() {
41+
return retries;
42+
}
43+
44+
/** Retrieve the ID of this task. */
45+
public final String id() {
46+
return id;
47+
}
48+
49+
/**
50+
* Mark the task successful. This status cannot be changed, so calling
51+
* {@link #setError} afterwards will have no effect.
52+
*/
53+
public final boolean setSuccess() {
54+
return current.complete(null);
55+
}
56+
57+
/**
58+
* Mark the task failed. This status cannot be changed, so calling
59+
* {@link #setSuccess} afterwards will have no effect.
60+
*
61+
* @param error Error message. Null values are tolerated, but are only expected
62+
* to occur due to a server's mistake.
63+
* Do not use {@code setError(null)} if the server reports success
64+
* status for the task; prefer {@link #setSuccess} in that case.
65+
*/
66+
public final boolean setError(Throwable t) {
67+
return current.completeExceptionally(t);
68+
}
69+
70+
/**
71+
* Track completion of this task.
72+
*
73+
* @return A future which completes when the server reports success
74+
* for this tasks or the applied {@link RetryPolicy}
75+
* no longer permits retrying the task.
76+
*/
77+
public final CompletableFuture<Void> done() {
78+
return root;
79+
}
80+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.weaviate.client6.v1.api.collections.batch;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.util.function.Predicate;
6+
7+
public class RetryPolicy {
8+
/** Create a retry policy that never permits retrying a task. */
9+
static RetryPolicy never() {
10+
return new RetryPolicy(__ -> false);
11+
}
12+
13+
private final Predicate<RetriableTask> retry;
14+
15+
/**
16+
* Construct a simple RetryPolicy that retries up to a certain number of times.
17+
*
18+
* @param maxRetries Maximum number of retries.
19+
*/
20+
public RetryPolicy(int maxRetries) {
21+
this(task -> task.timesRetried() < maxRetries);
22+
}
23+
24+
/**
25+
* Construct a RetryPolicy with a custom predicate.
26+
*
27+
* @param retry Predicate that returns true if the task should be retried.
28+
*/
29+
public RetryPolicy(Predicate<RetriableTask> retry) {
30+
this.retry = requireNonNull(retry, "retry is null");
31+
}
32+
33+
/**
34+
* Override this method to control which exceptions are considered retriable.
35+
*/
36+
protected boolean canRetryThrowable(Throwable t) {
37+
return t instanceof ServerException;
38+
}
39+
40+
boolean canRetry(RetriableTask task, Throwable t) {
41+
return canRetryThrowable(t) && retry.test(task);
42+
}
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.weaviate.client6.v1.api.collections.batch;
2+
3+
import io.weaviate.client6.v1.api.WeaviateException;
4+
5+
/**
6+
* ServerException carries an error message the server returns in
7+
* {@link Event.Results}. This is a retriable exception.
8+
*/
9+
public class ServerException extends WeaviateException {
10+
ServerException(String message) {
11+
super(message);
12+
}
13+
}

0 commit comments

Comments
 (0)