}
private void connectComplete(final Future<?> future) {
- final Throwable cause;
-
// Locked manipulation of internal state
synchronized (this) {
// A quick sanity check
}
currentTask = null;
- cause = future.cause();
+ final var cause = future.cause();
if (cause == null || cause instanceof CancellationException) {
// Success or cancellation, nothing else to do.
// In case of success the rest of the setup is driven by RemoteDeviceHandler callbacks
}
// We are invoking callbacks, do not hold locks
- onDeviceFailed(cause);
+ reconnectOrFail();
}
@Override
@Override
public void onDeviceDisconnected() {
delegate.onDeviceDisconnected();
- scheduleReconnect();
+ reconnectOrFail();
}
@Override
public void onDeviceFailed(final Throwable throwable) {
+ // We have not reported onDeviceConnected(), so from the view of delete we are still connecting
LOG.debug("Connection attempt failed", throwable);
- delegate.onDeviceFailed(throwable);
- scheduleReconnect();
+ reconnectOrFail();
}
@Override
delegate.onNotification(domNotification);
}
- private synchronized void scheduleReconnect() {
+ private void reconnectOrFail() {
+ final var ex = scheduleReconnect();
+ if (ex != null) {
+ delegate.onDeviceFailed(ex);
+ }
+ }
+
+ private synchronized Exception scheduleReconnect() {
if (isClosed()) {
- return;
+ return null;
}
final long delayMillis;
// We have exceeded the number of connection attempts
if (maxAttempts > 0 && attempts >= maxAttempts) {
LOG.info("Failed to connect {} after {} attempts, not attempting", deviceId, attempts);
- return;
+ return new ConnectGivenUpException("Given up connecting " + deviceId + " after " + attempts + " attempts");
}
// First connection attempt gets initialized to minimum sleep, each subsequent is exponentially backed off
lastSleep = delayMillis;
LOG.debug("Retrying {} connection attempt {} after {} milliseconds", deviceId, attempts, delayMillis);
- // If we are not sleeping at all, return an already-succeeded future
- if (delayMillis == 0) {
- lockedConnect();
- return;
- }
-
- // Schedule a task for the right time. It will also clear the flag.
+ // Schedule a task for the right time. We always go through the executor to eliminate the special case of
+ // immediate reconnect. While we could check and got to lockedConnect(), it makes for a rare special case.
+ // That special case makes for more code paths to test and introduces additional uncertainty as to whether
+ // the attempt was executed on on this thread or not.
currentTask = eventExecutor.schedule(this::reconnect, delayMillis, TimeUnit.MILLISECONDS);
+ return null;
}
private synchronized void reconnect() {