Add connectivity checkers for our own mailbox and a contact's mailbox.

This commit is contained in:
akwizgran
2022-05-26 12:53:42 +01:00
parent ef6e3bb2a7
commit 6358518f88
7 changed files with 674 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.concurrent.ThreadSafe;
/**
* An interface for checking whether a mailbox is reachable.
*/
@ThreadSafe
@NotNullByDefault
interface ConnectivityChecker {
/**
* Destroys the checker. Any current connectivity check is cancelled.
*/
void destroy();
/**
* Starts a connectivity check if needed and calls the given observer when
* the check succeeds. If a check is already running then the observer is
* called when the check succeeds. If a connectivity check has recently
* succeeded then the observer is called immediately.
*/
void checkConnectivity(MailboxProperties properties,
ConnectivityObserver o);
interface ConnectivityObserver {
void onConnectivityCheckSucceeded();
}
}

View File

@@ -0,0 +1,111 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.ThreadSafe;
@ThreadSafe
@NotNullByDefault
abstract class ConnectivityCheckerImpl implements ConnectivityChecker {
/**
* If no more than this much time has elapsed since the last connectivity
* check succeeded, consider the result to be fresh and don't check again.
* <p>
* Package access for testing.
*/
static final long CONNECTIVITY_CHECK_FRESHNESS_MS = 10_000;
private final Object lock = new Object();
protected final Clock clock;
private final MailboxApiCaller mailboxApiCaller;
@GuardedBy("lock")
private boolean destroyed = false;
@GuardedBy("lock")
@Nullable
private Cancellable connectivityCheck = null;
@GuardedBy("lock")
private long lastConnectivityCheckSucceeded = 0;
@GuardedBy("lock")
private final List<ConnectivityObserver> connectivityObservers =
new ArrayList<>();
/**
* Creates an {@link ApiCall} for checking whether the mailbox is
* reachable. The {@link ApiCall} should call
* {@link #onConnectivityCheckSucceeded(long)} if the check succeeds.
*/
abstract ApiCall createConnectivityCheckTask(MailboxProperties properties);
ConnectivityCheckerImpl(Clock clock, MailboxApiCaller mailboxApiCaller) {
this.clock = clock;
this.mailboxApiCaller = mailboxApiCaller;
}
@Override
public void destroy() {
synchronized (lock) {
destroyed = true;
connectivityObservers.clear();
if (connectivityCheck != null) {
connectivityCheck.cancel();
connectivityCheck = null;
}
}
}
@Override
public void checkConnectivity(MailboxProperties properties,
ConnectivityObserver o) {
boolean callNow = false;
synchronized (lock) {
if (destroyed) return;
if (connectivityCheck == null) {
// No connectivity check is running
long now = clock.currentTimeMillis();
if (now - lastConnectivityCheckSucceeded
> CONNECTIVITY_CHECK_FRESHNESS_MS) {
// The last connectivity check is stale, start a new one
connectivityObservers.add(o);
ApiCall task =
createConnectivityCheckTask(properties);
connectivityCheck = mailboxApiCaller.retryWithBackoff(task);
} else {
// The last connectivity check is fresh
callNow = true;
}
} else {
// A connectivity check is running, wait for it to succeed
connectivityObservers.add(o);
}
}
if (callNow) o.onConnectivityCheckSucceeded();
}
protected void onConnectivityCheckSucceeded(long now) {
List<ConnectivityObserver> observers;
synchronized (lock) {
if (destroyed) return;
connectivityCheck = null;
lastConnectivityCheckSucceeded = now;
observers = new ArrayList<>(connectivityObservers);
connectivityObservers.clear();
}
for (ConnectivityObserver o : observers) {
o.onConnectivityCheckSucceeded();
}
}
}

View File

@@ -0,0 +1,40 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import java.io.IOException;
import javax.annotation.concurrent.ThreadSafe;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
@ThreadSafe
@NotNullByDefault
class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl {
private final MailboxApi mailboxApi;
ContactMailboxConnectivityChecker(Clock clock,
MailboxApiCaller mailboxApiCaller, MailboxApi mailboxApi) {
super(clock, mailboxApiCaller);
this.mailboxApi = mailboxApi;
}
@Override
ApiCall createConnectivityCheckTask(MailboxProperties properties) {
if (properties.isOwner()) throw new IllegalArgumentException();
return new SimpleApiCall() {
@Override
void tryToCallApi() throws IOException, ApiException {
mailboxApi.getFiles(properties,
requireNonNull(properties.getInboxId()));
// Call the observers and cache the result
onConnectivityCheckSucceeded(clock.currentTimeMillis());
}
};
}
}

View File

