mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-14 11:49:04 +01:00
Add MailboxApiCaller for calling API endpoints with retries.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package org.briarproject.bramble.api;
|
||||
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface Supplier<T> {
|
||||
|
||||
T get();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.Supplier;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.DAYS;
|
||||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MailboxApiCaller {
|
||||
|
||||
/**
|
||||
* The minimum interval between retries in milliseconds.
|
||||
*/
|
||||
long MIN_RETRY_INTERVAL_MS = MINUTES.toMillis(1);
|
||||
|
||||
/**
|
||||
* The maximum interval between retries in milliseconds.
|
||||
*/
|
||||
long MAX_RETRY_INTERVAL_MS = DAYS.toMillis(1);
|
||||
|
||||
/**
|
||||
* Asynchronously calls the given supplier, automatically retrying at
|
||||
* increasing intervals until the supplier returns false. The returned
|
||||
* {@link Cancellable} can be used to cancel any future retries.
|
||||
*
|
||||
* @param supplier A wrapper for an API call. The supplier's
|
||||
* {@link Supplier#get() get()} method will be called on the
|
||||
* {@link IoExecutor}. It should return true if the API call needs to be
|
||||
* retried, or false if the API call succeeded or
|
||||
* {@link TolerableFailureException failed tolerably}.
|
||||
*/
|
||||
Cancellable retryWithBackoff(Supplier<Boolean> supplier);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.Supplier;
|
||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MailboxApiCallerImpl implements MailboxApiCaller {
|
||||
|
||||
private final TaskScheduler taskScheduler;
|
||||
private final Executor ioExecutor;
|
||||
|
||||
@Inject
|
||||
MailboxApiCallerImpl(TaskScheduler taskScheduler,
|
||||
@IoExecutor Executor ioExecutor) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
this.ioExecutor = ioExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cancellable retryWithBackoff(Supplier<Boolean> supplier) {
|
||||
Task task = new Task(supplier);
|
||||
task.start();
|
||||
return task;
|
||||
}
|
||||
|
||||
private class Task implements Cancellable {
|
||||
|
||||
private final Supplier<Boolean> supplier;
|
||||
private final Object lock = new Object();
|
||||
|
||||
@GuardedBy("lock")
|
||||
@Nullable
|
||||
private Cancellable scheduledTask = null;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private boolean cancelled = false;
|
||||
|
||||
@GuardedBy("lock")
|
||||
private long retryIntervalMs = MIN_RETRY_INTERVAL_MS;
|
||||
|
||||
private Task(Supplier<Boolean> supplier) {
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
private void start() {
|
||||
synchronized (lock) {
|
||||
if (cancelled) throw new AssertionError();
|
||||
scheduledTask = taskScheduler.schedule(this::callApi,
|
||||
ioExecutor, 0, MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@IoExecutor
|
||||
private void callApi() {
|
||||
synchronized (lock) {
|
||||
if (cancelled) return;
|
||||
}
|
||||
// The supplier returns true if we should retry
|
||||
if (supplier.get()) {
|
||||
synchronized (lock) {
|
||||
if (cancelled) return;
|
||||
scheduledTask = taskScheduler.schedule(this::callApi,
|
||||
ioExecutor, retryIntervalMs, MILLISECONDS);
|
||||
// Increase the retry interval each time we retry
|
||||
retryIntervalMs =
|
||||
min(MAX_RETRY_INTERVAL_MS, retryIntervalMs * 2);
|
||||
}
|
||||
} else {
|
||||
synchronized (lock) {
|
||||
scheduledTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
Cancellable scheduledTask;
|
||||
synchronized (lock) {
|
||||
cancelled = true;
|
||||
scheduledTask = this.scheduledTask;
|
||||
this.scheduledTask = null;
|
||||
}
|
||||
if (scheduledTask != null) scheduledTask.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.Supplier;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxApiCaller.MAX_RETRY_INTERVAL_MS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxApiCaller.MIN_RETRY_INTERVAL_MS;
|
||||
|
||||
public class MailboxApiCallerImplTest extends BrambleMockTestCase {
|
||||
|
||||
private final TaskScheduler taskScheduler =
|
||||
context.mock(TaskScheduler.class);
|
||||
private final Executor ioExecutor = context.mock(Executor.class);
|
||||
private final BooleanSupplier supplier =
|
||||
context.mock(BooleanSupplier.class);
|
||||
private final Cancellable scheduledTask = context.mock(Cancellable.class);
|
||||
|
||||
private final MailboxApiCallerImpl caller =
|
||||
new MailboxApiCallerImpl(taskScheduler, ioExecutor);
|
||||
|
||||
@Test
|
||||
public void testSubmitsTaskWithZeroDelay() {
|
||||
// Calling retryWithBackoff() should schedule a try with zero delay
|
||||
AtomicReference<Runnable> runnable = new AtomicReference<>(null);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(0L), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(runnable, Runnable.class, 0),
|
||||
returnValue(scheduledTask)
|
||||
));
|
||||
}});
|
||||
|
||||
caller.retryWithBackoff(supplier);
|
||||
|
||||
// When the scheduled task runs, the supplier should be called. The
|
||||
// supplier returns false, so no retries should be scheduled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(supplier).get();
|
||||
will(returnValue(false));
|
||||
}});
|
||||
|
||||
runnable.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotRetryTaskIfCancelled() {
|
||||
// Calling retryWithBackoff() should schedule a try with zero delay
|
||||
AtomicReference<Runnable> runnable = new AtomicReference<>(null);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(0L), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(runnable, Runnable.class, 0),
|
||||
returnValue(scheduledTask)
|
||||
));
|
||||
}});
|
||||
|
||||
Cancellable returned = caller.retryWithBackoff(supplier);
|
||||
|
||||
// When the scheduled task runs, the supplier should be called. The
|
||||
// supplier returns true, so a retry should be scheduled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(supplier).get();
|
||||
will(returnValue(true));
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(MIN_RETRY_INTERVAL_MS),
|
||||
with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(runnable, Runnable.class, 0),
|
||||
returnValue(scheduledTask)
|
||||
));
|
||||
}});
|
||||
|
||||
runnable.get().run();
|
||||
|
||||
// When the Cancellable returned by retryWithBackoff() is cancelled,
|
||||
// the scheduled task should be cancelled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(scheduledTask).cancel();
|
||||
}});
|
||||
|
||||
returned.cancel();
|
||||
|
||||
// Cancelling again should have no effect
|
||||
returned.cancel();
|
||||
|
||||
// If the scheduled task runs anyway (cancellation came too late),
|
||||
// the supplier should not be called and no further tries should be
|
||||
// scheduled
|
||||
runnable.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoublesRetryIntervalUntilMaximumIsReached() {
|
||||
List<Long> expectedIntervals = new ArrayList<>();
|
||||
for (long interval = MIN_RETRY_INTERVAL_MS;
|
||||
interval <= MAX_RETRY_INTERVAL_MS; interval *= 2) {
|
||||
expectedIntervals.add(interval);
|
||||
}
|
||||
// Once the interval reaches the maximum it should be capped
|
||||
expectedIntervals.add(MAX_RETRY_INTERVAL_MS);
|
||||
expectedIntervals.add(MAX_RETRY_INTERVAL_MS);
|
||||
|
||||
// Calling retryWithBackoff() should schedule a try with zero delay
|
||||
AtomicReference<Runnable> runnable = new AtomicReference<>(null);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(0L), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(runnable, Runnable.class, 0),
|
||||
returnValue(scheduledTask)
|
||||
));
|
||||
}});
|
||||
|
||||
caller.retryWithBackoff(supplier);
|
||||
|
||||
// Each time the scheduled task runs, the supplier returns true, so a
|
||||
// retry should be scheduled with a longer interval
|
||||
for (long interval : expectedIntervals) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(supplier).get();
|
||||
will(returnValue(true));
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(interval), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(
|
||||
runnable, Runnable.class, 0),
|
||||
returnValue(scheduledTask)
|
||||
));
|
||||
}});
|
||||
|
||||
runnable.get().run();
|
||||
}
|
||||
}
|
||||
|
||||
// Reify the generic type to mollify jMock
|
||||
private interface BooleanSupplier extends Supplier<Boolean> {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user