Skip to content

Add a first-party rate limiter package - #480

Merged
binaryfire merged 46 commits into
0.4from
feature/rate-limiter
Aug 7, 2026
Merged

Add a first-party rate limiter package#480
binaryfire merged 46 commits into
0.4from
feature/rate-limiter

Conversation

@binaryfire

@binaryfire binaryfire commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR replaces Hypervel's cache-backed rate limiter with a new first-party hypervel/rate-limiter package.

The package provides:

  • fixed-window rate limits;
  • weighted sliding-window rate limits;
  • continuously replenishing leaky buckets backed by GCRA;
  • weighted operations;
  • capped exponential failure backoff;
  • complete result objects with the decision, remaining capacity, and retry timing;
  • dedicated Redis, Swoole, database, and worker-array stores; and
  • one rate limiting path for routing, queues, Fortify, exception reporting, and Reverb.

The public Hypervel\Support\Facades\RateLimiter facade remains in the same location. Rate limit definitions and the concrete manager now live under Hypervel\RateLimiter.

In local end-to-end fixed-window benchmarks, the dedicated Redis store delivered 2.7–5.5 times the throughput of the cache-backed implementation.

Why this is a package

Hypervel inherited Laravel's rate limiter design. Laravel places its limiter in the Cache component and injects a cache repository into it. Operations such as checking a limit, recording a hit, reading the remaining capacity, and reading the retry delay are separate cache calls.

That design has a few costs:

  1. The cache contract becomes the limit of what a rate limiter store can do. A backend cannot expose one purpose-built atomic rate limit decision when the limiter has to express its work through general cache methods.
  2. A complete decision takes several operations. Callers coordinate methods such as tooManyAttempts, hit, remaining, and availableIn instead of receiving one result from one state change.
  3. Backend details leak into the public API. Routing and queueing each have separate Redis middleware classes in addition to their normal middleware.
  4. The public model is centered on fixed windows. There is no clean type or store contract for adding algorithms with different state and timing rules.
  5. Generic cache stores carry behavior that is not useful to a rate limiter, including general value serialization and cache payload formats.

The Redis path partly works around these limits with Redis-specific classes. This improves that one backend, but it also creates two implementations of the same middleware behavior and two ways to select rate limiting.

Rate limiting has its own state transitions, algorithms, timing, and result data. Other ecosystems treat it as its own component: Symfony has a RateLimiter component, Go provides golang.org/x/time/rate, and .NET provides System.Threading.RateLimiting. Hypervel now takes the same general approach while using stores designed for its Swoole runtime and distributed backends.

Performance

The dedicated store contract lets each backend perform one native rate limit decision instead of coordinating several general cache operations.

The old and new fixed-window paths were compared on the same machine and backend connections. Each repository booted through its own Testbench application and vendor directory. The environment used PHP 8.4.23, Swoole 6.2.2, phpredis 6.3.0, a local Redis server, and SQLite with identical durability settings.

The old accepted path called tooManyAttempts, hit, and remaining; its denied path called tooManyAttempts and availableIn. The new path called consume once and read the decision, remaining capacity, and retry delay from its result.

Each value below is the median of five independent runs:

Backend Path Clients Old operations/s New operations/s Throughput p50 reduction
Redis Allowed 1 1,316 6,108 4.64x 79.0%
Redis Allowed 16 2,615 14,262 5.45x 84.4%
Redis Denied 1 2,272 6,145 2.70x 64.3%
Redis Denied 16 5,100 13,592 2.66x 57.6%
SQLite Allowed 1 67.9 74.2 1.09x 6.9%
SQLite Denied 1 1,350.7 1,958.0 1.45x 32.0%

Redis runs used 5,000 measured operations, 100 warmup operations, and one or 16 clients contending for the same key. SQLite runs used 300 measured operations, 20 warmup operations, and one client. Concurrent SQLite measurements were omitted because tagged Swoole releases still contain the AIO scheduler stall fixed by Swoole PR #6140.

Redis benefits directly from replacing several cache calls with one cached Lua execution. SQLite's accepted path is largely dominated by the durable transaction commit that both implementations require, while its denied path more clearly shows the benefit of returning one native decision.

These are local comparison results, not production capacity figures.

API

Rate limits are immutable typed values. The concrete type selects the algorithm, so there is no strategy string followed by an array of unrelated options.

  • Limit defines a fixed window.
  • SlidingWindow defines a weighted sliding window.
  • LeakyBucket defines a continuously replenishing GCRA limit.
  • Unlimited bypasses storage.
  • Backoff defines failure-driven exponential delays.

Admission rate limits share by, cost, globally, after, and response modifiers. Fixed windows, sliding windows, and leaky buckets provide the familiar perSecond, perMinute, perMinutes, perHour, and perDay factories.

The main operations are:

  • consume to atomically check and consume capacity;
  • inspect to read a decision without changing state;
  • attempt to consume capacity before running a callback;
  • recordFailure to update exponential backoff state; and
  • clear to remove state.

Consuming capacity

use Hypervel\RateLimiter\Limit;
use Hypervel\Support\Facades\RateLimiter;

$result = RateLimiter::consume(
    Limit::perMinute(100)
        ->cost(5)
        ->by('uploads:'.$user->id),
);

if ($result->denied()) {
    return 'Try again in '.$result->retryAfter().' seconds.';
}

$remaining = $result->remaining();

LimitResult contains allowed, denied, limit, remaining, retryAfter, and resetAfter. The store returns all of this data from the same decision. Callers do not issue another read after consuming capacity.

Sliding windows

use Hypervel\RateLimiter\SlidingWindow;
use Hypervel\Support\Facades\RateLimiter;

$result = RateLimiter::store('redis')->consume(
    SlidingWindow::perMinute(100)
        ->cost(5)
        ->by('uploads:'.$user->id),
);