@@ -0,0 +1,70 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import java.io.IOException;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@ThreadSafe
@NotNullByDefault
class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl {
private static final Logger LOG =
getLogger(OwnMailboxConnectivityChecker.class.getName());
private final MailboxApi mailboxApi;
private final TransactionManager db;
private final MailboxSettingsManager mailboxSettingsManager;
OwnMailboxConnectivityChecker(Clock clock,
MailboxApiCaller mailboxApiCaller,
MailboxApi mailboxApi,
TransactionManager db,
MailboxSettingsManager mailboxSettingsManager) {
super(clock, mailboxApiCaller);
this.mailboxApi = mailboxApi;
this.db = db;
this.mailboxSettingsManager = mailboxSettingsManager;
}
@Override
ApiCall createConnectivityCheckTask(MailboxProperties properties) {
if (!properties.isOwner()) throw new IllegalArgumentException();
return () -> {
try {
try {
mailboxApi.getFolders(properties);
LOG.info("Own mailbox is reachable");
long now = clock.currentTimeMillis();
db.transaction(false, txn -> mailboxSettingsManager
.recordSuccessfulConnection(txn, now));
// Call the observers and cache the result
onConnectivityCheckSucceeded(now);
return false; // Don't retry
} catch (IOException | ApiException e) {
LOG.warning("Own mailbox is unreachable");
logException(LOG, WARNING, e);
long now = clock.currentTimeMillis();
db.transaction(false, txn -> mailboxSettingsManager
.recordFailedConnectionAttempt(txn, now));
}
} catch (DbException e) {
logException(LOG, WARNING, e);
}
return true; // Retry
};
}
}

View File

@@ -0,0 +1,201 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.jmock.Expectations;
import org.junit.Test;
import javax.annotation.Nonnull;
import static org.briarproject.bramble.mailbox.ConnectivityCheckerImpl.CONNECTIVITY_CHECK_FRESHNESS_MS;
import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
public class ConnectivityCheckerImplTest extends BrambleMockTestCase {
private final Clock clock = context.mock(Clock.class);
private final MailboxApiCaller mailboxApiCaller =
context.mock(MailboxApiCaller.class);
private final ApiCall apiCall = context.mock(ApiCall.class);
private final Cancellable task = context.mock(Cancellable.class);
private final ConnectivityObserver observer1 =
context.mock(ConnectivityObserver.class, "1");
private final ConnectivityObserver observer2 =
context.mock(ConnectivityObserver.class, "2");
private final MailboxProperties properties =
getMailboxProperties(true, CLIENT_SUPPORTS);
private final long now = System.currentTimeMillis();
@Test
public void testFirstObserverStartsCheck() {
ConnectivityCheckerImpl checker = createChecker();
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer1);
// When the check succeeds the observer should be called
context.checking(new Expectations() {{
oneOf(observer1).onConnectivityCheckSucceeded();
}});
checker.onConnectivityCheckSucceeded(now);
// The observer should not be called again when subsequent checks
// succeed
checker.onConnectivityCheckSucceeded(now);
}
@Test
public void testObserverIsAddedToExistingCheck() {
ConnectivityCheckerImpl checker = createChecker();
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer1);
// When checkConnectivity() is called again before the first check
// succeeds, the observer should be added to the existing check
checker.checkConnectivity(properties, observer2);
// When the check succeeds both observers should be called
context.checking(new Expectations() {{
oneOf(observer1).onConnectivityCheckSucceeded();
oneOf(observer2).onConnectivityCheckSucceeded();
}});
checker.onConnectivityCheckSucceeded(now);
// The observers should not be called again when subsequent checks
// succeed
checker.onConnectivityCheckSucceeded(now);
}
@Test
public void testFreshResultIsReused() {
ConnectivityCheckerImpl checker = createChecker();
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer1);
// When the check succeeds the observer should be called
context.checking(new Expectations() {{
oneOf(observer1).onConnectivityCheckSucceeded();
}});
checker.onConnectivityCheckSucceeded(now);
// When checkConnectivity() is called again within
// CONNECTIVITY_CHECK_FRESHNESS_MS the observer should be called with
// the result of the recent check
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS));
oneOf(observer2).onConnectivityCheckSucceeded();
}});
checker.checkConnectivity(properties, observer2);
}
@Test
public void testStaleResultIsNotReused() {
ConnectivityCheckerImpl checker = createChecker();
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer1);
// When the check succeeds the observer should be called
context.checking(new Expectations() {{
oneOf(observer1).onConnectivityCheckSucceeded();
}});
checker.onConnectivityCheckSucceeded(now);
// When checkConnectivity() is called again after more than
// CONNECTIVITY_CHECK_FRESHNESS_MS another check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer2);
// When the check succeeds the observer should be called
context.checking(new Expectations() {{
oneOf(observer2).onConnectivityCheckSucceeded();
}});
checker.onConnectivityCheckSucceeded(
now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1);
}
@Test
public void testCheckIsCancelledWhenCheckerIsDestroyed() {
ConnectivityCheckerImpl checker = createChecker();
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(apiCall);
will(returnValue(task));
}});
checker.checkConnectivity(properties, observer1);
// When the checker is destroyed the check should be cancelled
context.checking(new Expectations() {{
oneOf(task).cancel();
}});
checker.destroy();
// If the check runs anyway (cancellation came too late) the observer
// should not be called
checker.onConnectivityCheckSucceeded(now);
}
private ConnectivityCheckerImpl createChecker() {
return new ConnectivityCheckerImpl(clock, mailboxApiCaller) {
@Override
@Nonnull
protected ApiCall createConnectivityCheckTask(
@Nonnull MailboxProperties properties) {
return apiCall;
}
};
}
}

