BUG-58: add ReconnectStrategy concept 38/938/1
authorRobert Varga <rovarga@cisco.com>
Tue, 20 Aug 2013 19:17:10 +0000 (21:17 +0200)
committerRobert Varga <rovarga@cisco.com>
Wed, 21 Aug 2013 14:44:27 +0000 (16:44 +0200)
This adss the ReconnectStrategy concept, plus a few key implementations.

Change-Id: Ieda27db21ba874aa89cfc380d33556c6ac493ab7
Signed-off-by: Robert Varga <rovarga@cisco.com>
framework/pom.xml
framework/src/main/java/org/opendaylight/protocol/framework/NeverReconnectStrategy.java [new file with mode: 0644]
framework/src/main/java/org/opendaylight/protocol/framework/ReconnectImmediatelyStrategy.java [new file with mode: 0644]
framework/src/main/java/org/opendaylight/protocol/framework/ReconnectStrategy.java [new file with mode: 0644]
framework/src/main/java/org/opendaylight/protocol/framework/TimedReconnectStrategy.java [new file with mode: 0644]

index aa05beae8499238ad484bd8fca19c42ae2c14b0d..20dc67c888f27e7c9feebc50fdeec99e0d2081d8 100644 (file)
                        <artifactId>guava</artifactId>
                        <version>${guava.version}</version>
                </dependency>
+               <dependency>
+                       <groupId>com.google.code.findbugs</groupId>
+                       <artifactId>jsr305</artifactId>
+                       <version>2.0.1</version>
+               </dependency>
                <dependency>
                        <groupId>org.slf4j</groupId>
                        <artifactId>slf4j-api</artifactId>
