Skip to content

Add limiter leases and harden lock refresh behavior#428

Merged
binaryfire merged 24 commits into
0.4from
feature/limiter-leases-lock-refresh
Jul 9, 2026
Merged

Add limiter leases and harden lock refresh behavior#428
binaryfire merged 24 commits into
0.4from
feature/limiter-leases-lock-refresh

Conversation

@binaryfire

@binaryfire binaryfire commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR turns the concurrency limiter into a more full-featured primitive: acquire a slot, hold it for as long as the caller owns the work, and release it explicitly when done.

The existing callback API remains. It is now sugar over the public lease path. This gives long-lived workers, connection pools, streamed jobs, and multi-step operations a first-class way to hold bounded capacity without wrapping everything in one closure.

While doing that, this PR also hardens the lock refresh surface. Hypervel already had refreshable locks, and Laravel has since added similar APIs. The merged result keeps Laravel's refresh() naming where it fits, but preserves Hypervel's stronger semantics: explicit non-positive refresh TTLs throw, permanent refreshes verify ownership, and database locks re-extend their crash-safety timeout rather than pretending a TTL-less backend can hold a truly permanent lock safely.

Finally, this ports Redis Cluster hash-tag safety to the multi-key Redis paths touched by this work: Redis funnels, Redis-backed cache funnels, and Redis queues.

For more detailes, see: docs/plans/2026-07-09-limiter-leases-and-lock-hardening.md

What Changed

Limiter leases

  • Added Hypervel\Contracts\Limiters\Lease.
  • Added Hypervel\Contracts\Limiters\RefreshableLease.
  • Added one shared Hypervel\Contracts\Limiters\LimiterTimeoutException.
  • Removed the duplicate cache and redis limiter timeout exception classes.
  • Added Redis::funnel(...)->acquire().
  • Added Cache::funnel(...)->acquire().
  • Kept then() and block() behavior, but routed callback-scoped execution through the same lease acquisition path.
  • Added concrete lease implementations for Redis funnels and cache-backed funnels.
  • Made cache funnel leases refreshable when the underlying lock supports refresh.
  • Fixed the Redis cache funnel fast path so acquired leases keep their original TTL and refresh(null) actually re-extends that TTL.

Lock refresh hardening

  • Added isLocked() to the cache lock contract.
  • Added base refresh() and getRemainingLifetime() fallbacks to the abstract cache lock.
  • Tightened RefreshableLock semantics and docs.
  • Hardened Redis, array, database, file, and no-op lock behavior.
  • Made file locks refreshable with owner-checked refresh and lifetime inspection.
  • Fixed database locks so expired rows are not treated as owned and cannot be revived by refresh.
  • Fixed array locks so expired records are treated as absent at the owner lookup boundary.
  • Fixed Redis lock acquisition comparisons to match actual Redis wrapper return types.
  • Fixed FileStore lock contention handling to catch the filesystem lock timeout exception that is actually thrown.

Redis Cluster safety

  • Added RedisConnection::hasHashTag().
  • Hash-tagged Redis funnel slot keys on cluster connections.
  • Hash-tagged Redis-backed cache funnel slot keys while preserving store-prefix round-tripping.
  • Hash-tagged Redis queue storage keys for queue, delayed, reserved, and notify keys.
  • Kept logical queue names unchanged in payloads and public queue APIs.

Package hardening

  • Added explicit Hypervel\Support\now imports where standalone packages were relying on the global foundation helper.
  • Ported the previously skipped database lock/cache store integration tests out of Todo and into the active database integration suite.
  • Updated Boost docs and package READMEs with lease, refresh, timeout exception, and Redis Cluster behavior.

Design Notes

The important API choice is that leases are capability-based, just like refreshable locks. Code that only needs release works against Lease. Code that can heartbeat a held slot checks for RefreshableLease. There is no isRefreshable() flag and no method that advertises support only to throw later.

The Redis paths stay efficient. Funnel acquisition still uses the existing Lua slot claim. Refresh and release are owner-checked atomic Lua operations. Cluster tagging is computed once per limiter or queue instance, not inside the hot script path.

The non-Redis cache funnel path still uses the cache lock provider abstraction. That means each driver gets the best behavior its backend can support: Redis uses atomic Lua, database uses guarded SQL updates, file uses filesystem locks, and array/worker-array stays in-memory and non-yielding for coroutine safety.

The database driver is intentionally different from native-TTL drivers. A database row cannot expire itself. Non-positive acquisition durations therefore map to the default timeout, and refresh(null) re-applies that same safety timeout. That is the clean behavior for a TTL-less backend because it keeps crash recovery intact.

Testing