View File

@@ -0,0 +1,102 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
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.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ContactMailboxConnectivityCheckerTest extends BrambleMockTestCase {
private final Clock clock = context.mock(Clock.class);
private final MailboxApiCaller mailboxApiCaller =
context.mock(MailboxApiCaller.class);
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
private final Cancellable task = context.mock(Cancellable.class);
private final ConnectivityObserver observer =
context.mock(ConnectivityObserver.class);
private final MailboxProperties properties =
getMailboxProperties(false, CLIENT_SUPPORTS);
private final long now = System.currentTimeMillis();
@Test
public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
ContactMailboxConnectivityChecker checker = createChecker();
AtomicReference<ApiCall> apiCall = new AtomicReference<>(null);
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
returnValue(task)
));
}});
checker.checkConnectivity(properties, observer);
// When the check succeeds the observer should be called
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(properties,
requireNonNull(properties.getInboxId()));
will(returnValue(emptyList()));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(observer).onConnectivityCheckSucceeded();
}});
// The call should not be retried
assertFalse(apiCall.get().callApi());
}
@Test
public void testObserverIsNotCalledWhenCheckFails() throws Exception {
ContactMailboxConnectivityChecker checker = createChecker();
AtomicReference<ApiCall> apiCall = new AtomicReference<>(null);
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
returnValue(task)
));
}});
checker.checkConnectivity(properties, observer);
// When the check fails, the observer should not be called
context.checking(new Expectations() {{
oneOf(mailboxApi).getFiles(properties,
requireNonNull(properties.getInboxId()));
will(throwException(new IOException()));
}});
// The call should be retried
assertTrue(apiCall.get().callApi());
}
private ContactMailboxConnectivityChecker createChecker() {
return new ContactMailboxConnectivityChecker(clock, mailboxApiCaller,
mailboxApi);
}
}

View File

@@ -0,0 +1,118 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.Cancellable;
import org.briarproject.bramble.api.db.Transaction;
import org.briarproject.bramble.api.db.TransactionManager;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
import org.briarproject.bramble.api.system.Clock;
import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver;
import org.briarproject.bramble.test.BrambleMockTestCase;
import org.briarproject.bramble.test.CaptureArgumentAction;
import org.briarproject.bramble.test.DbExpectations;
import org.jmock.Expectations;
import org.jmock.lib.action.DoAllAction;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import static java.util.Collections.emptyList;
import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS;
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase {
private final Clock clock = context.mock(Clock.class);
private final MailboxApiCaller mailboxApiCaller =
context.mock(MailboxApiCaller.class);
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
private final TransactionManager db =
context.mock(TransactionManager.class);
private final MailboxSettingsManager mailboxSettingsManager =
context.mock(MailboxSettingsManager.class);
private final Cancellable task = context.mock(Cancellable.class);
private final ConnectivityObserver observer =
context.mock(ConnectivityObserver.class);
private final MailboxProperties properties =
getMailboxProperties(true, CLIENT_SUPPORTS);
private final long now = System.currentTimeMillis();
@Test
public void testObserverIsCalledWhenCheckSucceeds() throws Exception {
OwnMailboxConnectivityChecker checker = createChecker();
AtomicReference<ApiCall> apiCall = new AtomicReference<>(null);
Transaction txn = new Transaction(null, false);
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
returnValue(task)
));
}});
checker.checkConnectivity(properties, observer);
// When the check succeeds, the success should be recorded in the DB
// and the observer should be called
context.checking(new DbExpectations() {{
oneOf(mailboxApi).getFolders(properties);
will(returnValue(emptyList()));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now);
oneOf(observer).onConnectivityCheckSucceeded();
}});
// The call should not be retried
assertFalse(apiCall.get().callApi());
}
@Test
public void testObserverIsNotCalledWhenCheckFails() throws Exception {
OwnMailboxConnectivityChecker checker = createChecker();
AtomicReference<ApiCall> apiCall = new AtomicReference<>(null);
Transaction txn = new Transaction(null, false);
// When checkConnectivity() is called a check should be started
context.checking(new Expectations() {{
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
will(new DoAllAction(
new CaptureArgumentAction<>(apiCall, ApiCall.class, 0),
returnValue(task)
));
}});
checker.checkConnectivity(properties, observer);
// When the check fails, the failure should be recorded in the DB and
// the observer should not be called
context.checking(new DbExpectations() {{
oneOf(mailboxApi).getFolders(properties);
will(throwException(new IOException()));
oneOf(clock).currentTimeMillis();
will(returnValue(now));
oneOf(db).transaction(with(false), withDbRunnable(txn));
oneOf(mailboxSettingsManager)
.recordFailedConnectionAttempt(txn, now);
}});
// The call should be retried
assertTrue(apiCall.get().callApi());
}
private OwnMailboxConnectivityChecker createChecker() {
return new OwnMailboxConnectivityChecker(clock, mailboxApiCaller,
mailboxApi, db, mailboxSettingsManager);
}
}