The sliding window is anchored by its first accepted operation. It keeps the current and previous counts, then gradually reduces how much the previous count contributes as the current window passes. This smooths the sharp reset at a fixed-window boundary while using constant state and bounded work on every store.

Leaky buckets and named stores

use Hypervel\Http\Request;
use Hypervel\RateLimiter\LeakyBucket;
use Hypervel\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    return LeakyBucket::perSecond(100)
        ->burst(200)
        ->by($request->user()?->getAuthIdentifier() ?? $request->ip());
}, store: 'redis');

This sustains 100 operations per second and allows an initial burst of 200. The named limiter may be used by both route and queue middleware.

Applications may also select a store for a direct operation:

$result = RateLimiter::store('redis')->consume($limit);

Store names accept strings or enums. Custom stores may be registered through the manager's existing extend pattern.

Response-based limits

Named route limits may decide whether to consume capacity after the response is known:

use Hypervel\Http\Request;
use Hypervel\RateLimiter\Limit;
use Hypervel\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;

RateLimiter::for('resource-not-found', function (Request $request) {
    return Limit::perMinute(10)
        ->by($request->user()?->id ?: $request->ip())
        ->after(fn (Response $response) => $response->getStatusCode() === 404);
});

Hypervel inspects the limit before running the route and consumes capacity only when the callback returns true.

Exponential backoff

Backoff tracks failures rather than admitted requests. It is separate from the admission rate limit hierarchy because recording a failure is a different operation from consuming request capacity.

use Hypervel\Auth\AuthenticationException;
use Hypervel\RateLimiter\Backoff;
use Hypervel\Support\Facades\RateLimiter;

$backoff = Backoff::exponential(
    after: 5,
    initialDelay: 1,
    maxDelay: 300,
    resetAfter: 3600,
)->by('login:'.$username.':'.$request->ip());

if (RateLimiter::inspect($backoff)->denied()) {
    return 'Try again later.';
}

try {
    $this->authenticate($request);
    RateLimiter::clear($backoff);
} catch (AuthenticationException $exception) {
    RateLimiter::recordFailure($backoff);

    throw $exception;
}

BackoffResult contains the decision, failure count, and retry delay.

Stores

Each store implements the rate limiter's small native-operation contract. Stores receive a validated policy and an already resolved physical key, then perform the complete state transition themselves.

Store Implementation Scope
Redis One cached Lua execution and one pooled connection checkout for each decision Shared across application servers
Database A dedicated numeric table with transactional locking Shared across application servers
Swoole Native integer columns in a shared Swoole\Table protected by striped locks Workers belonging to one Swoole server instance
Worker array Numeric in-process state with no suspension points One worker process; automated tests only

All first-party stores implement the same fixed-window, sliding-window, leaky-bucket, and backoff semantics. A denied consume does not change state or extend its expiry. Inspection does not create state. Invalid or corrupt state throws instead of silently allowing work.

Redis

Admission decisions, inspections, and failure records each use one Lua script. The normal path uses EVALSHA, with the existing NOSCRIPT fallback on first use. Clearing state uses one DEL. Fixed and sliding windows derive their position from key expiry, while leaky buckets and backoff use Redis server time. Each script sets expiry in the same atomic operation as the state change.

Rate limiter state does not pass through cache serialization or compression. Each operation uses one key, so the scripts can be routed by Redis Cluster without a cross-slot operation. The same portable scripts run against Redis 8 and Valkey 9.

Database

The database store uses a dedicated rate_limits table with compact numeric state:

Schema::create('rate_limits', function (Blueprint $table) {
    $table->char('key', 32)->primary();
    $table->unsignedBigInteger('value')->default(0);
    $table->unsignedBigInteger('secondary_value')->default(0);
    $table->unsignedBigInteger('expires_at')->index();
});

MySQL, MariaDB, and PostgreSQL use row locks. SQLite acquires its writer lock before reading state because it does not implement FOR UPDATE. Redis remains the recommended store for high-throughput distributed limiting; the database store is the portable shared fallback.

The matching Hypervel application skeleton update includes this migration for fresh applications. Existing applications may generate it with:

php artisan make:rate-limiter-table
php artisan migrate

Expired database rows are removed by rate-limiter:prune, which should be scheduled regularly:

use Hypervel\Support\Facades\Schedule;

Schedule::command('rate-limiter:prune')->hourly();

The database store deliberately does not delete expired rows during a rate limit check. Without the prune command, the rate_limits table will continue growing as new limiter keys are encountered.

Database limiter mutations cannot run inside an existing transaction on the selected connection. Doing so could allow an outer rollback to undo an accepted charge and would disable the limiter's own transaction retry behavior. Applications that need this may configure a separate rate limiter connection.

Swoole

The Swoole store allocates its table and locks before workers fork. It keeps three native integer columns and performs each operation in one short key-scoped critical section. It does not serialize PHP values.

Worker zero prunes expired entries and reports table pressure. If the table cannot allocate a new row after pruning expired state, the store throws instead of evicting a live rate limit and allowing excess traffic.

The Swoole store is shared by workers belonging to one server master. It is not a distributed store across independent server instances or hosts.

Worker array

The configured worker-array store is for automated tests only. Its state is not shared across workers or servers, and expired untouched keys remain in memory until the worker exits.

The underlying store remains available as a low-level package primitive for framework code with a proven worker-local ownership model. Reverb uses it directly for per-connection message limits, where one worker owns the connection for its full lifetime and clears its key when the connection closes. This internal use does not depend on application rate limiter configuration.

Key identity

Every physical key is a fixed 32-character hash. The identity includes the application prefix, named limiter and optional scope, caller key, policy type, stable algorithm settings, and global scope.

Operation cost and callbacks do not change identity. This allows the same rate limit to charge different costs. Changing the algorithm settings starts fresh state while the old entry expires naturally, and clear must receive the same settings that created the state.

