Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CI

on:
pull_request:
branches: [main,dev]
push:
branches: [main,dev]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-test:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4

- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DTEST_TIME_SLACK_MS=500

- name: Build
run: cmake --build build --config Release --parallel

- name: Test
run: ctest --test-dir build --build-config Release --output-on-failure
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ FetchContent_MakeAvailable(googletest)
enable_testing()
add_executable(unit_tests tests/ratelimiter_test.cpp)
target_link_libraries(unit_tests PRIVATE GTest::gtest_main)
set(TEST_TIME_SLACK_MS 0 CACHE STRING "Extra ms allowed on timing test upper bounds (e.g. for CI runners)")
target_compile_definitions(unit_tests PRIVATE TEST_TIME_SLACK_MS=${TEST_TIME_SLACK_MS})
# target_compile_definitions(unit_tests PRIVATE RATELIMITER_DEBUG)

include(GoogleTest)
Expand Down
59 changes: 33 additions & 26 deletions tests/ratelimiter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
#include <thread>
#include "ratelimiter.h"

// Extra milliseconds allowed on the upper bound of timing checks,
// for slow or loaded machines such as shared CI runners
#ifndef TEST_TIME_SLACK_MS
#define TEST_TIME_SLACK_MS 0
#endif
const long kTimeSlackMs = TEST_TIME_SLACK_MS;

long get_time_ms() {
auto duration = std::chrono::steady_clock::now().time_since_epoch();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
Expand All @@ -19,7 +26,7 @@ TEST(RateLimiterTest, AcquireWithinLimit) {
auto t0 = get_time_ms();
EXPECT_TRUE(rl.acquire(1));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 < 100);
EXPECT_LT(t1 - t0, 100 + kTimeSlackMs);
}

