/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include #include #include #include #include #include #include #include #include #include namespace folly { namespace detail { template struct HHWheelTimerDurationConst; template <> struct HHWheelTimerDurationConst { static constexpr int DEFAULT_TICK_INTERVAL = 10; }; template <> struct HHWheelTimerDurationConst { static constexpr int DEFAULT_TICK_INTERVAL = 200; }; } // namespace detail /** * Hashed Hierarchical Wheel Timer * * We model timers as the number of ticks until the next * due event. We allow 32-bits of space to track this * due interval, and break that into 4 regions of 8 bits. * Each region indexes into a bucket of 256 lists. * * Bucket 0 represents those events that are due the soonest. * Each tick causes us to look at the next list in a bucket. * The 0th list in a bucket is special; it means that it is time to * flush the timers from the next higher bucket and schedule them * into a different bucket. * * This technique results in a very cheap mechanism for * maintaining time and timers. * * Unlike the original timer wheel paper, this implementation does * *not* tick constantly, and instead calculates the exact next wakeup * time. */ template class HHWheelTimerBase : private folly::AsyncTimeout, public folly::DelayedDestruction { public: using UniquePtr = std::unique_ptr; using SharedPtr = std::shared_ptr; template static UniquePtr newTimer(Args&&... args) { return UniquePtr(new HHWheelTimerBase(std::forward(args)...)); } /** * A callback to be notified when a timeout has expired. */ class Callback : public boost::intrusive::list_base_hook< boost::intrusive::link_mode> { public: Callback(); virtual ~Callback(); /** * timeoutExpired() is invoked when the timeout has expired. */ virtual void timeoutExpired() noexcept = 0; /// This callback was canceled. The default implementation is to just /// proxy to `timeoutExpired` but if you care about the difference between /// the timeout finishing or being canceled you can override this. virtual void callbackCanceled() noexcept { timeoutExpired(); } /** * Cancel the timeout, if it is running. * * If the timeout is not scheduled, cancelTimeout() does nothing. */ void cancelTimeout() { if (wheel_ == nullptr) { // We're not scheduled, so there's nothing to do. return; } cancelTimeoutImpl(); } /** * Return true if this timeout is currently scheduled, and false otherwise. */ bool isScheduled() const { return wheel_ != nullptr; } /** * Get the time remaining until this timeout expires. Return 0 if this * timeout is not scheduled or expired. Otherwise, return expiration * time minus current time. */ Duration getTimeRemaining() const { return getTimeRemaining(std::chrono::steady_clock::now()); } private: // Get the time remaining until this timeout expires Duration getTimeRemaining(std::chrono::steady_clock::time_point now) const { if (now >= expiration_) { return Duration(0); } return std::chrono::duration_cast(expiration_ - now); } void setScheduled( HHWheelTimerBase* wheel, std::chrono::steady_clock::time_point deadline); void cancelTimeoutImpl(); HHWheelTimerBase* wheel_{nullptr}; std::chrono::steady_clock::time_point expiration_{}; int bucket_{-1}; typedef boost::intrusive:: list> List; std::shared_ptr requestContext_; // Give HHWheelTimerBase direct access to our members so it can take care // of scheduling/cancelling. friend class HHWheelTimerBase; }; /** * Create a new HHWheelTimerBase with the specified interval and the * default timeout value set. * * Objects created using this version of constructor can be used * to schedule both variable interval timeouts using * scheduleTimeout(callback, timeout) method, and default * interval timeouts using scheduleTimeout(callback) method. */ static int DEFAULT_TICK_INTERVAL; explicit HHWheelTimerBase( folly::TimeoutManager* timeoutMananger, Duration intervalDuration = Duration(DEFAULT_TICK_INTERVAL), AsyncTimeout::InternalEnum internal = AsyncTimeout::InternalEnum::NORMAL, Duration defaultTimeoutDuration = Duration(-1)); /** * Cancel all outstanding timeouts * * @returns the number of timeouts that were cancelled. */ size_t cancelAll(); /** * Get the tick interval for this HHWheelTimerBase. * * Returns the tick interval in milliseconds. */ Duration getTickInterval() const { return interval_; } /** * Get the default timeout interval for this HHWheelTimerBase. * * Returns the timeout interval in milliseconds. */ Duration getDefaultTimeout() const { return defaultTimeout_; } /** * Set the default timeout interval for this HHWheelTimerBase. */ void setDefaultTimeout(Duration timeout) { defaultTimeout_ = timeout; } /** * Schedule the specified Callback to be invoked after the * specified timeout interval. * * If the callback is already scheduled, this cancels the existing timeout * before scheduling the new timeout. */ void scheduleTimeout(Callback* callback, Duration timeout); /** * Schedule the specified Callback to be invoked after the * default timeout interval. * * If the callback is already scheduled, this cancels the existing timeout * before scheduling the new timeout. * * This method uses CHECK() to make sure that the default timeout was * specified on the object initialization. */ void scheduleTimeout(Callback* callback); template void scheduleTimeoutFn(F fn, Duration timeout) { struct Wrapper : Callback { Wrapper(F f) : fn_(std::move(f)) {} void timeoutExpired() noexcept override { try { fn_(); } catch (std::exception const& e) { LOG(ERROR) << "HHWheelTimerBase timeout callback threw an exception: " << e.what(); } catch (...) { LOG(ERROR) << "HHWheelTimerBase timeout callback threw a non-exception."; } delete this; } F fn_; }; Wrapper* w = new Wrapper(std::move(fn)); scheduleTimeout(w, timeout); } /** * Return the number of currently pending timeouts */ std::size_t count() const { return count_; } bool isDetachable() const { return !folly::AsyncTimeout::isScheduled(); } using folly::AsyncTimeout::attachEventBase; using folly::AsyncTimeout::detachEventBase; using folly::AsyncTimeout::getTimeoutManager; protected: /** * Protected destructor. * * Use destroy() instead. See the comments in DelayedDestruction for more * details. */ ~HHWheelTimerBase() override; private: // Forbidden copy constructor and assignment operator HHWheelTimerBase(HHWheelTimerBase const&) = delete; HHWheelTimerBase& operator=(HHWheelTimerBase const&) = delete; // Methods inherited from AsyncTimeout void timeoutExpired() noexcept override; Duration interval_; Duration defaultTimeout_; static constexpr int WHEEL_BUCKETS = 4; static constexpr int WHEEL_BITS = 8; static constexpr unsigned int WHEEL_SIZE = (1 << WHEEL_BITS); static constexpr unsigned int WHEEL_MASK = (WHEEL_SIZE - 1); static constexpr uint32_t LARGEST_SLOT = 0xffffffffUL; typedef typename Callback::List CallbackList; CallbackList buckets_[WHEEL_BUCKETS][WHEEL_SIZE]; std::array bitmap_; int64_t timeToWheelTicks(Duration t) { return t.count() / interval_.count(); } bool cascadeTimers( int bucket, int tick, std::chrono::steady_clock::time_point curTime); void scheduleTimeoutInternal(Duration timeout); int64_t expireTick_; std::size_t count_; std::chrono::steady_clock::time_point startTime_; int64_t calcNextTick(); int64_t calcNextTick(std::chrono::steady_clock::time_point curTime); static bool inSameEpoch(int64_t tickA, int64_t tickB) { return (tickA >> WHEEL_BITS) == (tickB >> WHEEL_BITS); } /** * Schedule a given timeout by putting it into the appropriate bucket of the * wheel. * * @param callback Callback to fire after `timeout` * @param dueTick Tick at which the timer is due. * @param nextTickToProcess next tick that was not processed by the timer * yet. Can be less than nextTick if we're lagging. * @param nextTick next tick based on the actual time */ void scheduleTimeoutImpl( Callback* callback, int64_t dueTick, int64_t nextTickToProcess, int64_t nextTick); /** * Compute next required wheel tick to fire and schedule the timeout for that * tick. * * @param nextTick next tick based on the actual time */ void scheduleNextTimeout(int64_t nextTick); /** * Schedule next wheel timeout in a fixed number of wheel ticks. * * @param nextTick next tick based on the actual time * @param ticks number of ticks in which the timer should fire */ void scheduleNextTimeout(int64_t nextTick, int64_t ticks); size_t cancelTimeoutsFromList(CallbackList& timeouts); bool* processingCallbacksGuard_; // Timeouts that we're about to run. They're already extracted from their // corresponding buckets, so we need this list for the `cancelAll` to be able // to cancel them. CallbackList timeoutsToRunNow_; std::chrono::steady_clock::time_point getCurTime() { return std::chrono::steady_clock::now(); } }; // std::chrono::milliseconds using HHWheelTimer = HHWheelTimerBase; extern template class HHWheelTimerBase; // std::chrono::microseconds template <> void HHWheelTimerBase::scheduleTimeoutInternal( std::chrono::microseconds timeout); using HHWheelTimerHighRes = HHWheelTimerBase; extern template class HHWheelTimerBase; } // namespace folly