Keys are always hashed. There is no runtime switch that can cause one worker to address the same rate limit differently from another.

Framework integration

The package is installed and registered by default. Foundation owns config/rate-limiter.php, and named stores merge using the same configuration rules as other manager-backed components. Fresh applications use the database store by default. Testbench uses the worker-array store while still providing the standard database migration for tests that select the database store.

This PR moves all framework rate limiting onto the new API:

  • routing uses one ThrottleRequests implementation for every store;
  • queue RateLimited and ThrottlesExceptions middleware use the same store selection and result types;
  • Fortify login limiting uses a typed fixed-window policy;
  • exception report throttling uses atomic attempt;
  • Reverb composes a direct worker-local limiter for per-connection message limits; and
  • the facade resolves the new manager while keeping its existing public location.

Routing still supports named limiters, throttle:60,1, ThrottleRequests::using, ThrottleRequests::with, custom 429 responses, response-based limits, and stacked limits. Queue middleware can use the store registered on a named limiter or override it with store('redis').

Stacked rate limits are consumed in order. If a later limit denies an operation, earlier accepted charges remain. An all-or-nothing multi-key operation cannot be implemented consistently across every store or Redis Cluster without adding races or a new distributed transaction system.

Laravel compatibility

This deliberately differs from Laravel in one area.

The facade remains Hypervel\Support\Facades\RateLimiter, and familiar definition APIs such as RateLimiter::for, Limit::perMinute, by, after, and response remain. Route middleware syntax also remains familiar.

The implementation is no longer exposed as Hypervel\Cache\RateLimiter, and there is no compatibility shim under the Cache namespace. The old primitive counter methods and Redis-specific middleware classes are removed.

A shim would create two plausible entry points for the same feature:

  • a Laravel-shaped Cache class with the old expectations; and
  • the new RateLimiter package with a different operation and policy model.

That is harder for developers and coding models to reason about than one documented difference. Code using Hypervel\RateLimiter is an explicit signal that Hypervel's rate limiter has a different API from Laravel's cache-backed implementation.

The removed Redis-specific surfaces include:

  • ThrottleRequestsWithRedis;
  • RateLimitedWithRedis;
  • ThrottlesExceptionsWithRedis;
  • the throttleWithRedis middleware switch; and
  • Redis connection selection methods tied to those middleware classes.

Redis is now selected through rate-limiter.default, the optional store on RateLimiter::for, or the middleware store modifier.

Supporting changes

The new Swoole store needed the same short, key-scoped locking behavior already present in Cache and Reverb. This PR extracts that behavior into Hypervel\Core\Swoole\StripedLock and uses it in all three packages.

Cache's Swoole maintenance now uses the existing Coordinator timer instead of maintaining a second timer wrapper and worker-exit registry. Existing Redis duration and concurrency limiters now use the shared SHA-cached Lua path rather than sending their full scripts on every operation.

The implementation also exposed two small shared lifecycle issues:

  • Parallel::wait now fails before running callbacks when called outside a coroutine; and
  • MultipleInstanceManager::setApplication and MailManager::setApplication now refresh their cached configuration reference when tests replace the application.

Failure behavior

Rate limiter store failures propagate. Hypervel never silently allows an operation and never switches to a weaker or worker-local store after a backend failure.

This is important for both correctness and operations: a configured limit either uses the selected shared state or fails visibly.

Testing

The package has a shared behavioral contract that runs against worker-array, Swoole, Redis, SQLite, MySQL, MariaDB, and PostgreSQL stores. It covers fixed windows, weighted sliding windows, weighted consumption, leaky-bucket recovery, backoff thresholds and expiry, inspection, clearing, denied-operation immutability, and corrupt state.

Additional tests cover:

  • concurrent Redis clients and MySQL, MariaDB, and PostgreSQL transactions never admitting beyond capacity;
  • Redis and Valkey using the same scripts;
  • Redis prefixes, serializer and compression settings, TTL retention, NOSCRIPT fallback, malformed state, and exact integer boundaries;
  • Swoole forked-worker contention, striped lock ordering, table allocation failure, pruning, and pressure reporting;
  • database connection selection, table prefixes, primary reads, server clocks, transaction guards, pruning, and generated migrations;
  • routing headers, 429 responses, response-based limits, named stores, weighted limits, and concurrent requests;
  • queue release timing, serialization, store overrides, and exception throttling;
  • Fortify, exception reporting, Reverb, default provider registration, application configuration, and Testbench migrations; and
  • the removed Redis-specific paths having one replacement implementation.

SQLite multi-connection contention coverage remains in the shared database contract but is temporarily skipped on tagged Swoole releases. It links to Swoole PR #6140, which fixes the AIO scheduler stall. All non-concurrency SQLite store coverage remains active.

The complete rate limiting documentation is in src/boost/docs/rate-limiting.md, with routing, queue, Fortify, Reverb, error handling, Redis, middleware, and facade documentation updated alongside it.

Extract the 64-stripe lock coordinator into Core so Cache, Reverb, and the rate limiter share one pre-fork-safe primitive.

Cover deterministic key striping, ascending multi-lock acquisition, shared-stripe deduplication, bounded contention, rollback, and reverse-order release.
Replace Cache’s package-local Atomic lock implementation with the shared Core primitive while preserving row and all-lock behavior.

Move generic contention, timeout, and rollback coverage to Core and retain Cache’s real cross-process store concurrency coverage.
Refresh each manager’s cached configuration repository when tests swap the application container.

Cover both MultipleInstanceManager and MailManager so subsequent resolutions read the replacement application without rebuilding existing instances.
Fail before executing callbacks when Parallel::wait() is called without an active coroutine.

