/*
* 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/STTimerFDTimeoutManager.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::microseconds;
typedef UndelayedDestruction<HHWheelTimerHighRes> StackWheelTimer;
class TestTimeout : public HHWheelTimerHighRes::Callback {
public:
TestTimeout() {}
TestTimeout(HHWheelTimerHighRes* t, microseconds 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 HHWheelTimerHighResTest : public ::testing::Test {
HHWheelTimerHighResTest() : timeoutMgr(&evb) {}
EventBase evb;
STTimerFDTimeoutManager timeoutMgr;
};
/*
* Test firing some simple timeouts that are fired once and never rescheduled
*/
TEST_F(HHWheelTimerHighResTest, FireOnce) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, microseconds(50));
t.scheduleTimeout(&t2, microseconds(50));
// Verify scheduling it twice cancels, then schedules.
// Should only get one callback.
t.scheduleTimeout(&t2, microseconds(50));
t.scheduleTimeout(&t3, microseconds(100));
ASSERT_EQ(t.count(), 3);
TestTimeout ts;
ts.fn = [&]() { evb.terminateLoopSoon(); };
t.scheduleTimeout(&ts, microseconds(1000));
TimePoint start;
evb.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], microseconds(500));
T_CHECK_TIMEOUT(start, t2.timestamps[0], microseconds(500));
T_CHECK_TIMEOUT(start, t3.timestamps[0], microseconds(10));
T_CHECK_TIMEOUT(start, end, microseconds(10));
}
/*
* Test scheduling a timeout from another timeout callback.
*/
TEST_F(HHWheelTimerHighResTest, TestSchedulingWithinCallback) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1, t2;
t.scheduleTimeout(&t1, microseconds(500 * 1000));
t1.fn = [&] {
t.scheduleTimeout(&t2, microseconds(1000));
std::this_thread::sleep_for(std::chrono::microseconds(5000));
};
t2.fn = [&] { evb.terminateLoopSoon(); };
ASSERT_EQ(t.count(), 1);
evb.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(HHWheelTimerHighResTest, TestSetDefaultTimeout) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
t.setDefaultTimeout(microseconds(1000));
// verify: default-time has been modified
ASSERT_EQ(t.getDefaultTimeout(), microseconds(1000));
}
/*
* Test cancelling a timeout when it is scheduled to be fired right away.
*/
TEST_F(HHWheelTimerHighResTest, CancelTimeout) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
// Create several timeouts that will all fire in 5microseconds
TestTimeout t5_1(&t, microseconds(50));
TestTimeout t5_2(&t, microseconds(50));
TestTimeout t5_3(&t, microseconds(50));
TestTimeout t5_4(&t, microseconds(50));
TestTimeout t5_5(&t, microseconds(50));
// Also create a few timeouts to fire in 10microseconds
TestTimeout t10_1(&t, microseconds(100));
TestTimeout t10_2(&t, microseconds(100));
TestTimeout t10_3(&t, microseconds(100));
TestTimeout t20_1(&t, microseconds(200));
TestTimeout t20_2(&t, microseconds(200));
TestTimeout et(&t, microseconds(1000));
et.fn = [&] { evb.terminateLoopSoon(); };
// 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, microseconds(500));
// 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;
evb.loop();
TimePoint end;
ASSERT_EQ(t5_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_1.timestamps[0], microseconds(500));
ASSERT_EQ(t5_3.timestamps.size(), 2);
T_CHECK_TIMEOUT(start, t5_3.timestamps[0], microseconds(500));
T_CHECK_TIMEOUT(t5_3.timestamps[0], t5_3.timestamps[1], microseconds(500));
ASSERT_EQ(t10_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t10_1.timestamps[0], microseconds(10));
ASSERT_EQ(t10_3.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t10_3.timestamps[0], microseconds(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, microseconds(10));
}
/*
* Test destroying a HHWheelTimerHighRes with timeouts outstanding
*/
TEST_F(HHWheelTimerHighResTest, DestroyTimeoutSet) {
HHWheelTimerHighRes::UniquePtr t(
HHWheelTimerHighRes::newTimer(&timeoutMgr, microseconds(100)));
TestTimeout t5_1(t.get(), microseconds(50));
TestTimeout t5_2(t.get(), microseconds(50));
TestTimeout t5_3(t.get(), microseconds(50));
TestTimeout t10_1(t.get(), microseconds(100));
TestTimeout t10_2(t.get(), microseconds(100));
// 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();
evb.terminateLoopSoon();
};
TimePoint start;
evb.loop();
TimePoint end;
ASSERT_EQ(t5_1.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_1.timestamps[0], microseconds(500));
ASSERT_EQ(t5_2.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, t5_2.timestamps[0], microseconds(500));
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, microseconds(500));
}
/*
* Test an event scheduled before the last event fires on time
*/
TEST_F(HHWheelTimerHighResTest, SlowFast) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, microseconds(100));
t.scheduleTimeout(&t2, microseconds(50));
ASSERT_EQ(t.count(), 2);
TestTimeout et(&t, microseconds(1000));
et.fn = [&] { evb.terminateLoopSoon(); };
TimePoint start;
evb.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], microseconds(100));
T_CHECK_TIMEOUT(start, t2.timestamps[0], microseconds(50));
}
TEST_F(HHWheelTimerHighResTest, ReschedTest) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
t.scheduleTimeout(&t1, microseconds(12800));
TimePoint start2;
t1.fn = [&]() {
t.scheduleTimeout(&t2, microseconds(25500));
start2.reset();
ASSERT_EQ(t.count(), 2); // we scheduled et
};
ASSERT_EQ(t.count(), 1);
TestTimeout et(&t, microseconds(100000));
et.fn = [&] { evb.terminateLoopSoon(); };
TimePoint start;
evb.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], microseconds(12800));
T_CHECK_TIMEOUT(start2, t2.timestamps[0], microseconds(25500));
}
TEST_F(HHWheelTimerHighResTest, DeleteWheelInTimeout) {
auto t = HHWheelTimerHighRes::newTimer(&timeoutMgr, microseconds(100));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t->count(), 0);
t->scheduleTimeout(&t1, microseconds(128));
t->scheduleTimeout(&t2, microseconds(128));
t->scheduleTimeout(&t3, microseconds(128));
t1.fn = [&]() { t2.cancelTimeout(); };
t3.fn = [&]() {
t.reset();
evb.terminateLoopSoon();
};
ASSERT_EQ(t->count(), 3);
TimePoint start;
evb.loop();
TimePoint end;
ASSERT_EQ(t1.timestamps.size(), 1);
ASSERT_EQ(t2.timestamps.size(), 0);
T_CHECK_TIMEOUT(start, t1.timestamps[0], microseconds(128));
}
/*
* Test scheduling a mix of timers with default timeout and variable timeout.
*/
TEST_F(HHWheelTimerHighResTest, DefaultTimeout) {
microseconds defaultTimeout(microseconds(500));
StackWheelTimer t(
&timeoutMgr,
microseconds(100),
AsyncTimeout::InternalEnum::NORMAL,
defaultTimeout);
TestTimeout t1;
TestTimeout t2;
ASSERT_EQ(t.count(), 0);
ASSERT_EQ(t.getDefaultTimeout(), defaultTimeout);
t.scheduleTimeout(&t1);
t.scheduleTimeout(&t2, microseconds(10));
ASSERT_EQ(t.count(), 2);
TestTimeout et(&t, microseconds(1000));
et.fn = [&] { evb.terminateLoopSoon(); };
TimePoint start;
evb.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], microseconds(10));
T_CHECK_TIMEOUT(start, end, microseconds(10));
}
TEST_F(HHWheelTimerHighResTest, lambda) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
size_t count = 0;
t.scheduleTimeoutFn([&] { count++; }, microseconds(100));
TestTimeout et(&t, microseconds(1000));
et.fn = [&] { evb.terminateLoopSoon(); };
evb.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(HHWheelTimerHighResTest, lambdaThrows) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
size_t count = 0;
t.scheduleTimeoutFn(
[&] {
count++;
throw std::runtime_error("expected");
},
microseconds(100));
TestTimeout et(&t, microseconds(1000));
et.fn = [&] { evb.terminateLoopSoon(); };
evb.loop();
// make sure the callback was invoked
EXPECT_EQ(1, count);
}
TEST_F(HHWheelTimerHighResTest, cancelAll) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1;
TestTimeout t2;
t.scheduleTimeout(&t1, std::chrono::microseconds(1000));
t.scheduleTimeout(&t2, std::chrono::microseconds(1000));
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);
evb.scheduleAt(
[&]() { evb.terminateLoopSoon(); },
std::chrono::steady_clock::now() + std::chrono::milliseconds(100));
evb.loop();
EXPECT_EQ(1, t1.canceledTimestamps.size() + t2.canceledTimestamps.size());
EXPECT_EQ(1, canceled);
}
TEST_F(HHWheelTimerHighResTest, IntrusivePtr) {
HHWheelTimerHighRes::UniquePtr t(
HHWheelTimerHighRes::newTimer(&timeoutMgr, microseconds(100)));
TestTimeout t1;
TestTimeout t2;
TestTimeout t3;
ASSERT_EQ(t->count(), 0);
t->scheduleTimeout(&t1, microseconds(500));
t->scheduleTimeout(&t2, microseconds(500));
DelayedDestruction::IntrusivePtr<HHWheelTimerHighRes> s(t);
s->scheduleTimeout(&t3, microseconds(10));
ASSERT_EQ(t->count(), 3);
// Kill the UniquePtr, but the SharedPtr keeps it alive
t.reset();
evb.scheduleAt(
[&]() { evb.terminateLoopSoon(); },
std::chrono::steady_clock::now() + std::chrono::milliseconds(10));
TimePoint start;
evb.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], microseconds(500));
T_CHECK_TIMEOUT(start, t2.timestamps[0], microseconds(500));
T_CHECK_TIMEOUT(start, t3.timestamps[0], microseconds(10));
T_CHECK_TIMEOUT(start, end, std::chrono::milliseconds(10)); // this is right
}
TEST_F(HHWheelTimerHighResTest, GetTimeRemaining) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout t1;
// Not scheduled yet, time remaining should be zero
ASSERT_EQ(t1.getTimeRemaining(), microseconds(0));
ASSERT_EQ(t.count(), 0);
// Scheduled, time remaining should be less than or equal to the scheduled
// timeout
t.scheduleTimeout(&t1, microseconds(10));
ASSERT_LE(t1.getTimeRemaining(), microseconds(10));
t1.fn = [&] { evb.terminateLoopSoon(); };
TimePoint start;
evb.loop();
TimePoint end;
// Expired and time remaining should be zero
ASSERT_EQ(t1.getTimeRemaining(), microseconds(0));
ASSERT_EQ(t.count(), 0);
T_CHECK_TIMEOUT(start, end, microseconds(10));
}
TEST_F(HHWheelTimerHighResTest, prematureTimeout) {
StackWheelTimer t(&timeoutMgr, microseconds(10));
TestTimeout t1;
TestTimeout t2;
// Schedule the timeout for the nextTick of timer
t.scheduleTimeout(&t1, std::chrono::microseconds(100));
// 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::microseconds(2555);
t.scheduleTimeout(&t2, std::chrono::microseconds(2555));
t2.fn = [&] { evb.terminateLoopSoon(); };
auto start = std::chrono::steady_clock::now();
evb.loop();
ASSERT_EQ(t2.timestamps.size(), 1);
auto elapsedUs = std::chrono::duration_cast<std::chrono::microseconds>(
t2.timestamps[0].getTime() - start);
EXPECT_GE(elapsedUs.count(), timeout.count());
}
TEST_F(HHWheelTimerHighResTest, Level1) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
TestTimeout tt;
// Schedule the timeout for the tick in a next epoch.
t.scheduleTimeout(&tt, std::chrono::microseconds(500));
tt.fn = [&] { evb.terminateLoopSoon(); };
TimePoint start;
evb.loop();
TimePoint end;
ASSERT_EQ(tt.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, end, microseconds(500));
}
// Test that we handle negative timeouts properly (i.e. treat them as 0)
TEST_F(HHWheelTimerHighResTest, NegativeTimeout) {
StackWheelTimer t(&timeoutMgr, microseconds(100));
std::this_thread::sleep_for(std::chrono::microseconds(10));
TestTimeout tt1;
TestTimeout tt2;
// Make sure we have event scheduled.
t.scheduleTimeout(&tt1, std::chrono::microseconds(100));
// Schedule another timeout that would appear to be earlier than
// the already scheduled one.
t.scheduleTimeout(&tt2, std::chrono::microseconds(-500000000));
evb.scheduleAt(
[&]() { evb.terminateLoopSoon(); },
std::chrono::steady_clock::now() + std::chrono::milliseconds(1));
TimePoint start;
evb.loop();
TimePoint end;
ASSERT_EQ(tt2.timestamps.size(), 1);
T_CHECK_TIMEOUT(start, end, std::chrono::milliseconds(10));
}