Add MailboxApiCaller for calling API endpoints with retries.

This commit is contained in:
akwizgran
2022-05-23 17:43:52 +01:00
parent 18b3865a86
commit 274963d9d1
4 changed files with 298 additions and 0 deletions

View File

@@ -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> {
}
}