Skip to content

Make array cache request-local and add worker-array store#406

Merged
binaryfire merged 8 commits into
0.4from
feature/request-local-array-cache
Jun 29, 2026
Merged

Make array cache request-local and add worker-array store#406
binaryfire merged 8 commits into
0.4from
feature/request-local-array-cache

Conversation

@binaryfire

@binaryfire binaryfire commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Laravel's array cache driver is effectively request-scoped because the PHP process normally ends after each request. Hypervel workers are long-lived, so keeping array values on the store object made the public API behave differently and allowed cache data to persist across unrelated requests handled by the same worker. This PR refactors array to store its mutable data in coroutine context so it behaves as request / unit-of-work local in Hypervel too. The old worker-lifetime behavior is still useful for intentional worker-local state, such as Reverb's websocket message rate limiter and tests that need mutexes or locks to survive coroutine boundaries, so it is preserved as a separate worker-array cache driver.

For more details, see: docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md

Summary

  • Makes array cache values, locks, and tag markers coroutine-local through CoroutineContext.
  • Adds worker-array as an explicit worker-lifetime in-memory cache store.
  • Updates cache manager driver resolution to support multi-word built-in driver names.
  • Moves Reverb's message rate limiter to worker-array so websocket message counters persist across messages in a worker.
  • Updates tests that intentionally need shared cache state to use worker-array with comments explaining why.
  • Documents the behavior difference in Boost cache docs and the cache package README.

Details

Request-Local array

ArrayStore now keeps separate coroutine-context buckets for cache values and lock records. Each store instance gets unique context keys, so manually-created stores still do not share state inside the same coroutine. The buckets are plain arrays so copied coroutine context receives a starting snapshot without sharing future writes.

Shared cache behavior now lives in AbstractArrayStore, while ArrayStore and WorkerArrayStore only define how items and locks are stored. ArrayLock no longer reaches into a public lock array; it uses store methods instead.

Worker-Lifetime worker-array

WorkerArrayStore preserves the old in-memory worker-lifetime behavior using instance properties. Persistence comes from the manager-cached repository/store, not static state, so multiple configured stores remain isolated.

The foundation cache config now lists and defines worker-array next to array. The app skeleton config needs the same store entry, handled separately in the skeleton repo.

Reverb And Tests

Reverb's Pusher server caches its RateLimiter on the server object and uses it across websocket messages handled by the same worker. That counter must be worker-local rather than request-local, so it now uses worker-array.

A few tests also intentionally need cache state to survive request or coroutine boundaries:

  • Inertia throttle test: limiter counter across two requests.
  • Queue unique after-response test: unique lock across child coroutine / after-response boundaries.
  • Scheduler mutex tests: mutexes across scheduler coroutines.

Those tests now use worker-array explicitly and include comments explaining why.

Tests

  • Added request-local array-store coroutine isolation tests for values, locks, tags, and copied context behavior.
  • Added worker-array tests for values, TTL, serialization, counters, touch, tags, locks, flush separation, and cross-coroutine sharing.
  • Added cache manager tests for worker-array construction, multi-word driver resolution, and custom creator precedence.
  • Added Reverb coverage for worker-array message rate limiting.
  • Ran composer fix successfully: cs-fixer, phpstan, and the full parallel test suite are green.

Review

Claude reviewed the implementation and signed off. The only post-review suggestion was adding an inline comment explaining why ArrayStore's static context-key sequence is intentionally not reset; that has been included.

Summary by CodeRabbit

  • New Features

    • Added a new worker-array cache store for worker-lifetime in-memory caching.
    • The default array cache store now behaves as request/local coroutine-scoped storage.
  • Bug Fixes

    • Improved cache isolation so separate coroutines and store instances no longer share unexpected state.
    • Lock flushing now preserves cached values, and value flushing no longer clears locks.
  • Documentation

    • Updated cache docs to explain the difference between array and worker-array stores.

Document the full design for making the array cache store request-local in Hypervel while preserving the current worker-lifetime behavior as an explicit worker-array store.