Prevent Coroutine::join() from terminating callers without a useful diagnostic and prove misuse produces no callback side effects.
Introduce the dedicated hypervel/rate-limiter component with immutable fixed-window, GCRA leaky-bucket, unlimited, and exponential-backoff policies.

Add atomic Redis Lua, Swoole numeric-table, database, and worker-array stores; typed decisions; named store management; pruning and table commands; default config and provider wiring; integration workflows; corruption checks; and concurrency coverage.

Use native store operations instead of generic cache serialization and return complete one-call admission decisions.
Add Testbench’s worker-array rate-limiter default and standard rate_limits migration.

Update migration inventory, refresh and rollback coverage, and default-config assertions so tests can opt into database limiting without forcing ordinary framework tests through SQLite.
Rewrite route throttling around atomic typed decisions and one backend-neutral middleware path.

Preserve Laravel-facing helpers, headers, named limiters, stacked policies, response predicates, and custom responses while removing Redis-specific middleware state and switches.

Keep decision data request-local and select Redis as an ordinary named store.
Move RateLimited and ThrottlesExceptions onto named rate-limiter stores with serializable store selectors.

Preserve release timing, dontRelease, exception predicates, reporting callbacks, and Laravel interoperability while deleting Redis-specific middleware subclasses.

Use the post-failure decision when concurrent failures open the circuit.
Replace Fortify’s cache counter calls with one canonical fixed-window policy shared by inspect, consume, and clear operations.

Preserve attempts, lockout, retry, username, guard, and IP scoping, plus successful-login clearing, while using typed rate-limiter results.
Use AdmissionPolicy and atomic limiter attempts for exception-report throttling.

Remove redundant key hashing and the hash opt-out while preserving Lottery, Unlimited, custom keys, and application overrides through the broader typed policy return.
Use the worker-array rate-limiter store for per-connection Reverb message limits and clear matching policy state on close.

Replace Reverb’s duplicate Atomic stripe implementation with the shared pre-fork Core lock while retaining call-site ordering, deduplication, and post-release reporting coverage.
Remove Cache’s legacy limiter implementation, binding, config, tests, and timer wrapper now that every consumer uses the dedicated component.

Move Swoole cache maintenance onto Coordinator Timer with validate-all, resolve-all, register ordering, captured stores, registration rollback, and worker-exit cancellation.

Add the missing direct Swoole and coordinator package requirements and retain real worker-recycle coverage.
Route the existing blocking concurrency and duration limiters through evalWithShaCache instead of resending Lua bodies on every operation.

Preserve connection selection, prefixes, cluster routing, builder APIs, and results while covering NOSCRIPT fallback and native-versus-script error classification.
Run no-merge config publishing through Testbench’s framework-configuration bootstrap and a disposable destination.

Assert every target is absent before publication so tests cannot pass by comparing Foundation config sources to themselves, while retaining the intentional empty-stub branch.
Add a developer-only end-to-end benchmark for fixed-window and leaky-bucket decisions through the real manager, pools, drivers, and result decoding.

Report throughput and latency percentiles for allowed-heavy, denied-heavy, single-client, and contended workloads without adding a production command.
Document typed policies, decisions, direct APIs, stores, configuration, database migration and pruning, distribution guarantees, failure behavior, and custom drivers.

Point facade users at the canonical Hypervel RateLimiter manager and keep rate-limiting.md as the single user guide.
Update routing and middleware guidance for named stores, weighted costs, leaky buckets, response-based charging, headers, and the single throttle middleware.

Remove the old Redis middleware switch and explain backend selection through rate-limiter stores.
Update queue, authentication, exception-reporting, and starter-kit examples to use the dedicated rate-limiter policies and store selectors.

Remove Redis-specific queue middleware and old Cache namespace examples while preserving Laravel-style task-focused prose.
Record the dedicated Hypervel RateLimiter namespace and typed atomic API as an intentional Laravel difference for agents and porting work.

Direct Cache users to the canonical rate-limiting guide and correct the Redis integration workflow inventory.
Remove completed legacy limiter and middleware defects from the framework TODO list.

Retain the focused future INCREX revisit and connection-owned backend capability detection work with their real ecosystem constraints.
Bring the committed implementation plan into exact agreement with the reviewed final code.

Record the final database locking, timer startup, shared clock, Redis portability, Testbench, Swoole release, consumer, testing, and cleanup decisions used during implementation.
Add a public working rule that prevents agents from spawning or delegating work to subagents unless the user explicitly requests or approves their use. This keeps delegation under the user's control across all work in the components repository.
Rewrite the canonical rate limiting guide around typed rate limits, atomic stores, fixed windows, leaky buckets, weighted operations, backoff, store selection, pruning, custom drivers, identity rules, and failure behavior.

Align routing and queue guidance with the unified limiter API, registered store selection, ordered consumption, response-based charging, result headers, and queue backoff semantics while preserving the wording and examples inherited from Laravel wherever no Hypervel adaptation is required.

Record public Laravel differences in the package and Foundation READMEs, fix the Redis throttle description and facade table of contents, document the retained native Redis optimization work, and keep benchmark terminology consistent with the public API.

Add the matching real-time facade removal marker to the Foundation bootstrap test so the intentional omission is recorded alongside its source and README documentation.
Show the exact pruneExpired method required by custom durable stores so driver authors can integrate with the rate-limiter:prune command without leaving the canonical documentation.
Rename the LeakyBucket factory duration parameters and validation labels from decay to period so named arguments match the continuously replenished GCRA model.

Convert each public duration unit directly to microseconds. This removes a redundant checked conversion and factory call while preserving every accepted range and reporting overflow errors in the unit supplied by the caller.

Document the period multiplier with its sustained-rate meaning, update the implementation plan, and cover every named factory argument and unit-specific overflow boundary.
Bring the rate limiter branch up to date with the latest database transaction ownership, pooled SQLite cleanup, queue backoff typing, notification parity, gRPC documentation, package metadata, and agent guidance from 0.4.