Validated with composer fix, which ran:

  • php-cs-fixer
  • phpstan
  • test:parallel
  • package-mode testbench coverage

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Added refreshable, releasable concurrency limiter leases and lock refresh/lifetime inspection.
    • Introduced acquire()-based slot acquisition with explicit lease release/refresh.
    • Added isLocked() support for locks.
  • Bug Fixes

    • Hardened lock ownership and expiry handling (including permanent lock refresh behavior).
    • Improved concurrency/rate limiter timeout precision and exception routing.
  • Documentation

    • Updated cache/Redis/queue docs with lease-based examples and Redis Cluster hash-tag behavior.
  • Tests

    • Expanded coverage for lease refresh, remaining lifetime, and cluster-safe key behavior.

binaryfire added 16 commits July 9, 2026 16:01
Record the agreed design for exposing concurrency limiter leases, hardening refreshable locks, unifying limiter timeout exceptions, and adding Redis Cluster hash-tag safety.

The plan captures the source audit, consensus decisions, implementation order, test coverage, and documentation updates so the following commits have a clear design reference.
Introduce the Lease and RefreshableLease capability contracts for public concurrency limiter slots, plus a single Contracts\Limiters\LimiterTimeoutException shared by cache funnels, redis funnels, and redis throttles.

Remove the older backend-specific timeout exception classes so generic limiter code can catch one exception regardless of which limiter implementation is used.
Add explicit Hypervel\Support\now imports to cache and Sanctum files that call the helper unqualified.

This keeps standalone package installs from depending on the global foundation helper while preserving the same Date-backed behavior.
Add isLocked() to the lock contract and provide base refresh() and getRemainingLifetime() fallbacks on the abstract cache lock.

Update RefreshableLock documentation to describe the unified refresh semantics: refresh(null) re-applies the acquisition duration as interpreted by the driver, while explicit non-positive TTLs are rejected.
Make RedisLock refresh(null) verify current ownership for permanent locks and reject explicit non-positive refresh TTLs with the shared message.

Tighten acquisition checks to the actual Redis wrapper return types, using set(...) === true and setnx(...) === 1, and keep phpredis serializer-sensitive refresh arguments split so TTL conversion stays numeric.

Extend Redis lock coverage across unit and integration tests, including serializer/compression refresh paths and ownership-sensitive permanent refresh behavior.
Treat expired array lock records as absent at the owner lookup boundary so ownership, release, refresh, and isLocked() all agree.

Simplify release through the shared ownership check, keep the refresh read-modify-write non-yielding for WorkerArrayStore, and add boundary coverage for remaining lifetime and expired records.
Filter expired rows when reading database lock owners and require an unexpired matching owner in the refresh UPDATE itself.

Rework database refresh(null) to re-extend the same default timeout used for non-positive acquisition durations, since TTL-less database rows need a crash-safety expiration.

Return the nullable database connection name honestly and cover expired ownership, refresh, lifetime, and isLocked() behavior in the database lock tests.
Add owner-checked FileStore refresh and lifetime inspection using file locks, make FileLock implement RefreshableLock, and route acquisition through the narrowed FileStore accessor.

Use a named permanent timestamp sentinel, return null for missing, expired, or permanent lifetimes, and preserve the file driver's atomicity with exclusive refresh writes and shared lifetime reads.

Also fix FileStore contention handling to catch the filesystem LockTimeoutException actually thrown by LockableFile, and keep permission checks strict.
Make NoLock report isLocked() as false while keeping refresh(null) aligned with the shared ownership-check semantics for permanent locks.

Add tests for refresh, lifetime, and locked-state behavior so generic lock code sees the same API shape even when the backend deliberately performs no locking.
Add RedisConnection::hasHashTag() so cluster-sensitive code can detect existing Redis Cluster hash tags before wrapping keys.

Cover normal, empty, missing, and already-tagged key forms to match the Redis Cluster rule that only non-empty brace contents count as a hash tag.
Add the Redis concurrency lease implementation and make Redis::funnel() expose acquire() through the limiter and builder.

Route then() and block() through the same lease acquisition path, release callback-scoped leases in finally blocks, keep fire-and-forget block() behavior, and use millisecond deadlines with a fakeable time source.

Hash-tag Redis funnel slot keys on cluster connections so the multi-key Lua acquire script stays on one Redis Cluster slot, while leaving pre-tagged names unchanged.

Cover lease acquire, release, refresh, lifetime, timeout, callback exception handling, fire-and-forget behavior, and cluster key tagging in unit and integration tests.
Move Redis throttle timeout handling onto the shared Contracts\Limiters\LimiterTimeoutException and update the builder/tests to match.

Use the same millisecond-precise, fakeable deadline loop style as the funnel limiters so block() does not oversleep the configured timeout.
Add cache-backed concurrency lease implementations and expose acquire() from Cache::funnel() through both the limiter and builder.

