omwagh28/EshopBox-Assignment

★ 0Forks 0PythonGitHub ↗Compare

README

Eshopbox Thread-Safe Rate Limiter

A simple thread-safe rate limiter implemented in Python for the Eshopbox Backend Engineering Test.

The limiter ensures that no more than a configurable number of requests (N) are allowed during a one-second window, even when multiple threads call the limiter concurrently.

Problem

External sales-channel APIs impose limits on how many requests can be made per second.

The rate limiter exposes:

allow_request() -> bool

It returns:

  • True — the request is allowed.
  • False — the configured limit has already been reached for the current second.

For example, with a limit of 3 requests per second:

Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → False
Request 5 → False

When the next one-second window begins, the counter resets and requests can be allowed again.

Approach

The implementation uses a fixed-window counter.

The RateLimiter maintains:

  • limit — maximum requests allowed per second.
  • request_count — number of requests already allowed in the current window.
  • current_second — identifies the active one-second window.
  • lock — protects shared state when multiple threads access the limiter.

For each call to allow_request():

  1. Get the current second.
  2. Acquire the lock.
  3. If a new second has started, reset the request counter.
  4. If the counter has reached the configured limit, return False.
  5. Otherwise, increment the counter and return True.

Thread Safety

Multiple threads may call allow_request() at approximately the same time.

Without synchronization, multiple threads could read the same counter value before another thread updates it, causing more than N requests to be allowed.

The implementation uses Python's threading.Lock to protect the critical section containing the window check, counter reset, limit check, and counter increment.

This ensures that only one thread can modify the shared rate-limiter state at a time.

For example, the concurrency test creates 100 threads against a limiter configured for 10 requests per second.

Expected result:

100 concurrent attempts
10 allowed
90 rejected

Project Structure

eshopbox_rate_limiter/
├── rate_limiter.py       # Rate limiter implementation
├── test_rate_limiter.py  # Unit and concurrency tests
├── requirements.txt      # Python dependencies
├── README.md             # Project documentation
└── .gitignore

rate_limiter.py

Contains the RateLimiter class and the allow_request() method.

This is the actual implementation of the rate limiter.

test_rate_limiter.py

Contains four test functions that verify different behaviors of the same RateLimiter implementation.

Running:

pytest -v

should show:

test_basic_limit PASSED
test_configurable_limit PASSED
test_window_reset PASSED
test_concurrent_requests PASSED

4 passed

4 passed means that all four test functions passed. It does not mean that only four requests were made.

Test Cases

1. test_basic_limit()

Tests the basic rate-limiting behavior.

The limiter is configured as:

limiter = RateLimiter(3)

Four requests are attempted during the same window.

Expected behavior:

Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → False

The first three requests are allowed because the limit is 3. The fourth request is rejected because the limit has already been reached.

Run only this test using:

pytest test_rate_limiter.py::test_basic_limit -v

2. test_configurable_limit()

Tests that the limit N can be configured instead of being hardcoded.

The current test uses:

limit = 7
number_of_requests = 8

This means the limiter allows at most 7 requests while the test attempts 8 requests.

Expected result:

Request 1 → True
Request 2 → True
Request 3 → True
Request 4 → True
Request 5 → True
Request 6 → True
Request 7 → True
Request 8 → False

Allowed: 7
Rejected: 1

The values can be changed to test different limits and request counts.

For example:

limit = 5
number_of_requests = 10

would result in 5 allowed and 5 rejected requests during the same window.

To run this test and see the printed request results:

pytest test_rate_limiter.py::test_configurable_limit -s -v

The -s option allows the print() output from the test to be displayed.


3. test_window_reset()

Tests that the rate limit resets when a new one-second window begins.

The limiter is configured as:

limiter = RateLimiter(2)

During the first second:

Request 1 → True
Request 2 → True
Request 3 → False

The test then waits until the current Unix second changes.

During the next second, a fresh quota is available:

Request 4 → True
Request 5 → True
Request 6 → False

This verifies that the limit applies per second rather than permanently blocking requests after the limit is reached.

Run only this test using:

pytest test_rate_limiter.py::test_window_reset -v

4. test_concurrent_requests()

Tests the main concurrency requirement of the assignment.

The test uses:

limit = 10
number_of_threads = 100

This simulates 100 threads sharing the same rate limiter and attempting to call allow_request() at approximately the same time.

Each thread represents one concurrent caller attempting to get permission to make a request.

A threading.Barrier is used to make the worker threads reach allow_request() at approximately the same time.

Expected result:

Total attempts: 100
Allowed: 10
Rejected: 90

The test verifies:

assert len(results) == 100
assert results.count(True) == 10
assert results.count(False) == 90

This demonstrates that even when many threads access the limiter concurrently, no more than the configured limit can succeed during the same second.

Run only the concurrency test using:

pytest test_rate_limiter.py::test_concurrent_requests -v

Running All Tests

Create a virtual environment:

python -m venv venv

Activate it on Windows:

venv\Scripts\activate

Install dependencies:

pip install -r requirements.txt

Run all tests:

pytest -v

To also display print() statements:

pytest -s -v

Expected result:

test_rate_limiter.py::test_basic_limit PASSED
test_rate_limiter.py::test_configurable_limit PASSED
test_rate_limiter.py::test_window_reset PASSED
test_rate_limiter.py::test_concurrent_requests PASSED

4 passed

Assumptions and Limitations

The phrase "current second" in the problem statement is interpreted as a fixed one-second window, so this implementation uses a fixed-window counter rather than a sliding-window or token-bucket algorithm.

The limiter is thread-safe for multiple threads sharing the same RateLimiter instance within a single Python process.

For a distributed system with multiple processes or application instances, the rate-limit state would need to be stored in shared infrastructure such as Redis and updated atomically.

Complexity

Each allow_request() call performs constant-time operations.

  • Time: O(1)
  • Space: O(1)

Contributors

omwagh28

Issues