/*
* Copyright (c) Meta Platforms, Inc. and 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.
*/
#include <folly/io/async/HHWheelTimer.h>
#include <folly/io/async/EventBase.h>
#include <folly/io/async/test/UndelayedDestruction.h>
#include <folly/io/async/test/Util.h>
#include <folly/portability/GTest.h>
using namespace folly;
using std::chrono::milliseconds;
typedef UndelayedDestruction<HHWheelTimer> StackWheelTimer;
class TestTimeout : public HHWheelTimer::Callback {
public:
TestTimeout() {}
TestTimeout(HHWheelTimer* t, milliseconds timeout) {
t->scheduleTimeout(this, timeout);
}
void timeoutExpired() noexcept override {
timestamps.emplace_back();
if (fn) {
fn();
}
}
void callbackCanceled() noexcept override {
canceledTimestamps.emplace_back();
if (fn) {
fn();
}
}
std::deque<TimePoint> timestamps;
std::deque<TimePoint> canceledTimestamps;
std::function<void()> fn;
};
struct HHWheelTimerTest : public ::testing::Test {
EventBase eventBase;
};
/*
* Test firing some simple timeouts that are fired once and never rescheduled
*/
TEST_F(HHWheelTimerTest, FireOnce) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, milliseconds(5));
t.scheduleTimeout(&t2, milliseconds(5));
// Verify scheduling it twice cancels, then schedules.
// Should only get one callback.
t.scheduleTimeout(&t2, milliseconds(5));
t.scheduleTimeout(&t3, milliseconds(10));
ASSERT_EQ(t.count(), 3);
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
ASSERT_EQ(t3.timestamps.size(), 1);
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(5));
T_CHECK_TIMEOUT(start, t2.timestamps[0], milliseconds(5));
T_CHECK_TIMEOUT(start, t3.timestamps[0], milliseconds(10));
T_CHECK_TIMEOUT(start, end, milliseconds(10));
}
TEST_F(HHWheelTimerTest, NoRequestContextLeak) {
StackWheelTimer t(&eventBase, milliseconds(1));
std::set<int> destructed;
class TestData : public RequestData {
public:
TestData(int data, std::set<int>& destructed)
: data_(data), destructed_(destructed) {}
~TestData() override { destructed_.insert(data_); }
bool hasCallback() override { return false; }
private:
int data_;
std::set<int>& destructed_;
};
folly::Optional<TestTimeout> t1 = TestTimeout{};
folly::Optional<TestTimeout> t2 = TestTimeout{};
{
RequestContextScopeGuard g;
RequestContext::get()->setContextData(
"k", std::make_unique<TestData>(1, destructed));
t.scheduleTimeout(&*t1, milliseconds(5));
}
{
RequestContextScopeGuard g;
RequestContext::get()->setContextData(
"k", std::make_unique<TestData>(2, destructed));
t.scheduleTimeout(&*t2, milliseconds(5));
}
EXPECT_EQ(0, destructed.size());
t1.reset();
EXPECT_EQ(1, destructed.count(1));
EXPECT_EQ(0, destructed.count(2));
}
/*
* Test scheduling a timeout from another timeout callback.
*/
TEST_F(HHWheelTimerTest, TestSchedulingWithinCallback) {
HHWheelTimer& t = eventBase.timer();
TestTimeout t1, t2;
t.scheduleTimeout(&t1, milliseconds(500));
t1.fn = [&] {
t.scheduleTimeout(&t2, milliseconds(1));
std::this_thread::sleep_for(std::chrono::milliseconds(5));
};
// If t is in an inconsistent state, detachEventBase should fail.
t2.fn = [&] { t.detachEventBase(); };
ASSERT_EQ(t.count(), 1);
eventBase.loop();
ASSERT_EQ(t.count(), 0);
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
}
/*
* Test changing default-timeout in timer.
*/
TEST_F(HHWheelTimerTest, TestSetDefaultTimeout) {
HHWheelTimer& t = eventBase.timer();
t.setDefaultTimeout(milliseconds(1000));
// verify: default-time has been modified
ASSERT_EQ(t.getDefaultTimeout(), milliseconds(1000));
}
/*
* Test cancelling a timeout when it is scheduled to be fired right away.
*/
TEST_F(HHWheelTimerTest, CancelTimeout) {
StackWheelTimer t(&eventBase, milliseconds(1));
// Create several timeouts that will all fire in 5ms.
TestTimeout t5_1(&t, milliseconds(5));
TestTimeout t5_2(&t, milliseconds(5));
TestTimeout t5_3(&t, milliseconds(5));
TestTimeout t5_4(&t, milliseconds(5));
TestTimeout t5_5(&t, milliseconds(5));
// Also create a few timeouts to fire in 10ms
TestTimeout t10_1(&t, milliseconds(10));
TestTimeout t10_2(&t, milliseconds(10));
TestTimeout t10_3(&t, milliseconds(10));
TestTimeout t20_1(&t, milliseconds(20));
TestTimeout t20_2(&t, milliseconds(20));
// Have t5_1 cancel t5_2 and t5_4.
//
// Cancelling t5_2 will test cancelling a timeout that is at the head of the
// list and ready to be fired.
//
// Cancelling t5_4 will test cancelling a timeout in the middle of the list
t5_1.fn = [&] {
t5_2.cancelTimeout();
t5_4.cancelTimeout();
};
// Have t5_3 cancel t5_5.
// This will test cancelling the last remaining timeout.
//
// Then have t5_3 reschedule itself.
t5_3.fn = [&] {
t5_5.cancelTimeout();
// Reset our function so we won't continually reschedule ourself
std::function<void()> fnDtorGuard;
t5_3.fn.swap(fnDtorGuard);
t.scheduleTimeout(&t5_3, milliseconds(5));
// Also test cancelling timeouts in another timeset that isn't ready to
// fire yet.
//
// Cancel the middle timeout in ts10.
t10_2.cancelTimeout();
// Cancel both the timeouts in ts20.
t20_1.cancelTimeout();
t20_2.cancelTimeout();
};
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t5_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_1.timestamps[0], milliseconds(5));
ASSERT_EQ(t5_3.timestamps.size(), 2);
T_CHECK_TIMEOUT(start, t5_3.timestamps[0], milliseconds(5));
T_CHECK_TIMEOUT(t5_3.timestamps[0], t5_3.timestamps[1], milliseconds(5));
ASSERT_EQ(t10_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t10_1.timestamps[0], milliseconds(10));
ASSERT_EQ(t10_3.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t10_3.timestamps[0], milliseconds(10));
// Cancelled timeouts
ASSERT_EQ(t5_2.timestamps.size(), 0);
ASSERT_EQ(t5_4.timestamps.size(), 0);
ASSERT_EQ(t5_5.timestamps.size(), 0);
ASSERT_EQ(t10_2.timestamps.size(), 0);
ASSERT_EQ(t20_1.timestamps.size(), 0);
ASSERT_EQ(t20_2.timestamps.size(), 0);
T_CHECK_TIMEOUT(start, end, milliseconds(10));
}
/*
* Test destroying a HHWheelTimer with timeouts outstanding
*/
TEST_F(HHWheelTimerTest, DestroyTimeoutSet) {
HHWheelTimer::UniquePtr t(
HHWheelTimer::newTimer(&eventBase, milliseconds(1)));
TestTimeout t5_1(t.get(), milliseconds(5));
TestTimeout t5_2(t.get(), milliseconds(5));
TestTimeout t5_3(t.get(), milliseconds(5));
TestTimeout t10_1(t.get(), milliseconds(10));
TestTimeout t10_2(t.get(), milliseconds(10));
// Have t5_2 destroy t
// Note that this will call destroy() inside t's timeoutExpired()
// method.
t5_2.fn = [&] {
t5_3.cancelTimeout();
t5_1.cancelTimeout();
t10_1.cancelTimeout();
t10_2.cancelTimeout();
t.reset();
};
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t5_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_1.timestamps[0], milliseconds(5));
ASSERT_EQ(t5_2.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_2.timestamps[0], milliseconds(5));
ASSERT_EQ(t5_3.timestamps.size(), 0);
ASSERT_EQ(t10_1.timestamps.size(), 0);
ASSERT_EQ(t10_2.timestamps.size(), 0);
T_CHECK_TIMEOUT(start, end, milliseconds(5));
}
/*
* Test an event scheduled before the last event fires on time
*/
TEST_F(HHWheelTimerTest, SlowFast) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, milliseconds(10));
t.scheduleTimeout(&t2, milliseconds(5));
ASSERT_EQ(t.count(), 2);
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(10));
T_CHECK_TIMEOUT(start, t2.timestamps[0], milliseconds(5));
}
TEST_F(HHWheelTimerTest, ReschedTest) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, milliseconds(128));
TimePoint start2;
t1.fn = [&]() {
t.scheduleTimeout(&t2, milliseconds(255)); // WHEEL_SIZE - 1
start2.reset();
ASSERT_EQ(t.count(), 1);
};
ASSERT_EQ(t.count(), 1);
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(128));
T_CHECK_TIMEOUT(start2, t2.timestamps[0], milliseconds(255));
}
TEST_F(HHWheelTimerTest, DeleteWheelInTimeout) {
auto t = HHWheelTimer::newTimer(&eventBase, milliseconds(1));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t->count(), 0);
t->scheduleTimeout(&t1, milliseconds(128));
t->scheduleTimeout(&t2, milliseconds(128));
t->scheduleTimeout(&t3, milliseconds(128));
t1.fn = [&]() { t2.cancelTimeout(); };
t3.fn = [&]() { t.reset(); };
ASSERT_EQ(t->count(), 3);
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(128));
}
/*
* Test scheduling a mix of timers with default timeout and variable timeout.
*/
TEST_F(HHWheelTimerTest, DefaultTimeout) {
milliseconds defaultTimeout(milliseconds(5));
StackWheelTimer t(
&eventBase,
milliseconds(1),
AsyncTimeout::InternalEnum::NORMAL,
defaultTimeout);
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
ASSERT_EQ(t.getDefaultTimeout(), defaultTimeout);
t.scheduleTimeout(&t1);
t.scheduleTimeout(&t2, milliseconds(10));
ASSERT_EQ(t.count(), 2);
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], defaultTimeout);
T_CHECK_TIMEOUT(start, t2.timestamps[0], milliseconds(10));
T_CHECK_TIMEOUT(start, end, milliseconds(10));
}
TEST_F(HHWheelTimerTest, lambda) {
StackWheelTimer t(&eventBase, milliseconds(1));
size_t count = 0;
t.scheduleTimeoutFn([&] { count++; }, milliseconds(1));
eventBase.loop();
EXPECT_EQ(1, count);
}
// shouldn't crash because we swallow and log the error (you'll have to look
// at the console to confirm logging)
TEST_F(HHWheelTimerTest, lambdaThrows) {
StackWheelTimer t(&eventBase, milliseconds(1));
t.scheduleTimeoutFn(
[&] { throw std::runtime_error("expected"); }, milliseconds(1));
eventBase.loop();
}
TEST_F(HHWheelTimerTest, cancelAll) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout t1;
TestTimeout t2;
t.scheduleTimeout(&t1, std::chrono::milliseconds(1));
t.scheduleTimeout(&t2, std::chrono::milliseconds(1));
size_t canceled = 0;
t1.fn = [&] { canceled += t.cancelAll(); };
t2.fn = [&] { canceled += t.cancelAll(); };
// Sleep 20ms to ensure both timeouts will fire in a single event (in case
// they ended up in different slots)
::usleep(20000);
eventBase.loop();
EXPECT_EQ(1, t1.canceledTimestamps.size() + t2.canceledTimestamps.size());
EXPECT_EQ(1, canceled);
}
TEST_F(HHWheelTimerTest, IntrusivePtr) {
HHWheelTimer::UniquePtr t(
HHWheelTimer::newTimer(&eventBase, milliseconds(1)));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t->count(), 0);
t->scheduleTimeout(&t1, milliseconds(5));
t->scheduleTimeout(&t2, milliseconds(5));
DelayedDestruction::IntrusivePtr<HHWheelTimer> s(t);
s->scheduleTimeout(&t3, milliseconds(10));
ASSERT_EQ(t->count(), 3);
// Kill the UniquePtr, but the SharedPtr keeps it alive
t.reset();
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 1);
ASSERT_EQ(t3.timestamps.size(), 1);
ASSERT_EQ(s->count(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], milliseconds(5));
T_CHECK_TIMEOUT(start, t2.timestamps[0], milliseconds(5));
T_CHECK_TIMEOUT(start, t3.timestamps[0], milliseconds(10));
T_CHECK_TIMEOUT(start, end, milliseconds(10));
}
TEST_F(HHWheelTimerTest, GetTimeRemaining) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout t1;
// Not scheduled yet, time remaining should be zero
ASSERT_EQ(t1.getTimeRemaining(), milliseconds(0));
ASSERT_EQ(t.count(), 0);
// Scheduled, time remaining should be less than or equal to the scheduled
// timeout
t.scheduleTimeout(&t1, milliseconds(10));
ASSERT_LE(t1.getTimeRemaining(), milliseconds(10));
TimePoint start;
eventBase.loop();
TimePoint end;
// Expired and time remaining should be zero
ASSERT_EQ(t1.getTimeRemaining(), milliseconds(0));
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, end, milliseconds(10));
}
TEST_F(HHWheelTimerTest, prematureTimeout) {
StackWheelTimer t(&eventBase, milliseconds(10));
TestTimeout t1;
TestTimeout t2;
// Schedule the timeout for the nextTick of timer
t.scheduleTimeout(&t1, std::chrono::milliseconds(1));
// Make sure that time is past that tick.
::usleep(10000);
// Schedule the timeout for the +255 tick, due to sleep above it will overlap
// with what would be ran on the next timeoutExpired of the timer.
auto timeout = std::chrono::milliseconds(2555);
t.scheduleTimeout(&t2, std::chrono::milliseconds(2555));
auto start = std::chrono::steady_clock::now();
eventBase.loop();
ASSERT_EQ(t2.timestamps.size(), 1);
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
t2.timestamps[0].getTime() - start);
EXPECT_GE(elapsedMs.count(), timeout.count());
}
TEST_F(HHWheelTimerTest, Level1) {
StackWheelTimer t(&eventBase, milliseconds(1));
TestTimeout tt;
// Schedule the timeout for the tick in a next epoch.
t.scheduleTimeout(&tt, std::chrono::milliseconds(500));
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(tt.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, end, milliseconds(500));
}
// Test that we handle negative timeouts properly (i.e. treat them as 0)
TEST_F(HHWheelTimerTest, NegativeTimeout) {
StackWheelTimer t(&eventBase, milliseconds(1));
std::this_thread::sleep_for(std::chrono::milliseconds(10));
TestTimeout tt1;
TestTimeout tt2;
// Make sure we have event scheduled.
t.scheduleTimeout(&tt1, std::chrono::milliseconds(1));
// Schedule another timeout that would appear to be earlier than
// the already scheduled one.
t.scheduleTimeout(&tt2, std::chrono::milliseconds(-500000000));
TimePoint start;
eventBase.loop();
TimePoint end;
ASSERT_EQ(tt2.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, end, milliseconds(1));
}
TEST(HHWheelTimerDetailsTest, Divider) {
auto no_overflow_add = [](uint64_t& base, int offset) -> bool {
if (offset >= 0 || static_cast<unsigned int>(-offset) < base) {
base += offset;
return true;
}
return false;
};
for (uint64_t denShift = 1; denShift <= 60; denShift++) {
for (int denOffset = -10; denOffset <= 10; denOffset++) {
uint64_t den = uint64_t(1) << denShift;
if (!no_overflow_add(den, denOffset)) {
continue;
}
folly::detail::HHWheelTimerDurationInterval<
std::chrono::milliseconds>::Divider divider{den};
for (uint64_t numShift = 3; numShift <= 58; numShift++) {
for (int numOffset = -10; numOffset <= 10; numOffset++) {
uint64_t num = (1LLU << numShift);
if (!no_overflow_add(num, numOffset)) {
continue;
}
int allowedError = 0;
if (numShift >= 32 || denShift >= 32) {
allowedError = 1;
}
auto expected = num / den;
auto calc = divider.divide(num);
// either it is accurate or the result is overstated by allowedError
EXPECT_TRUE(
expected == calc ||
(calc >= expected && calc <= expected + allowedError))
<< "expected=" << expected << " calc=" << calc
<< " allowedError=" << allowedError << ": " << num << "/" << den
<< " ns=" << numShift << " + " << numOffset << " ds=" << denShift
<< " + " << denOffset;
}
}
}
}
for (uint64_t den = 1; den <= 10000; den++) {
folly::detail::HHWheelTimerDurationInterval<
std::chrono::milliseconds>::Divider divider{den};
for (uint64_t num = 0; num <= 10000; num++) {
EXPECT_EQ(num / den, divider.divide(num)) << num << "/" << den;
}
}
}