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