The plan records the Laravel reference behavior, Hypervel coroutine-context implications, storage and lock design, configuration changes, documentation updates, Reverb usage, and the required test coverage so the implementation can be reviewed against the agreed shape.
Extract shared array-store behavior into AbstractArrayStore and move ArrayStore's mutable values and lock records into CoroutineContext.

ArrayStore now uses unique per-instance context keys so separate manually-created stores do not share data inside the same coroutine. The static key sequence is intentionally never reset because live context buckets may still reference earlier suffixes.

ArrayLock now works through store methods instead of reaching into a public locks array, preserving lock behavior while allowing both request-local and worker-lifetime backends.
Add WorkerArrayStore for deliberate worker-lifetime in-memory cache state.

CacheManager now resolves multi-word internal driver names with Str::studly(), adds createWorkerArrayDriver(), and keeps custom creators ahead of built-in drivers. The foundation cache config lists worker-array alongside array while keeping the default and failover behavior unchanged.
Update array-store tests to assert behavior instead of the removed public lock array.

Add coroutine-isolation coverage for request-local ArrayStore values, locks, tags, and copied context behavior. Add WorkerArrayStore coverage for values, TTLs, serialization, counters, touch, tags, locks, flush separation, and cross-coroutine sharing. Extend CacheManager tests for worker-array construction, multi-word driver resolution, and custom creator precedence.
Reverb's Pusher server keeps a RateLimiter instance on the server object and uses it across websocket messages handled by the same worker.

Switch that limiter from the request-local array store to worker-array so message counters persist for the worker lifetime as intended. Add coverage that the counter is written to worker-array, not array, and that repeated messages still trigger the configured rate-limit error.
Update tests that intentionally need cache state to survive request or coroutine boundaries.

The Inertia throttle test uses worker-array because the limiter counter must survive both requests. Queue unique-after-response coverage uses worker-array because unique locks must be visible across the after-response child coroutine. Scheduler mutex tests use WorkerArrayStore because scheduling mutexes must survive across scheduler coroutines.

The SubMinuteSchedulingTest setup now resolves Schedule after binding the test mutexes so the schedule receives the intended mutex implementations.
Document Hypervel's two in-memory array cache stores.

The Boost cache docs now describe array as request-local / unit-of-work local and worker-array as worker-lifetime state, including coroutine context copy behavior and object-reference behavior when serialization is disabled. The cache package README records the Laravel difference and the explicit worker-array store for deliberate worker-local persistence.
@coderabbitai

coderabbitai Bot commented Jun 29, 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: 9 minutes

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: 945c09c0-5822-491a-99a8-222f4fee9ecb

📥 Commits

Reviewing files that changed from the base of the PR and between d34101f and ba98001.

📒 Files selected for processing (1)
  • src/cache/src/AbstractArrayStore.php
📝 Walkthrough

Walkthrough

Refactors Hypervel's array cache store to be coroutine-scoped using CoroutineContext, extracts shared logic into a new AbstractArrayStore, adds a WorkerArrayStore for worker-lifetime in-memory caching, updates CacheManager to resolve multi-word driver names via Str::studly, switches Reverb's rate limiter to worker-array, and updates tests and documentation accordingly.

Changes

Array Cache Coroutine-Local Refactor + Worker-Array Store

