BUG-58: refactor to take advantage of netty
[bgpcep.git] / framework / src / main / java / org / opendaylight / protocol / framework / TimedReconnectStrategy.java
1 package org.opendaylight.protocol.framework;
2
3 import io.netty.util.concurrent.EventExecutor;
4 import io.netty.util.concurrent.Future;
5
6 import java.util.concurrent.Callable;
7 import java.util.concurrent.TimeUnit;
8 import java.util.concurrent.TimeoutException;
9
10 import javax.annotation.concurrent.GuardedBy;
11 import javax.annotation.concurrent.ThreadSafe;
12
13 import org.slf4j.Logger;
14 import org.slf4j.LoggerFactory;
15
16 import com.google.common.base.Preconditions;
17
18 /**
19  * Swiss army knife equivalent for reconnect strategies.
20  * 
21  * This strategy continues to schedule reconnect attempts, each having to
22  * complete in a fixed time (connectTime).
23  * 
24  * Initial sleep time is specified as minSleep. Each subsequent unsuccessful
25  * attempt multiplies this time by a constant factor (sleepFactor) -- this
26  * allows for either constant reconnect times (sleepFactor = 1), or various
27  * degrees of exponential back-off (sleepFactor > 1). Maximum sleep time
28  * between attempts can be capped to a specific value (maxSleep).
29  * 
30  * The strategy can optionally give up based on two criteria:
31  * 
32  * A preset number of connection retries (maxAttempts) has been reached, or
33  * 
34  * A preset absolute deadline is reached (deadline nanoseconds, as reported
35  * by System.nanoTime(). In this specific case, both connectTime and maxSleep
36  * will be controlled such that the connection attempt is resolved as closely
37  * to the deadline as possible.
38  * 
39  * Both these caps can be combined, with the strategy giving up as soon as the
40  * first one is reached.
41  */
42 @ThreadSafe
43 public final class TimedReconnectStrategy implements ReconnectStrategy {
44         private static final Logger logger = LoggerFactory.getLogger(TimedReconnectStrategy.class);
45         private final EventExecutor executor;
46         private final Long deadline, maxAttempts, maxSleep;
47         private final double sleepFactor;
48         private final int connectTime;
49         private final long minSleep;
50
51         @GuardedBy("this")
52         private long attempts;
53
54         @GuardedBy("this")
55         private long lastSleep;
56
57         @GuardedBy("this")
58         private boolean scheduled;
59
60         public TimedReconnectStrategy(final EventExecutor executor, final int connectTime,
61                         final long minSleep, final double sleepFactor, final Long maxSleep,
62                         final Long maxAttempts, final Long deadline) {
63                 Preconditions.checkArgument(maxSleep == null || minSleep <= maxSleep);
64                 Preconditions.checkArgument(sleepFactor >= 1);
65                 Preconditions.checkArgument(connectTime >= 0);
66                 this.executor = Preconditions.checkNotNull(executor);
67                 this.deadline = deadline;
68                 this.maxAttempts = maxAttempts;
69                 this.minSleep = minSleep;
70                 this.maxSleep = maxSleep;
71                 this.sleepFactor = sleepFactor;
72                 this.connectTime = connectTime;
73         }
74
75         @Override
76         public synchronized Future<Void> scheduleReconnect(final Throwable cause) {
77                 logger.debug("Connection attempt failed", cause);
78
79                 // Check if a reconnect attempt is scheduled
80                 Preconditions.checkState(scheduled == false);
81
82                 // Get a stable 'now' time for deadline calculations
83                 final long now = System.nanoTime();
84
85                 // Obvious stop conditions
86                 if (maxAttempts != null && attempts >= maxAttempts) {
87                         return executor.newFailedFuture(new Throwable("Maximum reconnection attempts reached"));
88                 }
89                 if (deadline != null && deadline <= now) {
90                         return executor.newFailedFuture(new TimeoutException("Reconnect deadline reached"));
91                 }
92
93                 /*
94                  * First connection attempt gets initialized to minimum sleep,
95                  * each subsequent is exponentially backed off by sleepFactor.
96                  */
97                 if (attempts != 0) {
98                         lastSleep *= sleepFactor;
99                 } else {
100                         lastSleep = minSleep;
101                 }
102
103                 // Cap the sleep time to maxSleep
104                 if (maxSleep != null && lastSleep > maxSleep) {
105                         lastSleep = maxSleep;
106                 }
107
108                 // Check if the reconnect attempt is within the deadline
109                 if (deadline != null && deadline <= now + TimeUnit.MILLISECONDS.toNanos(lastSleep)) {
110                         return executor.newFailedFuture(new TimeoutException("Next reconnect would happen after deadline"));
111                 }
112
113                 // If we are not sleeping at all, return an already-succeeded future
114                 if (lastSleep == 0) {
115                         return executor.newSucceededFuture(null);
116                 }
117
118                 // Need to retain a final reference to this for locking purposes,
119                 // also set the scheduled flag.
120                 final Object lock = this;
121                 scheduled = true;
122
123                 // Schedule a task for the right time. It will also clear the flag.
124                 return executor.schedule(new Callable<Void>() {
125                         @Override
126                         public Void call() throws TimeoutException {
127                                 synchronized (lock) {
128                                         Preconditions.checkState(scheduled == true);
129                                         scheduled = false;
130                                 }
131
132                                 return null;
133                         }
134                 }, lastSleep, TimeUnit.MILLISECONDS);
135         }
136
137         @Override
138         public synchronized void reconnectSuccessful() {
139                 Preconditions.checkState(scheduled == false);
140                 attempts = 0;
141         }
142
143         @Override
144         public int getConnectTimeout() throws TimeoutException {
145                 int timeout = connectTime;
146
147                 if (deadline != null) {
148
149                         // If there is a deadline, we may need to cap the connect
150                         // timeout to meet the deadline.
151                         final long now = System.nanoTime();
152                         if (now >= deadline) {
153                                 throw new TimeoutException("Reconnect deadline already passed");
154                         }
155
156                         final long left = TimeUnit.NANOSECONDS.toMillis(deadline - now);
157                         if (left < 1) {
158                                 throw new TimeoutException("Connect timeout too close to deadline");
159                         }
160
161                         /*
162                          * A bit of magic:
163                          * - if time left is less than the timeout, set it directly
164                          * - if there is no timeout, and time left is:
165                          *      - less than maximum integer, set timeout to time left
166                          *      - more than maximum integer, set timeout Integer.MAX_VALUE
167                          */
168                         if (timeout > left) {
169                                 timeout = (int) left;
170                         } else if (timeout == 0) {
171                                 timeout = left <= Integer.MAX_VALUE ? (int) left : Integer.MAX_VALUE;
172                         }
173                 }
174                 return timeout;
175         }
176 }