Return RefreshableLease instances when the claimed slot is backed by a RefreshableLock, preserve non-refreshable lock providers through the base Lease contract, and keep then()/block() as callback-scoped sugar over the same acquisition path.

Update the Redis cache fast path to construct locks with the acquisition TTL instead of restoring them as permanent locks, so refresh(null) re-extends the lease as intended.

Hash-tag Redis-backed cache funnel slots for cluster connections, keep prefix round-tripping intact, and cover lease lifecycle, TTL reclaim, refresh, serializer, prefix, and cluster behavior across unit and integration tests.
Add cluster-safe Redis queue storage key resolution that wraps untagged queue names in Redis Cluster hash tags while preserving logical queue names in payloads and public getQueue() output.

Cache the cluster-connection check on the queue instance and apply the storage-key resolver to queue, delayed, reserved, and notify keys used by multi-key queue operations.

Cover clustered, non-clustered, and pre-tagged queue names so Redis Queue Lua operations stay same-slot on cluster connections without changing non-cluster behavior.
Move the previously skipped database cache store and database lock integration tests out of Todo into the active Integration/Database suite.

Drop the stale skip wrappers, update namespaces, and keep the tests aligned with the current database lock/store APIs, including owner checks, connection names, refresh behavior, and expired-row handling.
Update cache docs with funnel lease acquisition, refreshable lease heartbeats, leaked-lease TTL reclaim, isLocked(), and the refined lock refresh semantics across drivers.

Add Redis funnel and throttle documentation, including the new lease API and automatic Redis Cluster hash-tagging for funnel and queue storage keys.

Record Hypervel's intentional Laravel differences in the cache and redis package READMEs, including unified limiter timeout exceptions, refresh capability interfaces, explicit non-positive refresh rejection, and lease acquisition APIs.
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@binaryfire, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a531efbd-00e9-4e45-a970-e5aca60384dd

📥 Commits

Reviewing files that changed from the base of the PR and between 15d69b3 and 2297029.

📒 Files selected for processing (2)
  • tests/Queue/FailOnExceptionMiddlewareTest.php
  • tests/Testbench/Concerns/DefineCacheRoutesTest.php
📝 Walkthrough

Walkthrough

This PR adds limiter lease contracts and implementations, reworks cache and Redis concurrency limiters to return leases, hardens lock refresh and lifetime behavior across drivers, adds Redis Cluster hash-tag safety for limiter and queue keys, and updates related docs and tests. LimiterTimeoutException moves to Hypervel\Contracts\Limiters.

Changes

Limiter leases, lock hardening, and cluster safety