Layer / File(s) Summary
AbstractArrayStore contract and implementation
src/cache/src/AbstractArrayStore.php
Introduces the abstract base class with concrete implementations of all cache operations (get, put, increment, touch, flush, flushLocks, lock, etc.) and declares abstract primitives for cache-item and lock-record persistence.
ArrayLock refactored to store-method API
src/cache/src/ArrayLock.php
Updates constructor to accept AbstractArrayStore and rewrites acquire, forceRelease, exists, getCurrentOwner, refresh, and getRemainingLifetime to call getLockRecord/putLockRecord/forgetLockRecord instead of directly accessing store->locks.
ArrayStore refactored to coroutine-local storage
src/cache/src/ArrayStore.php
Replaces class-owned $storage/$locks arrays with CoroutineContext reads/writes keyed by per-instance context keys derived from a static suffix counter; now extends AbstractArrayStore.
WorkerArrayStore with instance-level storage
src/cache/src/WorkerArrayStore.php
New class extending AbstractArrayStore backed by plain PHP instance arrays, providing worker-lifetime shared caching with no coroutine isolation.
CacheManager wiring and config
src/cache/src/CacheManager.php, src/foundation/config/cache.php
Switches driver method derivation from ucfirst to Str::studly to support multi-word driver names; adds createWorkerArrayDriver; registers the worker-array store in the foundation config.
Reverb rate limiter store change
src/reverb/src/Protocols/Pusher/Server.php
ensureWithinRateLimit switches cache store from 'array' to 'worker-array' so rate-limiter state persists across coroutines within a worker.
Cache unit and isolation tests
tests/Cache/CacheWorkerArrayStoreTest.php, tests/Cache/CacheArrayStoreCoroutineIsolationTest.php, tests/Cache/CacheArrayStoreTest.php, tests/Cache/CacheManagerTest.php
Adds CacheWorkerArrayStoreTest (storage, TTL, locks, tags, coroutine sharing), CacheArrayStoreCoroutineIsolationTest (per-coroutine isolation for values/locks/tags/copied context), and updates existing array store and manager tests for new semantics.
Integration and other test updates
tests/Integration/Console/CommandSchedulingTest.php, tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php, tests/Integration/Queue/JobDispatchingTest.php, tests/Inertia/InertiaServiceProviderTest.php, tests/Reverb/Protocols/Pusher/ServerTest.php
Updates scheduling mutex, unique-job lock, rate-limiter, and Reverb server tests to use worker-array where cross-coroutine state persistence is required.
Documentation and design plan
docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md, src/boost/docs/cache.md, src/cache/README.md
Adds the full design/implementation plan document and updates cache documentation to describe array vs worker-array store semantics and coroutine context behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hoppity-hop through coroutines we go,
Each request gets its own cache burrow below.
The worker-array holds on, sharing its hoard,
While array resets when the context is poured.
Abstract the base, let subclasses decide —
No more shared locks where they shouldn't reside! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.71% 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 clearly summarizes the main change: making the array cache request-local and adding the worker-array store.
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/request-local-array-cache

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 Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR refactors ArrayStore to store its mutable data in coroutine context so the array cache driver behaves as request-local in long-lived Hypervel workers, and introduces WorkerArrayStore as an explicit worker-lifetime in-memory store for cases like Reverb's message rate limiter and cross-coroutine mutexes.

  • ArrayStore now keeps values and lock records in CoroutineContext using per-instance unique keys derived from a static sequence counter, giving each coroutine its own isolated bucket; WorkerArrayStore preserves the old instance-property-backed behavior.
  • CacheManager gains a createWorkerArrayDriver method and uses Str::studly() on driver names so multi-word identifiers like worker-array resolve correctly, with custom creators still taking precedence.
  • Reverb's Server rate limiter, the Inertia throttle test, the queue unique-job test, and the scheduler mutex tests are updated to use worker-array where state legitimately needs to survive coroutine boundaries.

Confidence Score: 5/5

Safe to merge. The coroutine isolation and worker-lifetime split is correctly implemented, tests cover the key behavioral guarantees, and no incorrect logic was found.

The core design — CoroutineContext-backed ArrayStore, instance-property-backed WorkerArrayStore, Str::studly() driver dispatch in CacheManager — is straightforward and correct. Lock operations, flush separation, tagged-cache isolation, and the Reverb rate-limiter migration all check out. The static sequence counter on ArrayStore is correctly left unreset with an explanatory comment, and the test suite exercises coroutine interleaving, copied-context copy-on-write, and manager multi-word driver resolution.

No files require special attention.

Important Files Changed