Resolve the cache package metadata conflict by retaining the rate limiter branch requirements for Swoole and Coordinator while adding the upstream Symfony Console dependency. The reviewed upstream behavior requires no rate limiter implementation changes.
PhpRedis only defines COMPRESSION_LZF when the extension is compiled with LZF support. The Redis and Valkey CI jobs use a phpredis build without optional compression codecs, so resolving the constant failed before the limiter behavior could be exercised.

Guard the integration test with the same capability check used by the existing Redis and cache test suites. Builds with LZF continue to run the serializer and compression assertions, while builds without it report a clear skip instead of an error.
Warn that database limiter rows require scheduled pruning to prevent the rate_limits table from continuing to grow.

Restrict the worker-array store guidance to automated tests, explaining its per-worker isolation and retained expired-key memory in both the public documentation and framework/Testbench configuration.
Inject a directly composed worker-local limiter into the Pusher server so internal per-connection admission cannot be changed by application rate-limiter store configuration. Allow key resolvers to omit named scope callbacks for direct package composition.

Deliver the 4301 response before terminating rate-limited WebSocket connections, preserve termination when error reporting fails, require complete enabled settings, and normalize numeric config values from environment-backed or custom application providers.

Expand Reverb and key resolver coverage for configuration independence, limiter cleanup, delivery ordering, failure cleanup, numeric strings, and deterministic transport behavior. Document the public Reverb settings and record the final design in the implementation plan.
Bring the completed rate limiter work onto the current 0.4 routing, validation, facade-documenter, testing, and CI baseline.

Resolve the overlapping routing and Redis changes by preserving the dedicated Hypervel rate limiter APIs and unified throttle middleware while adopting the incoming routing correctness fixes, DurationLimiter result correction, generated facade metadata, and package dependency updates.

Keep Redis integration suites grouped by service and run them through bounded ParaTest workers for Redis and Valkey. Record the shared Redis and Queue test-harness requirements in the implementation plan and clarify the reserved Reverb integration database guidance.

Verified with all affected routing and Redis groups, the full 27,644-test framework suite, Testbench, package-mode dogfood, PHPStan, PHP CS Fixer, and the complete live-server Reverb integration suite.
Normalize every configured Redis connection to the current test database in both sequential and ParaTest runs. Named cache, session, queue, and Reverb connections now share the worker-isolated database instead of leaking through their configured database numbers.

Reject non-empty connection URLs because a URL path is merged after raw connection options and would override the worker database. Keep empty URLs equivalent to unset values, skip reserved Redis configuration groups, and retain one FLUSHDB because first-party integration workflows expose one service and all named connections now select the same database.

Remove the dead token-to-database trait helper and extend the focused harness coverage for sequential and parallel normalization, reserved entries, empty URLs, URL rejection before flushing, and lightweight opt-in setup.
Adapt QueueTestCase to Hypervel's automatically invoked InteractsWithRedis hooks instead of Laravel-only manual setup calls. Gate Redis setup on the live queue.default value and remember successful setup so teardown remains correct even when a test changes the configured driver.

Remove the stale driver snapshot and its subclass synchronization assignments, restoring 121 sync and database tests that were silently skipped without REDIS_HOST. Normalize WorkCommandTest's environment setup order now that the driver is read from live configuration.

Add an end-to-end Redis queue worker fixture under the service-specific integration directory so CI exercises the Redis lifecycle branch and proves a queued job is dispatched and processed through the real driver.
Bring in the latest View lifecycle audit, Pagination correctness work, request-owned timing support, portable migration APIs, and console and Testbench fixes from 0.4.

Resolve the Testbench application conflict by keeping 0.4's removal of the duplicate static-state cleanup method. The authoritative AfterEachTestSubscriber retains this branch's removal of the obsolete ThrottleRequests reset while adopting the new View cleanup methods.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 205 files, which is 105 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 832498aa-111a-47e2-b02d-a05393e302b7

📥 Commits

Reviewing files that changed from the base of the PR and between 2b614ae and 295114e.