TEST(RateLimiterTest, RejectsNegativeRequests) {
Expand All @@ -38,8 +45,8 @@ TEST(RateLimiterTest, AcquireWait2x) {
EXPECT_TRUE(my_rl.acquire(8));
EXPECT_TRUE(my_rl.acquire(9));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 17 / 10.0 * 1000);
EXPECT_TRUE(t1 - t0 < 18 / 10.0 * 1000);
EXPECT_GE(t1 - t0, 17 / 10.0 * 1000);
EXPECT_LT(t1 - t0, 18 / 10.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireWithinLimit3x) {
Expand All @@ -49,7 +56,7 @@ TEST(RateLimiterTest, AcquireWithinLimit3x) {
EXPECT_TRUE(my_rl.acquire(2));
EXPECT_TRUE(my_rl.acquire(2));
auto t1 = get_time_ms();
assert(t1 - t0 < 100);
EXPECT_LT(t1 - t0, 100 + kTimeSlackMs);
}

/* This test is flaky:
Expand All @@ -67,8 +74,8 @@ TEST(RateLimiterTest, AcquireWithinLimit3xNoBurst) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
EXPECT_TRUE(my_rl.acquire(9));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 13 / 10.0 * 1000);
EXPECT_TRUE(t1 - t0 < 14 / 10.0 * 1000);
EXPECT_GE(t1 - t0, 13 / 10.0 * 1000);
EXPECT_LT(t1 - t0, 14 / 10.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireWithinLimit3xWithDelays) {
Expand All @@ -80,8 +87,8 @@ TEST(RateLimiterTest, AcquireWithinLimit3xWithDelays) {
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
EXPECT_TRUE(my_rl.acquire(9));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 4000); // no wait if sleep_for(2000) twice
EXPECT_TRUE(t1 - t0 < 4500);
EXPECT_GE(t1 - t0, 4000); // no wait if sleep_for(2000) twice
EXPECT_LT(t1 - t0, 4500 + kTimeSlackMs);
}

TEST(RateLimiterTest, RejectOverBurstInThread) {
Expand All @@ -101,8 +108,8 @@ TEST(RateLimiterTest, InfiniteLoopFixWithCeil) {
EXPECT_TRUE(my_rl.acquire(4));
EXPECT_TRUE(my_rl.acquire(3));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 2333); // total 7 reqs when rps=3
EXPECT_TRUE(t1 - t0 < 2500);
EXPECT_GE(t1 - t0, 2333); // total 7 reqs when rps=3
EXPECT_LT(t1 - t0, 2500 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireBigRequestOverLimit) {
Expand All @@ -112,8 +119,8 @@ TEST(RateLimiterTest, AcquireBigRequestOverLimit) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_TRUE(my_rl.acquire(1));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 100 / 3.0 * 1000); // total 100 reqs when rps=3
EXPECT_TRUE(t1 - t0 < 101 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 100 / 3.0 * 1000); // total 100 reqs when rps=3
EXPECT_LT(t1 - t0, 101 / 3.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireMultipleRequestsOverLimit) {
Expand Down Expand Up @@ -141,8 +148,8 @@ TEST(RateLimiterTest, AcquireMultipleRequestsOverLimit) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_TRUE(my_rl.acquire(1));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 11 / 3.0 * 1000);
EXPECT_TRUE(t1 - t0 < 12 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 11 / 3.0 * 1000);
EXPECT_LT(t1 - t0, 12 / 3.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireMultipleRequestsEachOverLimit) {
Expand All @@ -160,8 +167,8 @@ TEST(RateLimiterTest, AcquireMultipleRequestsEachOverLimit) {
std::this_thread::sleep_for(std::chrono::milliseconds(2));
EXPECT_TRUE(my_rl.acquire(19));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 114 / 3.0 * 1000); // total 100 reqs when rps=3
EXPECT_TRUE(t1 - t0 < 115 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 114 / 3.0 * 1000); // total 100 reqs when rps=3
EXPECT_LT(t1 - t0, 115 / 3.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireWithinLimitInThreads) {
Expand All @@ -177,8 +184,8 @@ TEST(RateLimiterTest, AcquireWithinLimitInThreads) {
th1.join();
th2.join();
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 12 / 3.0 * 1000);
EXPECT_TRUE(t1 - t0 < 13 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 12 / 3.0 * 1000);
EXPECT_LT(t1 - t0, 13 / 3.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireWithinLimitInThreadsWithDelay) {
Expand All @@ -195,8 +202,8 @@ TEST(RateLimiterTest, AcquireWithinLimitInThreadsWithDelay) {
th1.join();
th2.join();
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 12 / 3.0 * 1000);
EXPECT_TRUE(t1 - t0 < 13 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 12 / 3.0 * 1000);
EXPECT_LT(t1 - t0, 13 / 3.0 * 1000 + kTimeSlackMs);
}

TEST(RateLimiterTest, AcquireWaitForLimitInThreadsWithDelay) {
Expand All @@ -213,8 +220,8 @@ TEST(RateLimiterTest, AcquireWaitForLimitInThreadsWithDelay) {
th1.join();
th2.join();
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 30 / 10.0 * 1000);
EXPECT_TRUE(t1 - t0 < 32 / 10.0 * 1000); // '32' for extra time to cover sleep_for
EXPECT_GE(t1 - t0, 30 / 10.0 * 1000);
EXPECT_LT(t1 - t0, 32 / 10.0 * 1000 + kTimeSlackMs); // '32' for extra time to cover sleep_for
}

TEST(RateLimiterTest, AcquireInThreadsWithDelayedStart) {
Expand All @@ -240,8 +247,8 @@ TEST(RateLimiterTest, AcquireInThreadsWithDelayedStart) {
th1.join();
th2.join();
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 7400);
EXPECT_TRUE(t1 - t0 < 7900); // approx upper bound
EXPECT_GE(t1 - t0, 7400);
EXPECT_LT(t1 - t0, 7900 + kTimeSlackMs); // approx upper bound
}
}

Expand All @@ -254,6 +261,6 @@ TEST(RateLimiterTest, AcquireWithTotalReqsDecayTotalReqs) {
EXPECT_TRUE(my_rl.acquire(9));
EXPECT_TRUE(my_rl.acquire(9));
auto t1 = get_time_ms();
EXPECT_TRUE(t1 - t0 >= 45 / 3.0 * 1000); // sleep_for(2000) not included
EXPECT_TRUE(t1 - t0 < 46 / 3.0 * 1000);
EXPECT_GE(t1 - t0, 45 / 3.0 * 1000); // sleep_for(2000) not included
EXPECT_LT(t1 - t0, 46 / 3.0 * 1000 + kTimeSlackMs);
}
Loading