Layer / File(s) Summary
Plan document
docs/plans/2026-07-09-limiter-leases-and-lock-hardening.md
Documents the limiter lease, lock hardening, Redis Cluster safety, documentation, and testing plan.
Lease contracts and exception namespace
src/contracts/src/Limiters/*, src/contracts/src/Cache/Lock.php, src/contracts/src/Cache/RefreshableLock.php, src/contracts/src/Redis/LimiterTimeoutException.php
Adds limiter lease interfaces, adds isLocked() to the cache lock contract, updates refreshable-lock docs, renames LimiterTimeoutException, and removes the old Redis-namespaced exception file.
Lock driver refresh and lifetime behavior
src/cache/src/Lock.php, ArrayLock.php, DatabaseLock.php, RedisLock.php, NoLock.php, FileLock.php, FileStore.php, AbstractArrayStore.php, Redis/Operations/AllTag/*, sanctum/src/*
Adds lock refresh and lifetime fallbacks, changes permanent-lock refresh handling, treats expired array/database locks as absent, upgrades file-backed locks with owner-checked refresh and remaining-lifetime inspection, and adds explicit now() imports in affected files.
Cache concurrency limiter leases
src/cache/src/Limiters/*
Adds cache lease classes, reworks cache concurrency limiter acquisition and builder flow to return leases, updates the Redis fast path, and hardens Redis limiter slot acquisition semantics.
Redis concurrency limiter leases and cluster tags
src/redis/src/Limiters/*, src/redis/src/RedisConnection.php
Adds Redis lease classes, reworks Redis concurrency and duration limiters to use lease-based acquisition and millisecond timing, and adds Redis Cluster hash-tag detection for slot-key generation.
RedisQueue cluster-safe keys
src/queue/src/RedisQueue.php
Adds cached cluster detection and cluster-safe queue key resolution across Redis queue operations.
Documentation updates
src/boost/docs/*.md, src/cache/README.md, src/redis/README.md
Updates docs and READMEs to describe lock refresh behavior, isLocked(), lease-based funnel and throttle usage, and Redis Cluster hash-tag wrapping.
Support code
src/foundation/src/Testing/Concerns/InteractsWithRedis.php, src/testbench/src/Concerns/HandlesRoutes.php
Adds Redis test connection helpers and refactors testbench route cleanup during cached-route reloads.
Tests
tests/**
Updates cache, Redis, queue, database, integration, and testbench coverage for leases, refresh semantics, cluster tags, and route cleanup.
Validation and cleanup
docs/todo.md, tests/Validation/ValidationEmailRuleTest.php
Adds a Redis wrapper TODO and removes the no-op email-rule test body.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • hypervel/components#406: Both PRs modify cache lock behavior, including ArrayLock ownership, refresh, and remaining-lifetime semantics.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: new limiter leases plus stricter lock refresh behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/limiter-leases-lock-refresh

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 Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR turns the Redis and cache concurrency limiters into first-class primitives by adding an explicit acquire()Lease path alongside the existing block()/then() callback APIs, hardens lock refresh semantics across all drivers (array, database, file, Redis, no-op), and adds Redis Cluster hash-tag safety to funnel and queue storage keys.

  • Limiter leases: New Lease and RefreshableLease contracts; acquire() added to both builder and limiter classes; block() and then() now delegate through the same lease path using finally for unconditional release; the cache limiter yields RefreshableConcurrencyLease when the underlying lock driver supports it.
  • Lock hardening: DatabaseLock and ArrayLock now filter expired records at the DB/store boundary; FileLock gains owner-checked atomic refresh via FileStore::refreshIfOwned(); all drivers' refresh(null) on a permanent lock now verifies ownership rather than returning unconditional true; Redis lock acquisition comparisons fixed to strict (===/=== 1).
  • Redis Cluster safety: RedisConnection::hasHashTag() added as a shared utility; slot keys in both funnel paths and all four queue storage keys (queues:, :delayed, :reserved, :notify) are hash-tagged when the connection is a cluster and no tag is already present.

Confidence Score: 5/5

PR is safe to merge. All lock and lease paths have been verified for correctness, ownership semantics are consistent across drivers, and the cluster tagging logic handles edge cases correctly.

The core timeout comparison is intentional and correct. Lock ownership checks are symmetric across acquire/release/refresh paths on every driver. The Lua return-value contract for acquireConcurrencySlot is preserved: prefixed KEYS go in, unprefixed name-plus-index comes back, and store->lock() re-applies the prefix exactly once. No logic regressions were found.

No files require special attention.

Important Files Changed

Filename Overview
src/redis/src/Limiters/ConcurrencyLimiter.php Adds acquire()/claimSlot() split, millisecond-precision timeout, and Redis Cluster hash-tag safety; removes stored $name property (now only $keyPrefix)
src/redis/src/Limiters/ConcurrencyLease.php New RefreshableLease implementation for Redis funnel slots; permanent-slot refresh correctly bypasses phpredis serialization via withoutSerializationOrCompression()
src/cache/src/Limiters/RedisConcurrencyLimiter.php Adds cluster hash-tag to name before parent construction; uses prefixedSlots for Lua KEYS and restores to store->lock() (was restoreLock()) for accurate prefix handling
src/cache/src/Limiters/ConcurrencyLimiter.php Refactored to add acquire() primary method; block() delegates through it; millisecond-precision timeout replaces second-resolution check; RefreshableLock detection yields RefreshableConcurrencyLease
src/cache/src/Limiters/ConcurrencyLimiterBuilder.php Adds public acquire() method; then() now acquires a lease then runs callback in finally block, which correctly handles exceptions
src/queue/src/RedisQueue.php Adds getQueueRedisKey() for cluster-safe storage keys (hash-tagged); getQueue() preserved for payload/public API usage; isCluster cached per-instance
src/redis/src/RedisConnection.php Adds static hasHashTag() helper; correctly handles empty braces, no-close-brace, and adjacent-brace edge cases per Redis Cluster hash-tag spec
src/cache/src/DatabaseLock.php getCurrentOwner() now filters expired rows; getRemainingLifetime() also filters expired rows; expiresAt() made parameterized; refresh() guards non-positive TTLs correctly
src/cache/src/ArrayLock.php getCurrentOwner() now checks expiry via isFuture(); refresh() for permanent locks now correctly verifies ownership rather than returning unconditional true
src/cache/src/FileLock.php Implements RefreshableLock; delegates to FileStore::refreshIfOwned() and remainingSeconds(); fileStore() cast helper replaces @PHPStan-Ignore workaround
src/cache/src/FileStore.php Adds refreshIfOwned() (atomic exclusive-lock guarded) and remainingSeconds(); introduces PERMANENT_TIMESTAMP constant replacing magic number 9999999999; LockTimeoutException import fixed
src/cache/src/RedisLock.php Acquisition comparisons changed from == to ===; setnx return type fixed to === 1; refresh() for permanent locks verifies ownership rather than returning unconditional true
src/cache/src/Lock.php Adds refresh() and getRemainingLifetime() base fallbacks that throw RuntimeException; adds isLocked() via getCurrentOwner() != null
src/contracts/src/Limiters/Lease.php New contract: release() + owner(); well-designed capability-based interface
src/contracts/src/Limiters/RefreshableLease.php New contract: extends Lease with refresh() and getRemainingLifetime(); semantics mirror RefreshableLock

Reviews (3): Last reviewed commit: "Harden throwable-path test assertions" | Re-trigger Greptile

Comment thread src/redis/src/Limiters/ConcurrencyLease.php
Comment thread src/cache/src/DatabaseLock.php

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/cache/src/ArrayLock.php (1)

98-110: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider avoiding double record fetch in refresh().

refresh() fetches the lock record on line 108, then calls isOwnedByCurrentProcess() on line 110, which fetches the record again via getCurrentOwner(). The already-fetched $record could be used for both the existence and ownership checks, avoiding the redundant store lookup.

♻️ Proposed refactor
     $record = $this->store->getLockRecord($this->name);

-    if ($record === null || ! $this->isOwnedByCurrentProcess()) {
+    if ($record === null) {
+        return false;
+    }
+
+    $expiresAt = $record['expiresAt'];
+    if ($expiresAt !== null && ! $expiresAt->isFuture()) {
+        return false;
+    }
+
+    if ($record['owner'] !== $this->owner) {
         return false;
     }

This inlines the expiry and ownership checks from getCurrentOwner() using the already-fetched $record, eliminating the second store lookup. The trade-off is duplicating the expiry-check logic, so the current approach is also acceptable if you prefer DRY over the minor efficiency gain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cache/src/ArrayLock.php` around lines 98 - 110, In refresh() of
ArrayLock, avoid fetching the lock record twice: the existing $record from
getLockRecord() should be reused for both the existence check and the ownership
check instead of calling isOwnedByCurrentProcess(), which triggers another store
lookup via getCurrentOwner(). Update the refresh flow to derive ownership from
the already-fetched record, keeping the same expiry behavior while removing the
redundant access.
tests/Cache/CacheDatabaseLockTest.php (1)

141-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add : void return types to new test methods.

As per coding guidelines, test methods should use : void return types. The four new test methods (testExpiredLockIsNotLockedOrOwned, testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout, testRefreshReturnsFalseWhenExpired, testGetConnectionNameCanReturnNull) are missing this return type.

♻️ Proposed fix
-    public function testExpiredLockIsNotLockedOrOwned()
+    public function testExpiredLockIsNotLockedOrOwned(): void
-    public function testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout()
+    public function testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout(): void
-    public function testRefreshReturnsFalseWhenExpired()
+    public function testRefreshReturnsFalseWhenExpired(): void
-    public function testGetConnectionNameCanReturnNull()
+    public function testGetConnectionNameCanReturnNull(): void

Also applies to: 236-236, 254-254, 334-334

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Cache/CacheDatabaseLockTest.php` at line 141, Add explicit : void
return types to the new test methods in CacheDatabaseLockTest, including
testExpiredLockIsNotLockedOrOwned,
testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout,
testRefreshReturnsFalseWhenExpired, and testGetConnectionNameCanReturnNull.
Update each method signature in the test class so it follows the project’s test
method convention without changing the test logic.

Source: Coding guidelines

tests/Cache/CacheArrayStoreTest.php (1)

229-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add : void return types to new test methods.

As per coding guidelines, test methods should use : void return types. All four new test methods (testExpiredLockIsNotLockedOrOwned, testExpiredLockCannotBeReleasedByOldOwner, testRefreshReturnsFalseWhenExpired, testRefreshOnPermanentLockReturnsFalseWhenNotOwned) are missing this return type.

♻️ Proposed fix
-    public function testExpiredLockIsNotLockedOrOwned()
+    public function testExpiredLockIsNotLockedOrOwned(): void
-    public function testExpiredLockCannotBeReleasedByOldOwner()
+    public function testExpiredLockCannotBeReleasedByOldOwner(): void
-    public function testRefreshReturnsFalseWhenExpired()
+    public function testRefreshReturnsFalseWhenExpired(): void
-    public function testRefreshOnPermanentLockReturnsFalseWhenNotOwned()
+    public function testRefreshOnPermanentLockReturnsFalseWhenNotOwned(): void

Also applies to: 289-300, 521-532, 544-552

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Cache/CacheArrayStoreTest.php` around lines 229 - 245, Add the missing
: void return type to each new test method in CacheArrayStoreTest, including
testExpiredLockIsNotLockedOrOwned, testExpiredLockCannotBeReleasedByOldOwner,
testRefreshReturnsFalseWhenExpired, and
testRefreshOnPermanentLockReturnsFalseWhenNotOwned. Update the method signatures
only, keeping the existing test logic unchanged, and ensure all newly added
PHPUnit test methods in this class follow the project’s void-return convention.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/boost/docs/cache.md`:
- Around line 868-897: The lease handling example in the cache docs should stop
assuming refresh always succeeds. Update the loop around
Cache::funnel(...)->acquire() so the code checks the result of
RefreshableLease::refresh() before proceeding to the next chunk, and if it
returns false, stop processing or reacquire the lease before continuing. Keep
the guidance tied to RefreshableLease, refresh(), and the explicit release() in
the try/finally flow.

In `@src/boost/docs/redis.md`:
- Around line 402-458: The Redis funnel lease example ignores the boolean result
from Lease::refresh(), so the loop can continue after the slot is no longer
owned. Update the sample around Redis::funnel(), acquire(), and refresh() to
check the return value before calling runStep() for the next iteration, and
either break/abort or reacquire the lease when refresh() fails.

In `@tests/Cache/CacheNoLockTest.php`:
- Around line 78-84: The new test method testIsLockedAlwaysReturnsFalse should
follow the test coding guidelines by declaring a void return type. Update the
method signature in CacheNoLockTest so the test method explicitly returns void,
matching the style used across tests/**/*.php.