Filename Overview
src/cache/src/ArrayStore.php Refactored to store values and locks in CoroutineContext using per-instance unique keys; static sequence counter correctly never resets to avoid key collisions with live buckets.
src/cache/src/AbstractArrayStore.php New abstract base extracting shared cache/lock logic; flush() and flushLocks() operate on separate buckets; increment() correctly keeps its read-modify-write path non-yielding with a clarifying comment scoped to WorkerArrayStore.
src/cache/src/WorkerArrayStore.php New worker-lifetime store using instance properties; clean implementation with no static state and no flushState() needed.
src/cache/src/ArrayLock.php Correctly delegates all lock record access through AbstractArrayStore methods instead of reaching into public arrays; acquire/release/refresh/getRemainingLifetime logic is correct for both permanent and TTL locks.
src/cache/src/CacheManager.php Adds createWorkerArrayDriver and uses Str::studly() for driver method resolution; custom creators correctly checked before built-in method dispatch, preserving existing extension behavior.
src/reverb/src/Protocols/Pusher/Server.php Rate limiter correctly migrated to worker-array store so message counters persist across WebSocket messages in the same worker; lazy initialization via ??= is appropriate for a server singleton.
tests/Cache/CacheArrayStoreCoroutineIsolationTest.php New test covering value, lock, and tag isolation across coroutines plus copied-context copy-on-write semantics; uses usleep() correctly to force coroutine interleaving.
tests/Cache/CacheWorkerArrayStoreTest.php New test covering values, TTL, serialization, increment/decrement, touch, tags, locks, and cross-coroutine sharing for WorkerArrayStore.
src/foundation/config/cache.php Adds worker-array store entry alongside the updated array store; updated driver comment lists both drivers correctly.

Reviews (2): Last reviewed commit: "fix(cache): align array store touch key ..." | Re-trigger Greptile

Comment thread src/cache/src/AbstractArrayStore.php
Comment thread src/cache/src/AbstractArrayStore.php Outdated

@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 `@src/reverb/src/Protocols/Pusher/Server.php`:
- Line 215: The rate limiter initialization in Server::... currently
hard-depends on the named worker-array cache store, which can fail on older
published cache configs. Update the rateLimiter setup to use the cache manager’s
built-in worker-array driver directly via build(['driver' => 'worker-array']) or
add a safe fallback before constructing RateLimiter, so the code does not
require cache.stores.worker-array to exist.
🪄 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: 4abcbf1f-f8d7-495f-8f16-b62cd9993a07

📥 Commits

Reviewing files that changed from the base of the PR and between 966f9e5 and d34101f.

📒 Files selected for processing (19)
  • docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md
  • src/boost/docs/cache.md
  • src/cache/README.md
  • src/cache/src/AbstractArrayStore.php
  • src/cache/src/ArrayLock.php
  • src/cache/src/ArrayStore.php
  • src/cache/src/CacheManager.php
  • src/cache/src/WorkerArrayStore.php
  • src/foundation/config/cache.php
  • src/reverb/src/Protocols/Pusher/Server.php
  • tests/Cache/CacheArrayStoreCoroutineIsolationTest.php
  • tests/Cache/CacheArrayStoreTest.php
  • tests/Cache/CacheManagerTest.php
  • tests/Cache/CacheWorkerArrayStoreTest.php
  • tests/Inertia/InertiaServiceProviderTest.php
  • tests/Integration/Console/CommandSchedulingTest.php
  • tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php
  • tests/Integration/Queue/JobDispatchingTest.php
  • tests/Reverb/Protocols/Pusher/ServerTest.php

Comment thread src/reverb/src/Protocols/Pusher/Server.php
Remove the redundant getPrefix() call from AbstractArrayStore::touch() so touch uses the same key path as get, put, forget, and increment.

Qualify the increment read/modify/write comment so it clearly applies to WorkerArrayStore's shared worker-lifetime backing without implying the request-local ArrayStore has the same coroutine-sharing concern.

This resolves the accepted Greptile feedback from PR #406 while keeping the rejected CodeRabbit Reverb fallback and docstring threshold items out of the codebase. Focused cache/Reverb tests and composer fix were green before commit.
@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@binaryfire binaryfire merged commit 9ae4b86 into 0.4 Jun 29, 2026
33 of 36 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