Callbacks should ref the HHWheelTimer
[folly.git] / folly / io / async / HHWheelTimer.h
1 /*
2  * Copyright 2015 Facebook, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *   http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 #pragma once
18
19 #include <folly/Optional.h>
20 #include <folly/io/async/AsyncTimeout.h>
21 #include <folly/io/async/DelayedDestruction.h>
22
23 #include <boost/intrusive/list.hpp>
24 #include <glog/logging.h>
25
26 #include <chrono>
27 #include <cstddef>
28 #include <memory>
29 #include <list>
30
31 namespace folly {
32
33 /**
34  * Hashed Hierarchical Wheel Timer
35  *
36  * Comparison:
37  * AsyncTimeout - a single timeout.
38  * HHWheelTimer - a set of efficient timeouts with different interval,
39  *    but timeouts are not exact.
40  *
41  * All of the above are O(1) in insertion, tick update and cancel
42
43  * This implementation ticks once every 10ms.
44  * We model timers as the number of ticks until the next
45  * due event.  We allow 32-bits of space to track this
46  * due interval, and break that into 4 regions of 8 bits.
47  * Each region indexes into a bucket of 256 lists.
48  *
49  * Bucket 0 represents those events that are due the soonest.
50  * Each tick causes us to look at the next list in a bucket.
51  * The 0th list in a bucket is special; it means that it is time to
52  * flush the timers from the next higher bucket and schedule them
53  * into a different bucket.
54  *
55  * This technique results in a very cheap mechanism for
56  * maintaining time and timers, provided that we can maintain
57  * a consistent rate of ticks.
58  */
59 class HHWheelTimer : private folly::AsyncTimeout,
60                      public folly::DelayedDestruction {
61  public:
62   typedef std::unique_ptr<HHWheelTimer, Destructor> UniquePtr;
63
64   template <typename... Args>
65   static UniquePtr newTimer(Args&&... args) {
66     return UniquePtr(new HHWheelTimer(std::forward<Args>(args)...));
67   }
68
69   /**
70    * A callback to be notified when a timeout has expired.
71    */
72   class Callback {
73    public:
74     Callback()
75       : wheel_(nullptr)
76       , expiration_(0) {}
77
78     virtual ~Callback();
79
80     /**
81      * timeoutExpired() is invoked when the timeout has expired.
82      */
83     virtual void timeoutExpired() noexcept = 0;
84
85     /// This callback was canceled. The default implementation is to just
86     /// proxy to `timeoutExpired` but if you care about the difference between
87     /// the timeout finishing or being canceled you can override this.
88     virtual void callbackCanceled() noexcept {
89       timeoutExpired();
90     }
91
92     /**
93      * Cancel the timeout, if it is running.
94      *
95      * If the timeout is not scheduled, cancelTimeout() does nothing.
96      */
97     void cancelTimeout() {
98       if (wheel_ == nullptr) {
99         // We're not scheduled, so there's nothing to do.
100         return;
101       }
102       cancelTimeoutImpl();
103     }
104
105     /**
106      * Return true if this timeout is currently scheduled, and false otherwise.
107      */
108     bool isScheduled() const {
109       return wheel_ != nullptr;
110     }
111
112    protected:
113     /**
114      * Don't override this unless you're doing a test. This is mainly here so
115      * that we can override it to simulate lag in steady_clock.
116      */
117     virtual std::chrono::milliseconds getCurTime() {
118       return std::chrono::duration_cast<std::chrono::milliseconds>(
119         std::chrono::steady_clock::now().time_since_epoch());
120     }
121
122    private:
123     // Get the time remaining until this timeout expires
124     std::chrono::milliseconds getTimeRemaining(
125           std::chrono::milliseconds now) const {
126       if (now >= expiration_) {
127         return std::chrono::milliseconds(0);
128       }
129       return expiration_ - now;
130     }
131
132     void setScheduled(HHWheelTimer* wheel,
133                       std::chrono::milliseconds);
134     void cancelTimeoutImpl();
135
136     HHWheelTimer* wheel_;
137     folly::Optional<DestructorGuard> wheelGuard_;
138     std::chrono::milliseconds expiration_;
139
140     typedef boost::intrusive::list_member_hook<
141       boost::intrusive::link_mode<boost::intrusive::auto_unlink> > ListHook;
142
143     ListHook hook_;
144
145     typedef boost::intrusive::list<
146       Callback,
147       boost::intrusive::member_hook<Callback, ListHook, &Callback::hook_>,
148       boost::intrusive::constant_time_size<false> > List;
149
150     std::shared_ptr<RequestContext> context_;
151
152     // Give HHWheelTimer direct access to our members so it can take care
153     // of scheduling/cancelling.
154     friend class HHWheelTimer;
155   };
156
157   /**
158    * Create a new HHWheelTimer with the specified interval and the
159    * default timeout value set.
160    *
161    * Objects created using this version of constructor can be used
162    * to schedule both variable interval timeouts using
163    * scheduleTimeout(callback, timeout) method, and default
164    * interval timeouts using scheduleTimeout(callback) method.
165    */
166   static int DEFAULT_TICK_INTERVAL;
167   explicit HHWheelTimer(folly::EventBase* eventBase,
168                         std::chrono::milliseconds intervalMS =
169                         std::chrono::milliseconds(DEFAULT_TICK_INTERVAL),
170                         AsyncTimeout::InternalEnum internal =
171                         AsyncTimeout::InternalEnum::NORMAL,
172                         std::chrono::milliseconds defaultTimeoutMS =
173                         std::chrono::milliseconds(-1));
174
175   /**
176    * Destroy the HHWheelTimer.
177    *
178    * A HHWheelTimer should only be destroyed when there are no more
179    * callbacks pending in the set. (If it helps you may use cancelAll() to
180    * cancel all pending timeouts explicitly before calling this.)
181    */
182   virtual void destroy();
183
184   /**
185    * Cancel all outstanding timeouts
186    *
187    * @returns the number of timeouts that were cancelled.
188    */
189   size_t cancelAll();
190
191   /**
192    * Get the tick interval for this HHWheelTimer.
193    *
194    * Returns the tick interval in milliseconds.
195    */
196   std::chrono::milliseconds getTickInterval() const {
197     return interval_;
198   }
199
200   /**
201    * Get the default timeout interval for this HHWheelTimer.
202    *
203    * Returns the timeout interval in milliseconds.
204    */
205   std::chrono::milliseconds getDefaultTimeout() const {
206     return defaultTimeout_;
207   }
208
209   /**
210    * Schedule the specified Callback to be invoked after the
211    * specified timeout interval.
212    *
213    * If the callback is already scheduled, this cancels the existing timeout
214    * before scheduling the new timeout.
215    */
216   void scheduleTimeout(Callback* callback,
217                        std::chrono::milliseconds timeout);
218   void scheduleTimeoutImpl(Callback* callback,
219                        std::chrono::milliseconds timeout);
220
221   /**
222    * Schedule the specified Callback to be invoked after the
223    * fefault timeout interval.
224    *
225    * If the callback is already scheduled, this cancels the existing timeout
226    * before scheduling the new timeout.
227    *
228    * This method uses CHECK() to make sure that the default timeout was
229    * specified on the object initialization.
230    */
231   void scheduleTimeout(Callback* callback);
232
233   template <class F>
234   void scheduleTimeoutFn(F fn, std::chrono::milliseconds timeout) {
235     struct Wrapper : Callback {
236       Wrapper(F f) : fn_(std::move(f)) {}
237       void timeoutExpired() noexcept override {
238         try {
239           fn_();
240         } catch (std::exception const& e) {
241           LOG(ERROR) << "HHWheelTimer timeout callback threw an exception: "
242             << e.what();
243         } catch (...) {
244           LOG(ERROR) << "HHWheelTimer timeout callback threw a non-exception.";
245         }
246         delete this;
247       }
248       F fn_;
249     };
250     Wrapper* w = new Wrapper(std::move(fn));
251     scheduleTimeout(w, timeout);
252   }
253
254   /**
255    * Return the number of currently pending timeouts
256    */
257   uint64_t count() const {
258     return count_;
259   }
260
261   /**
262    * This turns on more exact timing.  By default the wheel timer
263    * increments its cached time only once everyN (default) ticks.
264    *
265    * With catchupEveryN at 1, timeouts will only be delayed until the
266    * next tick, at which point all overdue timeouts are called.  The
267    * wheel timer is approximately 2x slower with this set to 1.
268    *
269    * Load testing in opt mode showed skew was about 1% with no catchup.
270    */
271   void setCatchupEveryN(uint32_t everyN) {
272     catchupEveryN_ = everyN;
273   }
274
275   bool isDetachable() const {
276     return !folly::AsyncTimeout::isScheduled();
277   }
278
279   using folly::AsyncTimeout::attachEventBase;
280   using folly::AsyncTimeout::detachEventBase;
281   using folly::AsyncTimeout::getTimeoutManager;
282
283  protected:
284   /**
285    * Protected destructor.
286    *
287    * Use destroy() instead.  See the comments in DelayedDestruction for more
288    * details.
289    */
290   virtual ~HHWheelTimer();
291
292  private:
293   // Forbidden copy constructor and assignment operator
294   HHWheelTimer(HHWheelTimer const &) = delete;
295   HHWheelTimer& operator=(HHWheelTimer const &) = delete;
296
297   // Methods inherited from AsyncTimeout
298   virtual void timeoutExpired() noexcept;
299
300   std::chrono::milliseconds interval_;
301   std::chrono::milliseconds defaultTimeout_;
302
303   static constexpr int WHEEL_BUCKETS = 4;
304   static constexpr int WHEEL_BITS = 8;
305   static constexpr unsigned int WHEEL_SIZE = (1 << WHEEL_BITS);
306   static constexpr unsigned int WHEEL_MASK = (WHEEL_SIZE - 1);
307   static constexpr uint32_t LARGEST_SLOT = 0xffffffffUL;
308
309   typedef Callback::List CallbackList;
310   CallbackList buckets_[WHEEL_BUCKETS][WHEEL_SIZE];
311
312   int64_t timeToWheelTicks(std::chrono::milliseconds t) {
313     return t.count() / interval_.count();
314   }
315
316   bool cascadeTimers(int bucket, int tick);
317   int64_t nextTick_;
318   uint64_t count_;
319   std::chrono::milliseconds now_;
320
321   static constexpr uint32_t DEFAULT_CATCHUP_EVERY_N = 10;
322
323   uint32_t catchupEveryN_;
324   uint32_t expirationsSinceCatchup_;
325   bool processingCallbacksGuard_;
326 };
327
328 } // folly