In `@tests/Cache/CacheRedisLockTest.php`:
- Around line 144-191: Add explicit : void return types to the four new test
methods in CacheRedisLockTest:
testRefreshOnPermanentLockReturnsTrueWhenStillOwned,
testRefreshOnPermanentLockReturnsFalseWhenNotOwned,
testIsLockedReturnsTrueWhenLockExists, and
testIsLockedReturnsFalseWhenLockDoesNotExist. Keep the existing assertions and
mock expectations unchanged; only update the method signatures to match the test
convention used in tests/**/*.php.

In `@tests/Integration/Cache/RedisCacheFunnelTest.php`:
- Line 92: The new test method testFastPathLeaseRefreshReappliesReleaseAfterTtl
should declare a void return type to match the test coding guidelines. Update
the method signature in RedisCacheFunnelTest so this test explicitly returns
void, consistent with the other test methods in the class.

---

Nitpick comments:
In `@src/cache/src/ArrayLock.php`:
- Around line 98-110: In refresh() of ArrayLock, avoid fetching the lock record
twice: the existing $record from getLockRecord() should be reused for both the
existence check and the ownership check instead of calling
isOwnedByCurrentProcess(), which triggers another store lookup via
getCurrentOwner(). Update the refresh flow to derive ownership from the
already-fetched record, keeping the same expiry behavior while removing the
redundant access.