📒 Files selected for processing (205)
  • .env.example
  • .github/workflows/redis.yml
  • AGENTS.md
  • composer.json
  • docs/plans/2026-08-04-1543-rate-limiter-package.md
  • docs/plans/2026-08-07-1439-sliding-window-rate-limiter.md
  • docs/todo.md
  • src/boost/docs/errors.md
  • src/boost/docs/facades.md
  • src/boost/docs/fortify.md
  • src/boost/docs/middleware.md
  • src/boost/docs/queues.md
  • src/boost/docs/rate-limiting.md
  • src/boost/docs/redis.md
  • src/boost/docs/reverb.md
  • src/boost/docs/routing.md
  • src/boost/docs/starter-kits.md
  • src/cache/README.md
  • src/cache/composer.json
  • src/cache/src/CacheServiceProvider.php
  • src/cache/src/Listeners/BaseListener.php
  • src/cache/src/Listeners/CreateSwooleTimers.php
  • src/cache/src/Listeners/RegisterSwooleMaintenanceTimers.php
  • src/cache/src/RateLimiter.php
  • src/cache/src/RateLimiting/GlobalLimit.php
  • src/cache/src/RateLimiting/Limit.php
  • src/cache/src/RateLimiting/Unlimited.php
  • src/cache/src/SwooleTableState.php
  • src/cache/src/SwooleTimer.php
  • src/core/src/Swoole/StripedLock.php
  • src/coroutine/src/Parallel.php
  • src/fortify/composer.json
  • src/fortify/src/LoginRateLimiter.php
  • src/fortify/stubs/FortifyServiceProvider.stub
  • src/foundation/README.md
  • src/foundation/composer.json
  • src/foundation/config/cache.php
  • src/foundation/config/rate-limiter.php
  • src/foundation/src/Bootstrap/LoadConfiguration.php
  • src/foundation/src/Configuration/Middleware.php
  • src/foundation/src/Exceptions/Handler.php
  • src/foundation/src/Http/Kernel.php
  • src/foundation/src/Testing/Concerns/InteractsWithRedis.php
  • src/mail/src/MailManager.php
  • src/queue/composer.json
  • src/queue/src/Middleware/RateLimited.php
  • src/queue/src/Middleware/RateLimitedWithRedis.php
  • src/queue/src/Middleware/ThrottlesExceptions.php
  • src/queue/src/Middleware/ThrottlesExceptionsWithRedis.php
  • src/rate-limiter/LICENSE.md
  • src/rate-limiter/README.md
  • src/rate-limiter/composer.json
  • src/rate-limiter/src/AdmissionPolicy.php
  • src/rate-limiter/src/Backoff.php
  • src/rate-limiter/src/BackoffResult.php
  • src/rate-limiter/src/Concerns/CalculatesRateLimits.php
  • src/rate-limiter/src/Console/PruneCommand.php
  • src/rate-limiter/src/Console/RateLimiterTableCommand.php
  • src/rate-limiter/src/Console/stubs/rate-limits.stub
  • src/rate-limiter/src/Contracts/Decision.php
  • src/rate-limiter/src/Contracts/PrunableStore.php
  • src/rate-limiter/src/Contracts/Store.php
  • src/rate-limiter/src/DatabaseStore.php
  • src/rate-limiter/src/Exceptions/InvalidRateLimitException.php
  • src/rate-limiter/src/Exceptions/SwooleTableFullException.php
  • src/rate-limiter/src/KeyResolver.php
  • src/rate-limiter/src/LeakyBucket.php
  • src/rate-limiter/src/Limit.php
  • src/rate-limiter/src/LimitResult.php
  • src/rate-limiter/src/Limiter.php
  • src/rate-limiter/src/Listeners/InitializeSwooleTables.php
  • src/rate-limiter/src/Listeners/RegisterPruneTimer.php
  • src/rate-limiter/src/RateLimiter.php
  • src/rate-limiter/src/RateLimiterServiceProvider.php
  • src/rate-limiter/src/RedisStore.php
  • src/rate-limiter/src/SlidingWindow.php
  • src/rate-limiter/src/Swoole/TableManager.php
  • src/rate-limiter/src/Swoole/TableState.php
  • src/rate-limiter/src/SwooleStore.php
  • src/rate-limiter/src/Unlimited.php
  • src/rate-limiter/src/WorkerArrayStore.php
  • src/redis/src/Limiters/ConcurrencyLimiter.php
  • src/redis/src/Limiters/DurationLimiter.php
  • src/redis/src/RedisConnection.php
  • src/reverb/composer.json
  • src/reverb/src/Protocols/Pusher/Server.php
  • src/reverb/src/ReverbServiceProvider.php
  • src/reverb/src/Servers/Hypervel/HypervelServerProvider.php
  • src/reverb/src/Servers/Hypervel/Scaling/SwooleTableSharedState.php
  • src/routing/composer.json
  • src/routing/src/Middleware/ThrottleRequests.php
  • src/routing/src/Middleware/ThrottleRequestsWithRedis.php
  • src/routing/src/RoutingServiceProvider.php
  • src/support/src/DefaultProviders.php
  • src/support/src/Facades/RateLimiter.php
  • src/support/src/MultipleInstanceManager.php
  • src/testbench/hypervel/config/rate-limiter.php
  • src/testbench/hypervel/migrations/0001_01_01_000008_testbench_create_rate_limits_table.php
  • src/testing/src/PHPUnit/AfterEachTestSubscriber.php
  • tests/Benchmarks/RateLimiter/README.md
  • tests/Benchmarks/RateLimiter/benchmark.php
  • tests/Cache/CacheRateLimiterTest.php
  • tests/Cache/CacheServiceProviderTest.php
  • tests/Cache/CacheSwooleStoreConcurrencyTest.php
  • tests/Cache/CreateSwooleTimersTest.php
  • tests/Cache/Fixtures/SwooleMaintenanceTimerRecycleServer.php
  • tests/Cache/Fixtures/SwooleTimerRecycleServer.php
  • tests/Cache/LimitTest.php
  • tests/Cache/RateLimiterTest.php
  • tests/Cache/RegisterSwooleMaintenanceTimersTest.php
  • tests/Cache/SwooleMaintenanceTimerWorkerRecycleTest.php
  • tests/Core/Swoole/StripedLockTest.php
  • tests/Coroutine/ParallelNonCoroutineTest.php
  • tests/Fortify/AuthenticatedSessionControllerTest.php
  • tests/Fortify/LoginRateLimiterTest.php
  • tests/Foundation/Bootstrap/RegisterFacadesTest.php
  • tests/Foundation/Configuration/MiddlewareTest.php
  • tests/Foundation/Fixtures/config/rate-limiter.php
  • tests/Foundation/FoundationApplicationTest.php
  • tests/Foundation/FoundationExceptionsHandlerTest.php
  • tests/Foundation/Testing/Concerns/ExternalServiceOptInTest.php
  • tests/Foundation/Testing/Concerns/InteractsWithRedisParallelTest.php
  • tests/Inertia/InertiaServiceProviderTest.php
  • tests/Integration/Auth/Redis/EloquentUserProviderCacheTagsTest.php
  • tests/Integration/Auth/Redis/EloquentUserProviderRedisCacheTest.php
  • tests/Integration/Cache/Redis/MemoizedStoreTest.php
  • tests/Integration/Cache/Redis/PhpRedisCacheFunnelTest.php
  • tests/Integration/Cache/Redis/PhpRedisCacheLockTest.php
  • tests/Integration/Cache/Redis/RedisCacheFunnelTest.php
  • tests/Integration/Cache/Redis/RedisCacheIntegrationTest.php
  • tests/Integration/Cache/Redis/RedisCacheLockTest.php
  • tests/Integration/Cache/Redis/RedisStoreTest.php
  • tests/Integration/Foundation/Console/ConfigPublishCommandTest.php
  • tests/Integration/Foundation/Console/ConfigPublishCommandWithoutMergedConfigurationTest.php
  • tests/Integration/Generators/RateLimiterTableCommandTest.php
  • tests/Integration/Http/Redis/ThrottleRequestsRedisStoreTest.php
  • tests/Integration/Http/ThrottleRequestsTest.php
  • tests/Integration/Http/ThrottleRequestsWithRedisTest.php
  • tests/Integration/Queue/DebouncedJobTest.php
  • tests/Integration/Queue/DeleteModelWhenMissingTest.php
  • tests/Integration/Queue/DeleteNotificationWhenMissingModelTest.php
  • tests/Integration/Queue/JobEncryptionTest.php
  • tests/Integration/Queue/QueueTestCase.php
  • tests/Integration/Queue/RateLimitedTest.php
  • tests/Integration/Queue/Redis/RateLimitedRedisStoreTest.php
  • tests/Integration/Queue/Redis/RedisQueueDriverTest.php
  • tests/Integration/Queue/Redis/RedisQueueTest.php
  • tests/Integration/Queue/Redis/ThrottlesExceptionsRedisStoreTest.php
  • tests/Integration/Queue/ThrottlesExceptionsTest.php
  • tests/Integration/Queue/UniqueJobTest.php
  • tests/Integration/Queue/UniqueUntilProcessingJobTest.php
  • tests/Integration/Queue/WorkCommandTest.php
  • tests/Integration/RateLimiter/Database/DatabaseStoreTestCase.php
  • tests/Integration/RateLimiter/Database/MariaDb/DatabaseStoreTest.php
  • tests/Integration/RateLimiter/Database/MySql/DatabaseStoreTest.php
  • tests/Integration/RateLimiter/Database/Postgres/DatabaseStoreTest.php
  • tests/Integration/RateLimiter/Database/Sqlite/DatabaseStoreTest.php
  • tests/Integration/RateLimiter/Redis/RedisStoreTest.php
  • tests/Integration/Redis/ConcurrencyLimiterIntegrationTest.php
  • tests/Integration/Redis/DurationLimiterIntegrationTest.php
  • tests/Integration/Redis/EvalWithShaCacheIntegrationTest.php
  • tests/Integration/Routing/RoutingServiceProviderTest.php
  • tests/Integration/Support/Fixtures/MultipleInstanceManager.php
  • tests/Integration/Support/MultipleInstanceManagerTest.php
  • tests/Mail/MailManagerTest.php
  • tests/Queue/LaravelInteropTest.php
  • tests/Queue/Middleware/RateLimitedWithRedisTest.php
  • tests/Queue/Middleware/ThrottlesExceptionsWithRedisTest.php
  • tests/Queue/RateLimitedTest.php
  • tests/RateLimiter/BackoffTest.php
  • tests/RateLimiter/DatabaseStoreTest.php
  • tests/RateLimiter/Fixtures/RateLimiterStoreContract.php
  • tests/RateLimiter/InitializeSwooleTablesTest.php
  • tests/RateLimiter/KeyResolverTest.php
  • tests/RateLimiter/LeakyBucketTest.php
  • tests/RateLimiter/LimitTest.php
  • tests/RateLimiter/LimiterTest.php
  • tests/RateLimiter/PackageMetadataTest.php
  • tests/RateLimiter/PruneCommandTest.php
  • tests/RateLimiter/RateLimiterServiceProviderTest.php
  • tests/RateLimiter/RateLimiterTest.php
  • tests/RateLimiter/RedisStoreTest.php
  • tests/RateLimiter/RegisterPruneTimerTest.php
  • tests/RateLimiter/ResultTest.php
  • tests/RateLimiter/SlidingWindowCalculatorTest.php
  • tests/RateLimiter/SlidingWindowTest.php
  • tests/RateLimiter/SwooleStoreConcurrencyTest.php
  • tests/RateLimiter/SwooleStoreTest.php
  • tests/RateLimiter/SwooleTableManagerTest.php
  • tests/RateLimiter/WorkerArrayStoreTest.php
  • tests/Redis/ConcurrencyLimiterBuilderTest.php
  • tests/Redis/ConcurrencyLimiterTest.php
  • tests/Redis/DurationLimiterBuilderTest.php
  • tests/Redis/DurationLimiterTest.php
  • tests/Reverb/Fixtures/FakeConnection.php
  • tests/Reverb/PackageMetadataTest.php
  • tests/Reverb/Protocols/Pusher/ServerTest.php
  • tests/Reverb/Servers/Hypervel/Scaling/SwooleTableSharedStateLockTest.php
  • tests/Reverb/Servers/Hypervel/Scaling/SwooleTableSharedStateTest.php
  • tests/Routing/PackageMetadataTest.php
  • tests/Routing/RoutingStaticStateTest.php
  • tests/Routing/ThrottleRequestsTest.php
  • tests/Testbench/CommanderTest.php
  • tests/Testbench/Databases/WithMigrationAttributeTest.php
  • tests/Testbench/DefaultConfigurationTest.php

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the cache-backed limiter with a dedicated first-party rate-limiter package and adds fixed-window, weighted sliding-window, leaky-bucket, and backoff policies.

  • Adds atomic Redis, database, Swoole, and worker-array stores with shared result contracts.
  • Integrates the new limiter with routing, queues, Fortify, exception reporting, Reverb, configuration, migrations, and documentation.
  • Adds cross-store sliding-window arithmetic, concurrency, corruption, rollback, and integration coverage.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains in the eligible follow-up review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
