Improve timeout message
[controller.git] / opendaylight / md-sal / cds-access-client / src / main / java / org / opendaylight / controller / cluster / access / client / AbstractClientConnection.java
1 /*
2  * Copyright (c) 2016 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.controller.cluster.access.client;
9
10 import akka.actor.ActorRef;
11 import com.google.common.annotations.VisibleForTesting;
12 import com.google.common.base.MoreObjects;
13 import com.google.common.base.MoreObjects.ToStringHelper;
14 import com.google.common.base.Preconditions;
15 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
16 import java.util.Collection;
17 import java.util.Optional;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.TimeoutException;
20 import java.util.concurrent.locks.Lock;
21 import java.util.concurrent.locks.ReentrantLock;
22 import java.util.function.Consumer;
23 import javax.annotation.Nonnull;
24 import javax.annotation.concurrent.GuardedBy;
25 import javax.annotation.concurrent.NotThreadSafe;
26 import org.opendaylight.controller.cluster.access.concepts.Request;
27 import org.opendaylight.controller.cluster.access.concepts.RequestException;
28 import org.opendaylight.controller.cluster.access.concepts.Response;
29 import org.opendaylight.controller.cluster.access.concepts.ResponseEnvelope;
30 import org.opendaylight.controller.cluster.access.concepts.RuntimeRequestException;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 import scala.concurrent.duration.FiniteDuration;
34
35 /**
36  * Base class for a connection to the backend. Responsible to queueing and dispatch of requests toward the backend.
37  * Can be in three conceptual states: Connecting, Connected and Reconnecting, which are represented by public final
38  * classes exposed from this package.
39  *
40  * @author Robert Varga
41  */
42 @NotThreadSafe
43 public abstract class AbstractClientConnection<T extends BackendInfo> {
44     private static final Logger LOG = LoggerFactory.getLogger(AbstractClientConnection.class);
45
46     /*
47      * Timers involved in communication with the backend. There are three tiers which are spaced out to allow for
48      * recovery at each tier. Keep these constants in nanoseconds, as that prevents unnecessary conversions in the fast
49      * path.
50      */
51     /**
52      * Backend aliveness timer. This is reset whenever we receive a response from the backend and kept armed whenever
53      * we have an outstanding request. If when this time expires, we tear down this connection and attept to reconnect
54      * it.
55      */
56     @VisibleForTesting
57     static final long BACKEND_ALIVE_TIMEOUT_NANOS = TimeUnit.SECONDS.toNanos(30);
58
59     /**
60      * Request timeout. If the request fails to complete within this time since it was originally enqueued, we time
61      * the request out.
62      */
63     @VisibleForTesting
64     static final long REQUEST_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(2);
65
66     /**
67      * No progress timeout. A client fails to make any forward progress in this time, it will terminate itself.
68      */
69     @VisibleForTesting
70     static final long NO_PROGRESS_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(15);
71
72     // Emit a debug entry if we sleep for more that this amount
73     private static final long DEBUG_DELAY_NANOS = TimeUnit.MILLISECONDS.toNanos(100);
74
75     // Upper bound on the time a thread is forced to sleep to keep queue size under control
76     private static final long MAX_DELAY_SECONDS = 5;
77     private static final long MAX_DELAY_NANOS = TimeUnit.SECONDS.toNanos(MAX_DELAY_SECONDS);
78
79     private final Lock lock = new ReentrantLock();
80     private final ClientActorContext context;
81     @GuardedBy("lock")
82     private final TransmitQueue queue;
83     private final Long cookie;
84
85     @GuardedBy("lock")
86     private boolean haveTimer;
87
88     /**
89      * Time reference when we saw any activity from the backend.
90      */
91     private long lastReceivedTicks;
92
93     private volatile RequestException poisoned;
94
95     // Do not allow subclassing outside of this package
96     AbstractClientConnection(final ClientActorContext context, final Long cookie,
97             final TransmitQueue queue) {
98         this.context = Preconditions.checkNotNull(context);
99         this.cookie = Preconditions.checkNotNull(cookie);
100         this.queue = Preconditions.checkNotNull(queue);
101         this.lastReceivedTicks = currentTime();
102     }
103
104     // Do not allow subclassing outside of this package
105     AbstractClientConnection(final AbstractClientConnection<T> oldConnection, final int targetQueueSize) {
106         this.context = oldConnection.context;
107         this.cookie = oldConnection.cookie;
108         this.queue = new TransmitQueue.Halted(targetQueueSize);
109         this.lastReceivedTicks = oldConnection.lastReceivedTicks;
110     }
111
112     public final ClientActorContext context() {
113         return context;
114     }
115
116     public final @Nonnull Long cookie() {
117         return cookie;
118     }
119
120     public final ActorRef localActor() {
121         return context.self();
122     }
123
124     public final long currentTime() {
125         return context.ticker().read();
126     }
127
128     /**
129      * Send a request to the backend and invoke a specified callback when it finishes. This method is safe to invoke
130      * from any thread.
131      *
132      * <p>This method may put the caller thread to sleep in order to throttle the request rate.
133      * The callback may be called before the sleep finishes.
134      *
135      * @param request Request to send
136      * @param callback Callback to invoke
137      */
138     public final void sendRequest(final Request<?, ?> request, final Consumer<Response<?, ?>> callback) {
139         final long now = currentTime();
140         sendEntry(new ConnectionEntry(request, callback, now), now);
141     }
142
143     /**
144      * Send a request to the backend and invoke a specified callback when it finishes. This method is safe to invoke
145      * from any thread.
146      *
147      * <p>
148      * Note that unlike {@link #sendRequest(Request, Consumer)}, this method does not exert backpressure, hence it
149      * should never be called from an application thread.
150      *
151      * @param request Request to send
152      * @param callback Callback to invoke
153      * @param enqueuedTicks Time (according to {@link #currentTime()} of request enqueue
154      */
155     public final void enqueueRequest(final Request<?, ?> request, final Consumer<Response<?, ?>> callback,
156             final long enqueuedTicks) {
157         enqueueEntry(new ConnectionEntry(request, callback, enqueuedTicks), currentTime());
158     }
159
160     public final long enqueueEntry(final ConnectionEntry entry, final long now) {
161         lock.lock();
162         try {
163             final RequestException maybePoison = poisoned;
164             if (maybePoison != null) {
165                 throw new IllegalStateException("Connection " + this + " has been poisoned", maybePoison);
166             }
167
168             if (queue.isEmpty()) {
169                 // The queue is becoming non-empty, schedule a timer.
170                 scheduleTimer(entry.getEnqueuedTicks() + REQUEST_TIMEOUT_NANOS - now);
171             }
172             return queue.enqueue(entry, now);
173         } finally {
174             lock.unlock();
175         }
176     }
177
178     public abstract Optional<T> getBackendInfo();
179
180     final Collection<ConnectionEntry> startReplay() {
181         lock.lock();
182         return queue.drain();
183     }
184
185     @GuardedBy("lock")
186     final void finishReplay(final ReconnectForwarder forwarder) {
187         setForwarder(forwarder);
188
189         /*
190          * The process of replaying all messages may have taken a significant chunk of time, depending on type
191          * of messages, queue depth and available processing power. In extreme situations this may have already
192          * exceeded BACKEND_ALIVE_TIMEOUT_NANOS, in which case we are running the risk of not making reasonable forward
193          * progress before we start a reconnect cycle.
194          *
195          * Note that the timer is armed after we have sent the first message, hence we should be seeing a response
196          * from the backend before we see a timeout, simply due to how the mailbox operates.
197          *
198          * At any rate, reset the timestamp once we complete reconnection (which an atomic transition from the
199          * perspective of outside world), as that makes it a bit easier to reason about timing of events.
200          */
201         lastReceivedTicks = currentTime();
202         lock.unlock();
203     }
204
205     @GuardedBy("lock")
206     final void setForwarder(final ReconnectForwarder forwarder) {
207         queue.setForwarder(forwarder, currentTime());
208     }
209
210     @GuardedBy("lock")
211     abstract ClientActorBehavior<T> lockedReconnect(ClientActorBehavior<T> current,
212             RequestException runtimeRequestException);
213
214     final void sendEntry(final ConnectionEntry entry, final long now) {
215         long delay = enqueueEntry(entry, now);
216         try {
217             if (delay >= DEBUG_DELAY_NANOS) {
218                 if (delay > MAX_DELAY_NANOS) {
219                     LOG.info("Capping {} throttle delay from {} to {} seconds", this,
220                         TimeUnit.NANOSECONDS.toSeconds(delay), MAX_DELAY_SECONDS, new Throwable());
221                     delay = MAX_DELAY_NANOS;
222                 }
223                 if (LOG.isDebugEnabled()) {
224                     LOG.debug("{}: Sleeping for {}ms on connection {}", context.persistenceId(),
225                         TimeUnit.NANOSECONDS.toMillis(delay), this);
226                 }
227             }
228             TimeUnit.NANOSECONDS.sleep(delay);
229         } catch (InterruptedException e) {
230             Thread.currentThread().interrupt();
231             LOG.debug("Interrupted after sleeping {}ns", e, currentTime() - now);
232         }
233     }
234
235     final ClientActorBehavior<T> reconnect(final ClientActorBehavior<T> current, final RequestException cause) {
236         lock.lock();
237         try {
238             return lockedReconnect(current, cause);
239         } finally {
240             lock.unlock();
241         }
242     }
243
244     /**
245      * Schedule a timer to fire on the actor thread after a delay.
246      *
247      * @param delay Delay, in nanoseconds
248      */
249     @GuardedBy("lock")
250     private void scheduleTimer(final long delay) {
251         if (haveTimer) {
252             LOG.debug("{}: timer already scheduled on {}", context.persistenceId(), this);
253             return;
254         }
255         if (queue.hasSuccessor()) {
256             LOG.debug("{}: connection {} has a successor, not scheduling timer", context.persistenceId(), this);
257             return;
258         }
259
260         // If the delay is negative, we need to schedule an action immediately. While the caller could have checked
261         // for that condition and take appropriate action, but this is more convenient and less error-prone.
262         final long normalized =  delay <= 0 ? 0 : Math.min(delay, BACKEND_ALIVE_TIMEOUT_NANOS);
263
264         final FiniteDuration dur = FiniteDuration.fromNanos(normalized);
265         LOG.debug("{}: connection {} scheduling timeout in {}", context.persistenceId(), this, dur);
266         context.executeInActor(this::runTimer, dur);
267         haveTimer = true;
268     }
269
270     /**
271      * Check this queue for timeout and initiate reconnection if that happened. If the queue has not made progress
272      * in {@link #NO_PROGRESS_TIMEOUT_NANOS} nanoseconds, it will be aborted.
273      *
274      * @param current Current behavior
275      * @return Next behavior to use
276      */
277     @VisibleForTesting
278     final ClientActorBehavior<T> runTimer(final ClientActorBehavior<T> current) {
279         final Optional<Long> delay;
280
281         lock.lock();
282         try {
283             haveTimer = false;
284             final long now = currentTime();
285
286             LOG.debug("{}: running timer on {}", context.persistenceId(), this);
287
288             // The following line is only reliable when queue is not forwarding, but such state should not last long.
289             // FIXME: BUG-8422: this may not be accurate w.r.t. replayed entries
290             final long ticksSinceProgress = queue.ticksStalling(now);
291             if (ticksSinceProgress >= NO_PROGRESS_TIMEOUT_NANOS) {
292                 LOG.error("Queue {} has not seen progress in {} seconds, failing all requests", this,
293                     TimeUnit.NANOSECONDS.toSeconds(ticksSinceProgress));
294
295                 lockedPoison(new NoProgressException(ticksSinceProgress));
296                 current.removeConnection(this);
297                 return current;
298             }
299
300             // Requests are always scheduled in sequence, hence checking for timeout is relatively straightforward.
301             // Note we use also inquire about the delay, so we can re-schedule if needed, hence the unusual tri-state
302             // return convention.
303             delay = lockedCheckTimeout(now);
304             if (delay == null) {
305                 // We have timed out. There is no point in scheduling a timer
306                 LOG.debug("{}: connection {} timed out", context.persistenceId(), this);
307                 return lockedReconnect(current, new RuntimeRequestException("Backend connection timed out",
308                     new TimeoutException()));
309             }
310
311             if (delay.isPresent()) {
312                 // If there is new delay, schedule a timer
313                 scheduleTimer(delay.get());
314             } else {
315                 LOG.debug("{}: not scheduling timeout on {}", context.persistenceId(), this);
316             }
317         } finally {
318             lock.unlock();
319         }
320
321         return current;
322     }
323
324     @VisibleForTesting
325     final Optional<Long> checkTimeout(final long now) {
326         lock.lock();
327         try {
328             return lockedCheckTimeout(now);
329         } finally {
330             lock.unlock();
331         }
332     }
333
334     long backendSilentTicks(final long now) {
335         return now - lastReceivedTicks;
336     }
337
338     /*
339      * We are using tri-state return here to indicate one of three conditions:
340      * - if there is no timeout to schedule, return Optional.empty()
341      * - if there is a timeout to schedule, return a non-empty optional
342      * - if this connections has timed out, return null
343      */
344     @SuppressFBWarnings(value = "NP_OPTIONAL_RETURN_NULL",
345             justification = "Returning null Optional is documented in the API contract.")
346     @GuardedBy("lock")
347     private Optional<Long> lockedCheckTimeout(final long now) {
348         if (queue.isEmpty()) {
349             LOG.debug("{}: connection {} is empty", context.persistenceId(), this);
350             return Optional.empty();
351         }
352
353         final long backendSilentTicks = backendSilentTicks(now);
354         if (backendSilentTicks >= BACKEND_ALIVE_TIMEOUT_NANOS) {
355             LOG.debug("{}: Connection {} has not seen activity from backend for {} nanoseconds, timing out",
356                 context.persistenceId(), this, backendSilentTicks);
357             return null;
358         }
359
360         int tasksTimedOut = 0;
361         for (ConnectionEntry head = queue.peek(); head != null; head = queue.peek()) {
362             final long beenOpen = now - head.getEnqueuedTicks();
363             if (beenOpen < REQUEST_TIMEOUT_NANOS) {
364                 return Optional.of(REQUEST_TIMEOUT_NANOS - beenOpen);
365             }
366
367             tasksTimedOut++;
368             queue.remove(now);
369             LOG.debug("{}: Connection {} timed out entry {}", context.persistenceId(), this, head);
370
371             final double time = (beenOpen * 1.0) / 1_000_000_000;
372             head.complete(head.getRequest().toRequestFailure(
373                 new RequestTimeoutException("Timed out after " + time + "seconds")));
374         }
375
376         LOG.debug("Connection {} timed out {} tasks", this, tasksTimedOut);
377         if (tasksTimedOut != 0) {
378             queue.tryTransmit(now);
379         }
380
381         return Optional.empty();
382     }
383
384     final void poison(final RequestException cause) {
385         lock.lock();
386         try {
387             lockedPoison(cause);
388         } finally {
389             lock.unlock();
390         }
391     }
392
393     @GuardedBy("lock")
394     private void lockedPoison(final RequestException cause) {
395         poisoned = enrichPoison(cause);
396         queue.poison(cause);
397     }
398
399     RequestException enrichPoison(final RequestException ex) {
400         return ex;
401     }
402
403     @VisibleForTesting
404     final RequestException poisoned() {
405         return poisoned;
406     }
407
408     final void receiveResponse(final ResponseEnvelope<?> envelope) {
409         final long now = currentTime();
410         lastReceivedTicks = now;
411
412         final Optional<TransmittedConnectionEntry> maybeEntry;
413         lock.lock();
414         try {
415             maybeEntry = queue.complete(envelope, now);
416         } finally {
417             lock.unlock();
418         }
419
420         if (maybeEntry.isPresent()) {
421             final TransmittedConnectionEntry entry = maybeEntry.get();
422             LOG.debug("Completing {} with {}", entry, envelope);
423             entry.complete(envelope.getMessage());
424         }
425     }
426
427     @Override
428     public final String toString() {
429         return addToStringAttributes(MoreObjects.toStringHelper(this).omitNullValues()).toString();
430     }
431
432     ToStringHelper addToStringAttributes(final ToStringHelper toStringHelper) {
433         return toStringHelper.add("client", context.getIdentifier()).add("cookie", cookie).add("poisoned", poisoned);
434     }
435 }