In `@tests/Cache/CacheArrayStoreTest.php`:
- Around line 229-245: Add the missing : void return type to each new test
method in CacheArrayStoreTest, including testExpiredLockIsNotLockedOrOwned,
testExpiredLockCannotBeReleasedByOldOwner, testRefreshReturnsFalseWhenExpired,
and testRefreshOnPermanentLockReturnsFalseWhenNotOwned. Update the method
signatures only, keeping the existing test logic unchanged, and ensure all newly
added PHPUnit test methods in this class follow the project’s void-return
convention.

In `@tests/Cache/CacheDatabaseLockTest.php`:
- Line 141: Add explicit : void return types to the new test methods in
CacheDatabaseLockTest, including testExpiredLockIsNotLockedOrOwned,
testRefreshOnDefaultTimeoutLockReappliesDefaultTimeout,
testRefreshReturnsFalseWhenExpired, and testGetConnectionNameCanReturnNull.
Update each method signature in the test class so it follows the project’s test
method convention without changing the test logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e78576ab-f9b4-4c40-a629-6923e34f6404

📥 Commits

Reviewing files that changed from the base of the PR and between 9178be1 and 3397ed2.

📒 Files selected for processing (66)
  • docs/plans/2026-07-09-limiter-leases-and-lock-hardening.md
  • src/boost/docs/cache.md
  • src/boost/docs/queues.md
  • src/boost/docs/redis.md
  • src/cache/README.md
  • src/cache/src/AbstractArrayStore.php
  • src/cache/src/ArrayLock.php
  • src/cache/src/DatabaseLock.php
  • src/cache/src/FileLock.php
  • src/cache/src/FileStore.php
  • src/cache/src/Limiters/ConcurrencyLease.php
  • src/cache/src/Limiters/ConcurrencyLimiter.php
  • src/cache/src/Limiters/ConcurrencyLimiterBuilder.php
  • src/cache/src/Limiters/RedisConcurrencyLimiter.php
  • src/cache/src/Limiters/RefreshableConcurrencyLease.php
  • src/cache/src/Lock.php
  • src/cache/src/NoLock.php
  • src/cache/src/Redis/Operations/AllTag/Add.php
  • src/cache/src/Redis/Operations/AllTag/AddEntry.php
  • src/cache/src/Redis/Operations/AllTag/FlushStale.php
  • src/cache/src/Redis/Operations/AllTag/Put.php
  • src/cache/src/Redis/Operations/AllTag/PutMany.php
  • src/cache/src/Redis/Operations/AllTag/Remember.php
  • src/cache/src/Redis/Operations/AllTag/Touch.php
  • src/cache/src/RedisLock.php
  • src/contracts/src/Cache/Lock.php
  • src/contracts/src/Cache/RefreshableLock.php
  • src/contracts/src/Limiters/Lease.php
  • src/contracts/src/Limiters/LimiterTimeoutException.php
  • src/contracts/src/Limiters/RefreshableLease.php
  • src/contracts/src/Redis/LimiterTimeoutException.php
  • src/queue/src/RedisQueue.php
  • src/redis/README.md
  • src/redis/src/Limiters/ConcurrencyLease.php
  • src/redis/src/Limiters/ConcurrencyLimiter.php
  • src/redis/src/Limiters/ConcurrencyLimiterBuilder.php
  • src/redis/src/Limiters/DurationLimiter.php
  • src/redis/src/Limiters/DurationLimiterBuilder.php
  • src/redis/src/RedisConnection.php
  • src/sanctum/src/Console/Commands/PruneExpired.php
  • src/sanctum/src/PersonalAccessToken.php
  • src/sanctum/src/SanctumGuard.php
  • tests/Cache/CacheArrayStoreTest.php
  • tests/Cache/CacheDatabaseLockTest.php
  • tests/Cache/CacheFileStoreTest.php
  • tests/Cache/CacheNoLockTest.php
  • tests/Cache/CacheRedisLockTest.php
  • tests/Cache/ConcurrencyLimiterTest.php
  • tests/Cache/RedisLockTest.php
  • tests/Integration/Cache/CacheFunnelTestCase.php
  • tests/Integration/Cache/FileCacheLockTest.php
  • tests/Integration/Cache/PhpRedisCacheFunnelTest.php
  • tests/Integration/Cache/PhpRedisCacheLockTest.php
  • tests/Integration/Cache/RedisCacheFunnelTest.php
  • tests/Integration/Cache/RedisCacheLockTest.php
  • tests/Integration/Database/DatabaseCacheStoreTest.php
  • tests/Integration/Database/DatabaseLockTest.php
  • tests/Integration/Database/Todo/DatabaseLockTest.php
  • tests/Integration/Redis/ConcurrencyLimiterIntegrationTest.php
  • tests/Integration/Redis/DurationLimiterIntegrationTest.php
  • tests/Queue/QueueRedisQueueTest.php
  • tests/Redis/ConcurrencyLimiterBuilderTest.php
  • tests/Redis/ConcurrencyLimiterTest.php
  • tests/Redis/DurationLimiterBuilderTest.php
  • tests/Redis/DurationLimiterTest.php
  • tests/Redis/RedisConnectionTest.php