diff --git a/framework/src/main/java/org/opendaylight/protocol/framework/NeverReconnectStrategy.java b/framework/src/main/java/org/opendaylight/protocol/framework/NeverReconnectStrategy.java
new file mode 100644 (file)
index 0000000..3c12fe1
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.framework;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Utility ReconnectStrategy singleton, which will cause the reconnect process
+ * to always fail.
+ */
+@ThreadSafe
+public final class NeverReconnectStrategy implements ReconnectStrategy {
+       private final EventExecutor executor;
+       private final int timeout;
+
+       public NeverReconnectStrategy(final EventExecutor executor, final int timeout) {
+               Preconditions.checkArgument(timeout >= 0);
+               this.executor = Preconditions.checkNotNull(executor);
+               this.timeout = timeout;
+       }
+
+       @Override
+       public Future<Void> scheduleReconnect() {
+               return executor.newFailedFuture(new Throwable());
+       }
+
+       @Override
+       public void reconnectSuccessful() {
+               throw new IllegalStateException("Reconnection successful when no reconnect should have been attempted");
+       }
+
+       @Override
+       public int getConnectTimeout() {
+               return timeout;
+       }
+}
diff --git a/framework/src/main/java/org/opendaylight/protocol/framework/ReconnectImmediatelyStrategy.java b/framework/src/main/java/org/opendaylight/protocol/framework/ReconnectImmediatelyStrategy.java
new file mode 100644 (file)
index 0000000..9793e84
--- /dev/null
@@ -0,0 +1,39 @@
+package org.opendaylight.protocol.framework;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Utility ReconnectStrategy singleton, which will cause the reconnect process
+ * to immediately schedule a reconnection attempt.
+ */
+@ThreadSafe
+public final class ReconnectImmediatelyStrategy implements ReconnectStrategy {
+       private final EventExecutor executor;
+       private final int timeout;
+
+       public ReconnectImmediatelyStrategy(final EventExecutor executor, final int timeout) {
+               Preconditions.checkArgument(timeout >= 0);
+               this.executor = Preconditions.checkNotNull(executor);
+               this.timeout = timeout;
+       }
+
+       @Override
+       public Future<Void> scheduleReconnect() {
+               return executor.newSucceededFuture(null);
+       }
+
+       @Override
+       public void reconnectSuccessful() {
+               // Nothing to do
+       }
+
+       @Override
+       public int getConnectTimeout() {
+               return timeout;
+       }
+}
diff --git a/framework/src/main/java/org/opendaylight/protocol/framework/ReconnectStrategy.java b/framework/src/main/java/org/opendaylight/protocol/framework/ReconnectStrategy.java
new file mode 100644 (file)
index 0000000..e1fb14c
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.protocol.framework;
+
+import io.netty.util.concurrent.Future;
+
+/**
+ * Interface exposed by a reconnection strategy provider. A reconnection
+ * strategy decides whether to attempt reconnection and when to do that.
+ * 
+ * The proper way of using this API is such that when a connection attempt
+ * has failed, the user will call scheduleReconnect() to obtain a future,
+ * which tracks schedule of the next connect attempt. The user should add its
+ * own listener to be get notified when the future is done. Once the
+ * the notification fires, user should examine the future to see whether
+ * it is successful or not. If it is successful, the user should immediately
+ * initiate a connection attempt. If it is unsuccessful, the user must
+ * not attempt any more connection attempts and should abort the reconnection
+ * process.
+ */
+public interface ReconnectStrategy {
+       /**
+        * Query the strategy for the connect timeout.
+        * 
+        * @return connect try timeout in milliseconds, or
+        *         0 for infinite (or system-default) timeout
+        * @throws Exception if the connection should not be attempted
+        */
+       public int getConnectTimeout() throws Exception;
+
+       /**
+        * Schedule a connection attempt. The precise time when the connection
+        * should be attempted is signaled by successful completion of returned
+        * future.
+        * 
+        * @return a future tracking the schedule, may not be null
+        * @throws IllegalStateException when a connection attempt is currently
+        *         scheduled.
+        */
+       public Future<Void> scheduleReconnect();
+
+       /**
+        * Reset the strategy state. Users call this method once the reconnection
+        * process succeeds.
+        */
+       public void reconnectSuccessful();
+}
diff --git a/framework/src/main/java/org/opendaylight/protocol/framework/TimedReconnectStrategy.java b/framework/src/main/java/org/opendaylight/protocol/framework/TimedReconnectStrategy.java
new file mode 100644 (file)
index 0000000..195671a
--- /dev/null
@@ -0,0 +1,163 @@
+package org.opendaylight.protocol.framework;
+
+import io.netty.util.concurrent.EventExecutor;
+import io.netty.util.concurrent.Future;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Swiss army knife equivalent for reconnect strategies.
+ * 
+ * This strategy continues to schedule reconnect attempts, each having to
+ * complete in a fixed time (connectTime).
+ * 
+ * Initial sleep time is specified as minSleep. Each subsequent unsuccessful
+ * attempt multiplies this time by a constant factor (sleepFactor) -- this
+ * allows for either constant reconnect times (sleepFactor = 1), or various
+ * degrees of exponential back-off (sleepFactor > 1). Maximum sleep time
+ * between attempts can be capped to a specific value (maxSleep).
+ * 
+ * The strategy can optionally give up based on two criteria:
+ * 
+ * A preset number of connection retries (maxAttempts) has been reached, or
+ * 
+ * A preset absolute deadline is reached (deadline nanoseconds, as reported
+ * by System.nanoTime(). In this specific case, both connectTime and maxSleep
+ * will be controlled such that the connection attempt is resolved as closely
+ * to the deadline as possible.
+ * 
+ * Both these caps can be combined, with the strategy giving up as soon as the
+ * first one is reached.
+ */
+@ThreadSafe
+public final class TimedReconnectStrategy implements ReconnectStrategy {
+       private final EventExecutor executor;
+       private final Long deadline, maxAttempts, maxSleep;
+       private final double sleepFactor;
+       private final int connectTime;
+       private final long minSleep;
+
+       @GuardedBy("this")
+       private long attempts;
+
+       @GuardedBy("this")
+       private long lastSleep;
+
+       @GuardedBy("this")
+       private boolean scheduled;
+
+       public TimedReconnectStrategy(final EventExecutor executor, final int connectTime,
+                       final long minSleep, final double sleepFactor, final Long maxSleep,
+                       final Long maxAttempts, final Long deadline) {
+               Preconditions.checkArgument(maxSleep == null || minSleep <= maxSleep);
+               Preconditions.checkArgument(sleepFactor >= 1);
+               Preconditions.checkArgument(connectTime >= 0);
+               this.executor = Preconditions.checkNotNull(executor);
+               this.deadline = deadline;
+               this.maxAttempts = maxAttempts;
+               this.minSleep = minSleep;
+               this.maxSleep = maxSleep;
+               this.sleepFactor = sleepFactor;
+               this.connectTime = connectTime;
+       }
+
+       @Override
+       public synchronized Future<Void> scheduleReconnect() {
+               // Check if a reconnect attempt is scheduled
+               Preconditions.checkState(scheduled == false);
+
+               // Get a stable 'now' time for deadline calculations
+               final long now = System.nanoTime();
+
+               // Obvious stop conditions
+               if (maxAttempts != null && attempts >= maxAttempts)
+                       return executor.newFailedFuture(new Throwable("Maximum reconnection attempts reached"));
+               if (deadline != null && deadline <= now)
+                       return executor.newFailedFuture(new TimeoutException("Reconnect deadline reached"));
+
+               /*
+                * First connection attempt gets initialized to minimum sleep,
+                * each subsequent is exponentially backed off by sleepFactor.
+                */
+               if (attempts != 0) {
+                       lastSleep *= sleepFactor;
+               } else {
+                       lastSleep = minSleep;
+               }
+
+               // Cap the sleep time to maxSleep
+               if (maxSleep != null && lastSleep > maxSleep)
+                       lastSleep = maxSleep;
+
+               // Check if the reconnect attempt is within the deadline
+               if (deadline != null && deadline <= now + TimeUnit.MILLISECONDS.toNanos(lastSleep)) {
+                       return executor.newFailedFuture(new TimeoutException("Next reconnect would happen after deadline"));
+               }
+
+               // If we are not sleeping at all, return an already-succeeded future
+               if (lastSleep == 0)
+                       return executor.newSucceededFuture(null);
+
+               // Need to retain a final reference to this for locking purposes,
+               // also set the scheduled flag.
+               final Object lock = this;
+               scheduled = true;
+
+               // Schedule a task for the right time. It will also clear the flag.
+               return executor.schedule(new Callable<Void>() {
+                       @Override
+                       public Void call() throws TimeoutException {
+                               synchronized (lock) {
+                                       Preconditions.checkState(scheduled == true);
+                                       scheduled = false;
+                               }
+
+                               return null;
+                       }
+               }, lastSleep, TimeUnit.MILLISECONDS);
+       }
+
+       @Override
+       public synchronized void reconnectSuccessful() {
+               Preconditions.checkState(scheduled == false);
+               attempts = 0;
+       }
+
+       @Override
+       public int getConnectTimeout() throws TimeoutException {
+               int timeout = connectTime;
+
+               if (deadline != null) {
+
+                       // If there is a deadline, we may need to cap the connect
+                       // timeout to meet the deadline.
+                       final long now = System.nanoTime();
+                       if (now >= deadline)
+                               throw new TimeoutException("Reconnect deadline already passed");
+
+                       final long left = TimeUnit.NANOSECONDS.toMillis(deadline - now);
+                       if (left < 1)
+                               throw new TimeoutException("Connect timeout too close to deadline");
+
+                       /*
+                        * A bit of magic:
+                        * - if time left is less than the timeout, set it directly
+                        * - if there is no timeout, and time left is:
+                        *      - less than maximum integer, set timeout to time left
+                        *      - more than maximum integer, set timeout Integer.MAX_VALUE
+                        */
+                       if (timeout > left)
+                               timeout = (int) left;
+                       else if (timeout == 0)
+                               timeout = left <= Integer.MAX_VALUE ? (int) left : Integer.MAX_VALUE;
+               }
+               return timeout;
+       }
+}