1 | /* |
2 | * Copyright 2014-present 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 | #include <folly/io/async/HHWheelTimer.h> |
18 | |
19 | #include <cassert> |
20 | |
21 | #include <folly/Memory.h> |
22 | #include <folly/Optional.h> |
23 | #include <folly/ScopeGuard.h> |
24 | #include <folly/container/BitIterator.h> |
25 | #include <folly/io/async/Request.h> |
26 | #include <folly/lang/Bits.h> |
27 | |
28 | using std::chrono::milliseconds; |
29 | |
30 | namespace folly { |
31 | |
32 | /** |
33 | * We want to select the default interval carefully. |
34 | * An interval of 10ms will give us 10ms * WHEEL_SIZE^WHEEL_BUCKETS |
35 | * for the largest timeout possible, or about 497 days. |
36 | * |
37 | * For a lower bound, we want a reasonable limit on local IO, 10ms |
38 | * seems short enough |
39 | * |
40 | * A shorter interval also has CPU implications, less than 1ms might |
41 | * start showing up in cpu perf. Also, it might not be possible to set |
42 | * tick interval less than 10ms on older kernels. |
43 | */ |
44 | int HHWheelTimer::DEFAULT_TICK_INTERVAL = 10; |
45 | |
46 | HHWheelTimer::Callback::~Callback() { |
47 | if (isScheduled()) { |
48 | cancelTimeout(); |
49 | } |
50 | } |
51 | |
52 | void HHWheelTimer::Callback::setScheduled( |
53 | HHWheelTimer* wheel, |
54 | std::chrono::steady_clock::time_point deadline) { |
55 | assert(wheel_ == nullptr); |
56 | assert(expiration_ == decltype(expiration_){}); |
57 | |
58 | wheel_ = wheel; |
59 | expiration_ = deadline; |
60 | } |
61 | |
62 | void HHWheelTimer::Callback::cancelTimeoutImpl() { |
63 | if (--wheel_->count_ <= 0) { |
64 | assert(wheel_->count_ == 0); |
65 | wheel_->AsyncTimeout::cancelTimeout(); |
66 | } |
67 | unlink(); |
68 | if ((-1 != bucket_) && (wheel_->buckets_[0][bucket_].empty())) { |
69 | auto bi = makeBitIterator(wheel_->bitmap_.begin()); |
70 | *(bi + bucket_) = false; |
71 | } |
72 | |
73 | wheel_ = nullptr; |
74 | expiration_ = {}; |
75 | } |
76 | |
77 | HHWheelTimer::HHWheelTimer( |
78 | folly::TimeoutManager* timeoutMananger, |
79 | std::chrono::milliseconds intervalMS, |
80 | AsyncTimeout::InternalEnum internal, |
81 | std::chrono::milliseconds defaultTimeoutMS) |
82 | : AsyncTimeout(timeoutMananger, internal), |
83 | interval_(intervalMS), |
84 | defaultTimeout_(defaultTimeoutMS), |
85 | expireTick_(1), |
86 | count_(0), |
87 | startTime_(getCurTime()), |
88 | processingCallbacksGuard_(nullptr) { |
89 | bitmap_.resize((WHEEL_SIZE / sizeof(std::size_t)) / 8, 0); |
90 | } |
91 | |
92 | HHWheelTimer::~HHWheelTimer() { |
93 | // Ensure this gets done, but right before destruction finishes. |
94 | auto destructionPublisherGuard = folly::makeGuard([&] { |
95 | // Inform the subscriber that this instance is doomed. |
96 | if (processingCallbacksGuard_) { |
97 | *processingCallbacksGuard_ = true; |
98 | } |
99 | }); |
100 | cancelAll(); |
101 | } |
102 | |
103 | void HHWheelTimer::scheduleTimeoutImpl( |
104 | Callback* callback, |
105 | std::chrono::milliseconds timeout, |
106 | int64_t nextTickToProcess, |
107 | int64_t nextTick) { |
108 | int64_t due = timeToWheelTicks(timeout) + nextTick; |
109 | int64_t diff = due - nextTickToProcess; |
110 | CallbackList* list; |
111 | |
112 | auto bi = makeBitIterator(bitmap_.begin()); |
113 | |
114 | if (diff < 0) { |
115 | list = &buckets_[0][nextTick & WHEEL_MASK]; |
116 | *(bi + (nextTick & WHEEL_MASK)) = true; |
117 | callback->bucket_ = nextTick & WHEEL_MASK; |
118 | } else if (diff < WHEEL_SIZE) { |
119 | list = &buckets_[0][due & WHEEL_MASK]; |
120 | *(bi + (due & WHEEL_MASK)) = true; |
121 | callback->bucket_ = due & WHEEL_MASK; |
122 | } else if (diff < 1 << (2 * WHEEL_BITS)) { |
123 | list = &buckets_[1][(due >> WHEEL_BITS) & WHEEL_MASK]; |
124 | } else if (diff < 1 << (3 * WHEEL_BITS)) { |
125 | list = &buckets_[2][(due >> 2 * WHEEL_BITS) & WHEEL_MASK]; |
126 | } else { |
127 | /* in largest slot */ |
128 | if (diff > LARGEST_SLOT) { |
129 | diff = LARGEST_SLOT; |
130 | due = diff + nextTickToProcess; |
131 | } |
132 | list = &buckets_[3][(due >> 3 * WHEEL_BITS) & WHEEL_MASK]; |
133 | } |
134 | list->push_back(*callback); |
135 | } |
136 | |
137 | void HHWheelTimer::scheduleTimeout( |
138 | Callback* callback, |
139 | std::chrono::milliseconds timeout) { |
140 | // Cancel the callback if it happens to be scheduled already. |
141 | callback->cancelTimeout(); |
142 | callback->requestContext_ = RequestContext::saveContext(); |
143 | |
144 | count_++; |
145 | |
146 | auto now = getCurTime(); |
147 | auto nextTick = calcNextTick(now); |
148 | callback->setScheduled(this, now + timeout); |
149 | |
150 | // There are three possible scenarios: |
151 | // - we are currently inside of HHWheelTimer::timeoutExpired. In this case, |
152 | // we need to use its last tick as a base for computations |
153 | // - HHWheelTimer tick timeout is already scheduled. In this case, |
154 | // we need to use its scheduled tick as a base. |
155 | // - none of the above are true. In this case, it's safe to use the nextTick |
156 | // as a base. |
157 | int64_t baseTick = nextTick; |
158 | if (processingCallbacksGuard_ || isScheduled()) { |
159 | baseTick = std::min(expireTick_, nextTick); |
160 | } |
161 | scheduleTimeoutImpl(callback, timeout, baseTick, nextTick); |
162 | |
163 | /* If we're calling callbacks, timer will be reset after all |
164 | * callbacks are called. |
165 | */ |
166 | if (!processingCallbacksGuard_) { |
167 | scheduleNextTimeout(nextTick); |
168 | } |
169 | } |
170 | |
171 | void HHWheelTimer::scheduleTimeout(Callback* callback) { |
172 | CHECK(std::chrono::milliseconds(-1) != defaultTimeout_) |
173 | << "Default timeout was not initialized" ; |
174 | scheduleTimeout(callback, defaultTimeout_); |
175 | } |
176 | |
177 | bool HHWheelTimer::cascadeTimers(int bucket, int tick) { |
178 | CallbackList cbs; |
179 | cbs.swap(buckets_[bucket][tick]); |
180 | auto now = getCurTime(); |
181 | auto nextTick = calcNextTick(now); |
182 | while (!cbs.empty()) { |
183 | auto* cb = &cbs.front(); |
184 | cbs.pop_front(); |
185 | scheduleTimeoutImpl(cb, cb->getTimeRemaining(now), expireTick_, nextTick); |
186 | } |
187 | |
188 | // If tick is zero, timeoutExpired will cascade the next bucket. |
189 | return tick == 0; |
190 | } |
191 | |
192 | void HHWheelTimer::timeoutExpired() noexcept { |
193 | auto nextTick = calcNextTick(); |
194 | |
195 | // If the last smart pointer for "this" is reset inside the callback's |
196 | // timeoutExpired(), then the guard will detect that it is time to bail from |
197 | // this method. |
198 | auto isDestroyed = false; |
199 | // If scheduleTimeout is called from a callback in this function, it may |
200 | // cause inconsistencies in the state of this object. As such, we need |
201 | // to treat these calls slightly differently. |
202 | CHECK(!processingCallbacksGuard_); |
203 | processingCallbacksGuard_ = &isDestroyed; |
204 | auto reEntryGuard = folly::makeGuard([&] { |
205 | if (!isDestroyed) { |
206 | processingCallbacksGuard_ = nullptr; |
207 | } |
208 | }); |
209 | |
210 | // timeoutExpired() can only be invoked directly from the event base loop. |
211 | // It should never be invoked recursively. |
212 | // |
213 | while (expireTick_ < nextTick) { |
214 | int idx = expireTick_ & WHEEL_MASK; |
215 | |
216 | if (idx == 0) { |
217 | // Cascade timers |
218 | if (cascadeTimers(1, (expireTick_ >> WHEEL_BITS) & WHEEL_MASK) && |
219 | cascadeTimers(2, (expireTick_ >> (2 * WHEEL_BITS)) & WHEEL_MASK)) { |
220 | cascadeTimers(3, (expireTick_ >> (3 * WHEEL_BITS)) & WHEEL_MASK); |
221 | } |
222 | } |
223 | |
224 | auto bi = makeBitIterator(bitmap_.begin()); |
225 | *(bi + idx) = false; |
226 | |
227 | expireTick_++; |
228 | CallbackList* cbs = &buckets_[0][idx]; |
229 | while (!cbs->empty()) { |
230 | auto* cb = &cbs->front(); |
231 | cbs->pop_front(); |
232 | timeoutsToRunNow_.push_back(*cb); |
233 | } |
234 | } |
235 | |
236 | while (!timeoutsToRunNow_.empty()) { |
237 | auto* cb = &timeoutsToRunNow_.front(); |
238 | timeoutsToRunNow_.pop_front(); |
239 | count_--; |
240 | cb->wheel_ = nullptr; |
241 | cb->expiration_ = {}; |
242 | RequestContextScopeGuard rctx(cb->requestContext_); |
243 | cb->timeoutExpired(); |
244 | if (isDestroyed) { |
245 | // The HHWheelTimer itself has been destroyed. The other callbacks |
246 | // will have been cancelled from the destructor. Bail before causing |
247 | // damage. |
248 | return; |
249 | } |
250 | } |
251 | scheduleNextTimeout(expireTick_); |
252 | } |
253 | |
254 | size_t HHWheelTimer::cancelAll() { |
255 | size_t count = 0; |
256 | |
257 | if (count_ != 0) { |
258 | const std::size_t numElements = WHEEL_BUCKETS * WHEEL_SIZE; |
259 | auto maxBuckets = std::min(numElements, count_); |
260 | auto buckets = std::make_unique<CallbackList[]>(maxBuckets); |
261 | size_t countBuckets = 0; |
262 | for (auto& tick : buckets_) { |
263 | for (auto& bucket : tick) { |
264 | if (bucket.empty()) { |
265 | continue; |
266 | } |
267 | count += bucket.size(); |
268 | std::swap(bucket, buckets[countBuckets++]); |
269 | if (count >= count_) { |
270 | break; |
271 | } |
272 | } |
273 | } |
274 | |
275 | for (size_t i = 0; i < countBuckets; ++i) { |
276 | cancelTimeoutsFromList(buckets[i]); |
277 | } |
278 | // Swap the list to prevent potential recursion if cancelAll is called by |
279 | // one of the callbacks. |
280 | CallbackList timeoutsToRunNow; |
281 | timeoutsToRunNow.swap(timeoutsToRunNow_); |
282 | count += cancelTimeoutsFromList(timeoutsToRunNow); |
283 | } |
284 | |
285 | return count; |
286 | } |
287 | |
288 | void HHWheelTimer::scheduleNextTimeout(int64_t nextTick) { |
289 | int64_t tick = 1; |
290 | |
291 | if (nextTick & WHEEL_MASK) { |
292 | auto bi = makeBitIterator(bitmap_.begin()); |
293 | auto bi_end = makeBitIterator(bitmap_.end()); |
294 | auto it = folly::findFirstSet(bi + (nextTick & WHEEL_MASK), bi_end); |
295 | if (it == bi_end) { |
296 | tick = WHEEL_SIZE - ((nextTick - 1) & WHEEL_MASK); |
297 | } else { |
298 | tick = std::distance(bi + (nextTick & WHEEL_MASK), it) + 1; |
299 | } |
300 | } |
301 | |
302 | if (count_ > 0) { |
303 | if (!this->AsyncTimeout::isScheduled() || |
304 | (expireTick_ > tick + nextTick - 1)) { |
305 | this->AsyncTimeout::scheduleTimeout(interval_ * tick); |
306 | expireTick_ = tick + nextTick - 1; |
307 | } |
308 | } else { |
309 | this->AsyncTimeout::cancelTimeout(); |
310 | } |
311 | } |
312 | |
313 | size_t HHWheelTimer::cancelTimeoutsFromList(CallbackList& timeouts) { |
314 | size_t count = 0; |
315 | while (!timeouts.empty()) { |
316 | ++count; |
317 | auto& cb = timeouts.front(); |
318 | cb.cancelTimeout(); |
319 | cb.callbackCanceled(); |
320 | } |
321 | return count; |
322 | } |
323 | |
324 | int64_t HHWheelTimer::calcNextTick() { |
325 | return calcNextTick(getCurTime()); |
326 | } |
327 | |
328 | int64_t HHWheelTimer::calcNextTick( |
329 | std::chrono::steady_clock::time_point curTime) { |
330 | return (curTime - startTime_) / interval_; |
331 | } |
332 | |
333 | } // namespace folly |
334 | |