💤 Files with no reviewable changes (2)
  • src/contracts/src/Redis/LimiterTimeoutException.php
  • tests/Integration/Database/Todo/DatabaseLockTest.php

Comment thread src/boost/docs/cache.md
Comment thread src/boost/docs/redis.md
Comment thread tests/Cache/CacheNoLockTest.php Outdated
Comment thread tests/Cache/CacheRedisLockTest.php Outdated
Comment thread tests/Integration/Cache/RedisCacheFunnelTest.php Outdated
Move the custom Redis connection helper into InteractsWithRedis so integration tests can create serializer, prefix, and pool-size variants through one shared test concern.

Keep the more general max-connections option from RedisProxyIntegrationTest and make createRedisConnectionWithPrefix delegate to the same helper, removing the duplicate private helper from the proxy test.
Run permanent concurrency-lease ownership checks through RedisProxy::withoutSerializationOrCompression so Lua-written raw owner ids compare correctly on serializer-enabled phpredis connections.

Add an integration regression with PhpRedis::SERIALIZER_PHP and document the remaining Redis transformed-command return-type audit in docs/todo.md.
Capture the current timestamp once in DatabaseLock::getRemainingLifetime, query only unexpired rows, and calculate the remaining lifetime from that same timestamp.

Update the cache lock tests to assert the expiration predicate and model expired locks as absent from the live-row query, matching the source behavior.
Register cleanup immediately after defineCacheRoutes writes a testbench route file so failures during route:cache or cache-file verification cannot leave route files behind for the worker lifetime.