src/rate-limiter/src/Concerns/CalculatesRateLimits.php Implements shared fixed-window, sliding-window, leaky-bucket, and backoff calculations; reviewed sliding-window weighting, rotation, denial immutability, and retry arithmetic.
src/rate-limiter/src/RedisStore.php Adds atomic Lua-backed limiter operations whose examined sliding-window arithmetic and state transitions match the PHP implementation.
src/rate-limiter/src/DatabaseStore.php Adds transactional persistent limiter state using primary reads, locking, expiry, and secondary sliding-window values.
src/rate-limiter/src/SwooleStore.php Adds key-scoped locked shared-table operations using the common policy calculations.
src/rate-limiter/src/WorkerArrayStore.php Adds worker-local limiter state for tests and explicitly owned worker-local use cases.
src/rate-limiter/src/SlidingWindow.php Defines the immutable weighted sliding-window policy and its admission-policy modifiers.
src/routing/src/Middleware/ThrottleRequests.php Migrates route throttling to atomic typed limiter decisions, including named, stacked, weighted, and response-based policies.
src/queue/src/Middleware/RateLimited.php Migrates queue rate limiting to named stores and serializable typed middleware state.
src/foundation/config/rate-limiter.php Adds first-party rate-limiter store configuration and selects the database-backed default.
src/rate-limiter/src/Console/stubs/rate-limits.stub Defines the persistent schema required by database-backed policies, including secondary sliding-window state.

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/0.4..." | Re-trigger Greptile