Skip cleanup during the intentional reloadApplication cycle, re-register it in the finally block after reload teardown resets callbacks, and always delete tracked route files even when cache-path cleanup cannot use a Hypervel application instance.

Add regression coverage for the failure path and a RouteCacheCommandTest precondition that reports leaked testbench route filenames directly if another leak ever appears.
Delete the upstream placeholder testNativeValidationVsRfcCompliant method because its data set is empty and PHPUnit flags it as risky for performing zero assertions.

Leave a REMOVED marker at the upstream-matching location so future Laravel test syncs do not mechanically re-port the no-op method.
Update the refreshable lock and funnel lease examples so copied code checks the boolean result from refresh().

The examples now throw when a lock or lease has been lost instead of continuing work outside the limiter. This matches the RefreshableLock and RefreshableLease contracts, where false means the owner no longer holds the resource or the resource expired.

The snippets keep the framework documentation's class-code style, including explicit RuntimeException imports.
Finish the return-type cleanup across the test files touched by the limiter lease and lock hardening work.

Every PHPUnit test method in the eight affected files now declares : void, matching the components testing standard instead of only fixing the individual methods flagged by CodeRabbit.

Also replace the RedisProxy fully qualified return type in DurationLimiterIntegrationTest with an import while touching that file, keeping imports consistent with the components guide.

Focused test runs passed for each edited file, and composer fix passed before this commit.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/Testbench/Concerns/DefineCacheRoutesTest.php`:
- Around line 129-149: The exception assertion in
testDefineCacheRoutesCleansRouteFileWhenRouteCacheFails is being swallowed
because the fail() call sits inside the catch(Throwable) block. Move the
“expected failure” check out of the catch by using expectException() or a
separate flag/after-try assertion so defineCacheRoutes() must actually throw
before the cleanup assertions run; keep the cleanup verification in the same
test method using the existing testbenchRouteFiles and
callBeforeApplicationDestroyedCallbacks symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 792f89f3-ba16-43fd-a902-f1dca654264f

📥 Commits

Reviewing files that changed from the base of the PR and between 3397ed2 and 15d69b3.

📒 Files selected for processing (19)
  • docs/todo.md
  • src/boost/docs/cache.md
  • src/boost/docs/redis.md
  • src/cache/src/DatabaseLock.php
  • src/foundation/src/Testing/Concerns/InteractsWithRedis.php
  • src/redis/src/Limiters/ConcurrencyLease.php
  • src/testbench/src/Concerns/HandlesRoutes.php
  • tests/Cache/CacheArrayStoreTest.php
  • tests/Cache/CacheDatabaseLockTest.php
  • tests/Cache/CacheNoLockTest.php
  • tests/Cache/CacheRedisLockTest.php
  • tests/Integration/Cache/PhpRedisCacheFunnelTest.php
  • tests/Integration/Cache/RedisCacheFunnelTest.php
  • tests/Integration/Foundation/Console/RouteCacheCommandTest.php
  • tests/Integration/Redis/ConcurrencyLimiterIntegrationTest.php
  • tests/Integration/Redis/DurationLimiterIntegrationTest.php
  • tests/Integration/Redis/RedisProxyIntegrationTest.php
  • tests/Testbench/Concerns/DefineCacheRoutesTest.php
  • tests/Validation/ValidationEmailRuleTest.php
💤 Files with no reviewable changes (1)
  • tests/Integration/Redis/RedisProxyIntegrationTest.php
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/redis/src/Limiters/ConcurrencyLease.php
  • tests/Cache/CacheNoLockTest.php
  • src/boost/docs/redis.md
  • tests/Integration/Cache/PhpRedisCacheFunnelTest.php
  • src/boost/docs/cache.md
  • tests/Cache/CacheRedisLockTest.php
  • tests/Integration/Cache/RedisCacheFunnelTest.php
  • tests/Integration/Redis/ConcurrencyLimiterIntegrationTest.php
  • tests/Cache/CacheDatabaseLockTest.php
  • tests/Cache/CacheArrayStoreTest.php

Comment thread tests/Testbench/Concerns/DefineCacheRoutesTest.php
Capture expected throwables outside catch blocks in tests that need to keep asserting cleanup or job state after the exception path.

This prevents PHPUnit assertion failures from being swallowed by broad Throwable catches, so the tests now fail if the exercised code stops throwing while still preserving the post-exception assertions.

Also add explicit void return types to the touched queue middleware tests to match the repository test style.
@binaryfire
binaryfire merged commit 858a35d into 0.4 Jul 9, 2026
36 checks passed
@binaryfire
binaryfire deleted the feature/limiter-leases-lock-refresh branch July 21, 2026 09:08
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