@binaryfire
binaryfire requested a review from albertcht August 7, 2026 09:14
Record the approved weighted two-window algorithm, numeric bounds, retry arithmetic, and per-store state transitions.\n\nDocument the Redis one-key Lua design, schema changes, public API, test matrix, benchmark coverage, and deliberate exclusions so the implementation can be reviewed against one complete specification.
Introduce an immutable SlidingWindow policy with the same Laravel-style period factories and modifiers as the existing admission policies. Add stable key fingerprints and validate capacity, cost, exact-integer limits, and the complete two-window lifetime before store access.\n\nAlso make Limit factories validate their complete public-unit conversion so overflow errors consistently name the seconds, minutes, hours, or days supplied by the caller. Cover factories, immutability, key identity, validation order, boundaries, and error messages.
Add the constant-state weighted two-window transition to the shared calculator, including millisecond quantization, exact retry inversion, logical rotation, rollback-safe weighting, and immutable inspection and denial paths. Wire worker-array, Swoole, and database stores through the same behavior.\n\nGeneralize the shared second state value to secondary_value across memory, Swoole, database, generated migrations, and Testbench. Stop duplicating fixed-window expiry in that field and add exhaustive arithmetic, corruption, expiry, pruning, rotation, contention, schema, and database-driver coverage.
Implement sliding-window admission as one atomic cached Lua execution over a single two-field hash. Derive window position from PTTL, preserve raw reset timing, use HINCRBY for steady-state increments, and avoid Redis TIME, serializer paths, version branching, and additional keys or round trips.\n\nExercise initial, steady, rotated, denied, rollback, malformed-state, expiry, prefix, serializer, compression, NOSCRIPT fallback, and weighted contention behavior against the Redis integration store.
Extend the common store contract with weighted admission, denial and inspection immutability, boundary recovery, expiry, clearing, and parameter isolation so every first-party backend proves the same public behavior.\n\nAdd routing and queue integration coverage showing named sliding-window limits flow through the existing middleware paths and expose their remaining capacity and retry timing without consumer-specific source changes.
Add allowed-heavy and denied-heavy sliding-window cases to the existing Redis, Swoole, and database benchmark matrix. Keep the benchmark on the real manager and store paths so it measures connection handling, Lua execution or shared calculation, result decoding, and cleanup rather than an isolated arithmetic helper.
Explain when to choose fixed windows, sliding windows, leaky buckets, or backoff in the first-party rate-limiting guide. Document SlidingWindow factories, immutable modifiers, first-hit anchoring, weighted approximation, result timing, store support, and practical consumption examples in Laravel-style prose.\n\nUpdate the concise routing, queue, and package-difference references so every public policy list agrees without duplicating the main guide.
Update the original package design record to include SlidingWindow in the final typed-policy inventory, source layout, shared state model, documentation surface, and completion checklist. Point detailed arithmetic and backend behavior to the focused sliding-window plan while retaining Redis backoff's intentionally semantic available_at field.
@binaryfire

Copy link
Copy Markdown
Collaborator Author

@coderabbitai @greptile-apps Please review the latest changes.

Since the previous review, this PR now:

  • adds a first-class SlidingWindow policy with the same immutable factories and modifiers as the other admission policies;
  • implements one constant-state weighted two-window algorithm across worker-array, Swoole, database, Redis, and Valkey stores;
  • adds a Redis-specific one-key Lua path with one cached script execution, two hash fields, TTL-based rotation, and no new Redis or Valkey version requirement;
  • generalizes shared persistent state to secondary_value and updates the package, Testbench, and application migration shapes;
  • adds arithmetic, boundary, rollback, corruption, contention, shared-store-contract, routing, queue, and Redis fallback coverage;
  • updates the rate-limiting guide, supporting documentation, benchmarks, and PR description; and
  • merges the latest 0.4 translation, typing, dependency, and repository-guidance changes without changing the rate-limiter design.

Please take a fresh look at the sliding-window arithmetic, Redis atomic path, cross-store parity, schema change, public API consistency, performance characteristics, and test coverage.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

@binaryfire I will perform a fresh review of the latest changes. I will focus on the sliding-window algorithm, Redis Lua path, store parity, migration schema, public API, performance-sensitive paths, and test coverage.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@binaryfire
binaryfire merged commit 5ef2357 into 0.4 Aug 7, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant