Fix Swoole Cache store races and improve hot-path performance#414
Fix Swoole Cache store races and improve hot-path performance#414binaryfire wants to merge 19 commits into
Conversation
Add the signed-off implementation plans for the Swoole table cache work. The row-concurrency plan records the agreed design for shared pre-fork table state, striped row locks, hashed physical table keys, lock-free reads, row-safe mutators, cleanup and eviction behavior, and Swoole cache lock support. The interval-cache plan is committed alongside it so the follow-up work has its reviewed design preserved before implementation starts.
Introduce SwooleTableState as the cache driver's shared pre-fork state object. It owns the Swoole table, the seeded physical-key hash seed, and the shared striped Swoole\Atomic row locks used by row lifecycle operations. Refactor SwooleTableManager and CacheManager so Swoole cache stores receive that shared state instead of a raw table. This keeps the table and its synchronization primitives together and ensures workers inherit the same shared-memory objects. Rework SwooleStore around short seeded physical table keys for user rows, interval metadata, and lock rows. This avoids Swoole table key truncation, separates control rows from user cache entries, and lets flush operations preserve internal state intentionally. Keep live cache reads lock-free while replacing the old full-row get metadata write with policy-specific metadata updates. LRU uses a partial timestamp write, LFU uses Swoole's numeric incr, and ttl/noeviction reads do not write metadata. Protect single-key mutators with the shared row stripes. add now performs a logical expired-aware check-and-set under one row lock, increment/decrement/touch preserve current expiration without lost updates, and forget coordinates with concurrent writers. Make cleanup and eviction race-safe by scanning without locks, then rechecking and deleting each candidate under its row lock. Eviction is gated behind memory pressure on write paths, skips control rows, and stops when no progress can be made. Move the existing local interval metadata onto hashed control keys so this implementation stands alone before the follow-up interval refresh redesign. Normal cache flushes delete user rows while preserving interval metadata. Add SwooleLock and wire SwooleStore into LockProvider and CanFlushLocks. Lock acquire, release, refresh, owner lookup, force release, and lock flushing all use the same shared row-lock machinery.
Update the Swoole store test suite to work through the new hashed physical table keys instead of raw logical Swoole table keys. Add regressions for expired-aware add semantics, live-row preservation, policy-specific hit metadata updates, expired-read cleanup, increment/decrement expiration preservation, touch behavior, control-row preservation, memory-pressure-gated eviction, exact-expiration stale cleanup, long-key collision avoidance, and seeded hash behavior. Cover SwooleStore's new lock-provider surface in sequential tests, including lock acquisition, owner-checked release, force release, restoreLock, refresh, remaining lifetime, flushLocks, and funnel support.
Add process-level SwooleStore tests that share one pre-created SwooleTableState across forked Swoole processes, matching the production pre-fork table and atomic-lock model. Cover the races that motivated the fix: concurrent add on a missing key, concurrent add over a logically expired physical row, concurrent increment updates, and concurrent lock acquisition. Each regression asserts that only the correct winner count is observed and that the final stored value remains consistent. The test opts out of coroutine wrapping and terminates child processes after writing results so inherited PHPUnit/Testbench shutdown handlers do not interfere with the parent runtime.
Remove SwooleStore from the negative funnel support assertions now that the store implements LockProvider through a real shared Swoole table lock implementation. Keep the remaining unsupported-store coverage for stores that still do not provide cache locks, while positive Swoole funnel behavior is covered in the Swoole store test suite.
Update the current Boost documentation to reflect that the Swoole cache driver now supports atomic cache locks. Add Swoole to the lock-capable driver lists used by cache lock flushing, unique listeners, unique jobs, WithoutOverlapping jobs, and session blocking. Only src/boost/docs is changed; the stale v0.3 docs tree remains untouched.
Store interval registrations as shared control-plane metadata under seeded internal table keys and register those metadata rows in sharded interval index rows. This lets the manager process discover interval caches without relying on worker-local PHP arrays. Refreshes now claim metadata with a microsecond token before resolver execution, run resolvers outside row locks, complete only after the public cache value is written, and clear or reclaim stale claims safely. Same-instance fallback uses the same claim path while ordinary cache misses still avoid interval-index work. This fixes manager-discovery gaps, overlapping refresh races, stale claim freezes, resolver-failure timestamp poisoning, and interval control-row namespace leakage while keeping request hot paths fast.
Replace the eviction-only Swoole cache timer listener with a listener that registers both eviction and interval refresh timers for every configured Swoole cache store. The manager process now periodically calls refreshIntervalCaches(), allowing shared interval metadata to seed and refresh public cache values across workers. Add a small SwooleTimer wrapper around Swoole\Timer::tick so timer registration can be tested cleanly without patching static extension calls. Wire the renamed listener through CacheServiceProvider and add the interval_refresh_interval configuration default in milliseconds. This keeps interval refresh work out of request hot paths while giving interval caches a dedicated short refresh cadence independent of stale-record eviction.
Add dedicated coverage for the shared interval metadata and index design. The tests verify seeded internal metadata keys, sharded index registration, idempotent registration, same-instance fallback before the first timer tick, cross-instance manager refresh, due checks, and successful metadata completion. Cover concurrency and failure edges around refresh claims: fresh claims block overlapping resolver work, stale claims are reclaimable, claim completion and clearing do not overwrite newer claims, resolver exceptions are reported or rethrown appropriately, and failed public value writes clear claims without marking refresh success. Add regressions for control-row preservation through flush and eviction, cheap generic misses that do not consult the interval index, index-row expiration renewal, multi-interval shared-index discovery including same-shard accumulation, and null-returning resolvers preserving live public cache rows.
Add focused tests for the Swoole cache timer listener. The suite verifies that each configured Swoole cache store receives both an eviction timer and an interval refresh timer, while non-Swoole stores are ignored. The tests cover configured intervals and defaults, and exercise the registered callbacks to ensure they resolve the configured cache store and call evictRecords() and refreshIntervalCaches() respectively. This protects the manager-process wiring that makes shared interval cache refreshes automatic without requiring a live Swoole timer in the test process.
Document the interval_refresh_interval option in the Swoole table cache configuration example and explain how it differs from eviction_interval. Eviction remains responsible for stale-record pruning and memory-pressure cleanup, while interval_refresh_interval controls manager-driven interval cache refresh checks. Update the Octane cache interval docs to refer to Hypervel's cache system and describe the first-tick behavior introduced by shared manager refreshes: shared values are seeded by the manager refresh timer, while the registering worker can still resolve through local fallback immediately. Only the current Boost docs are updated; the old docs/docs tree remains untouched.
Document the final performance decisions added after review of the Swoole cache row-concurrency work. The plan now records why production timestamp reads use microtime while preserving Carbon::setTestNow for tests, and why the Swoole table write guard uses a plain loop instead of allocating a Collection and closure on every write. This keeps the design record aligned with the final implementation and explains the performance/correctness tradeoffs for future maintenance.
Use microtime(true) for production Swoole cache timestamps while keeping the existing Carbon path when test time is frozen. getCurrentTimestamp() is used by reads, writes, expiration checks, locks, and interval claims. Avoiding Carbon object construction removes a hot-path allocation without changing the stored timestamp shape or breaking Carbon::setTestNow-based tests. Focused Swoole cache tests and the full composer fix pipeline passed after this change.
Replace the Collection/closure path in SwooleTable::set() with a plain foreach loop while preserving the string column size guard and ValueTooLargeForColumnException behavior. Every Swoole cache write goes through this wrapper, including put, add, increment, touch, lock writes, interval writes, and LRU metadata updates. Removing the per-write Collection and closure allocations keeps the guard intact while reducing write-path overhead. Add a focused regression test that writes a nine-character value into an eight-byte string column and verifies the same exception path still fires.
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR redesigns the Swoole cache store around shared table state, striped row locks, hashed internal keys, lock-provider support, and manager-driven interval refresh timers. It also updates docs and expands test coverage for concurrency, intervals, locks, eviction, and timer wiring. ChangesSwoole cache store redesign
Estimated code review effort: 5 (Critical) | ~120 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR hardens the Swoole cache store for multi-worker correctness and improves hot-path performance by introducing shared row-lock striping, seeded hashed table keys to avoid Swoole key truncation collisions, cheaper hit-metadata updates, and maintenance work moved off per-write paths. It also adds first-class support for the framework cache lock API and implements cross-worker interval cache refresh driven by manager-process timers.
Changes:
- Add shared
SwooleTableState(row-lock stripes + hash seed) and refactorSwooleTableManager/CacheManagerwiring to pass state intoSwooleStore. - Make
SwooleStoreatomic foradd()and other single-key mutations, keep live reads lock-free, and add lock + interval-refresh support (including new manager timers). - Expand the test suite and docs to cover new concurrency/lock/interval semantics and configuration.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Cache/FunnelUnsupportedStoresTest.php | Removes the “Swoole doesn’t support locks” assertion; keeps stack/session as unsupported for funnels. |
| tests/Cache/CreateSwooleTimersTest.php | Adds coverage for manager timers registering eviction + interval-refresh ticks and dispatching callbacks to the correct store. |
| tests/Cache/CacheSwooleStoreTest.php | Updates/expands store-level tests for atomic add(), LRU/LFU metadata behavior, flush/eviction behavior, key hashing, and lock API behavior. |
| tests/Cache/CacheSwooleStoreIntervalTest.php | Adds dedicated interval-refresh tests (shared index discovery, claims, failure handling, flush/eviction invariants). |
| tests/Cache/CacheSwooleStoreConcurrencyTest.php | Adds multi-process regression tests for add(), increment(), and lock acquisition under contention. |
| src/foundation/config/cache.php | Adds interval_refresh_interval config for Swoole store manager refresh cadence. |
| src/cache/src/SwooleTimer.php | Introduces an injectable wrapper around Swoole\\Timer::tick() to simplify timer testing. |
| src/cache/src/SwooleTableState.php | Adds shared state for a table: striped Atomic locks and a shared hash seed. |
| src/cache/src/SwooleTableManager.php | Refactors manager to resolve/cache SwooleTableState instead of raw tables; adds createState(). |
| src/cache/src/SwooleTable.php | Removes Collection allocation from the column-size guard and uses a simple loop. |
| src/cache/src/SwooleStore.php | Implements row-locking for key mutations, seeded hashed keys, safe eviction/stale cleanup behavior, interval shared index + refresh claims, and lock-provider support. |
| src/cache/src/SwooleLock.php | Adds a Swoole-backed lock implementation supporting acquire/release/forceRelease/refresh/remaining-lifetime. |
| src/cache/src/Listeners/CreateTimer.php | Removes the old eviction-only manager timer listener. |
| src/cache/src/Listeners/CreateSwooleTimers.php | Adds manager timers for both eviction and interval refresh per configured Swoole store. |
| src/cache/src/CacheServiceProvider.php | Wires the new CreateSwooleTimers listener on manager start. |
| src/cache/src/CacheManager.php | Updates Swoole driver creation to use SwooleTableState (not a raw table). |
| src/boost/docs/session.md | Documents swoole as supporting atomic locks for session blocking. |
| src/boost/docs/queues.md | Documents swoole as supporting atomic locks for unique jobs and WithoutOverlapping. |
| src/boost/docs/octane.md | Updates wording to Hypervel and documents first-tick interval refresh behavior + same-instance fallback. |
| src/boost/docs/events.md | Documents swoole as supporting atomic locks for unique listeners. |
| src/boost/docs/cache.md | Documents new Swoole interval refresh config and adds swoole to flushLocks support list. |
| docs/plans/2026-07-03-swoole-store-row-concurrency-and-locks.md | Adds detailed design/plan doc for row-locking, hashed keys, eviction changes, and lock API implementation. |
| docs/plans/2026-07-03-swoole-store-interval-cache-refresh.md | Adds detailed design/plan doc for shared interval index and manager-driven refresh claims. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR delivers a thorough correctness and performance overhaul of the Swoole cache store. The most critical fix is making
Confidence Score: 5/5Safe to merge. The core concurrency fixes are well-reasoned, critical sections are small and non-yielding, and the design correctly shares Atomic objects via pre-fork shared memory. The atomic add() fix, lock-free reads, row-locked mutations, and LimitedMaxHeap correction are all correct and well-tested. The two style notes (timer in worker 0 instead of the manager process, and the hasSeparateLockStore documentation gap) are non-blocking observations. No correctness regressions were found. CreateSwooleTimers.php — the switch from the Swoole manager process to worker 0 is worth understanding if large-table eviction latency in worker 0 becomes observable in production. Important Files Changed
Reviews (4): Last reviewed commit: "fix: normalize cache putMany results" | Re-trigger Greptile |
There was a problem hiding this comment.
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/Cache/CacheSwooleStoreIntervalTest.php`:
- Around line 346-354: The tests around
testThrowingTimerResolverIsReportedAndCanRetry are leaving a mocked global
Container instance in place, which can leak the ExceptionHandler into later
tests. Update the affected test methods to wrap each
Container::setInstance($container) usage in try/finally, and in the finally
block restore or clear the global container state after the test finishes. Use
the existing Container::setInstance, ExceptionHandler, and
createState/createStore test setup points to locate and apply the fix.
🪄 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: cada0b1a-8d63-4973-b50d-420ec0be5929
📒 Files selected for processing (23)
docs/plans/2026-07-03-swoole-store-interval-cache-refresh.mddocs/plans/2026-07-03-swoole-store-row-concurrency-and-locks.mdsrc/boost/docs/cache.mdsrc/boost/docs/events.mdsrc/boost/docs/octane.mdsrc/boost/docs/queues.mdsrc/boost/docs/session.mdsrc/cache/src/CacheManager.phpsrc/cache/src/CacheServiceProvider.phpsrc/cache/src/Listeners/CreateSwooleTimers.phpsrc/cache/src/Listeners/CreateTimer.phpsrc/cache/src/SwooleLock.phpsrc/cache/src/SwooleStore.phpsrc/cache/src/SwooleTable.phpsrc/cache/src/SwooleTableManager.phpsrc/cache/src/SwooleTableState.phpsrc/cache/src/SwooleTimer.phpsrc/foundation/config/cache.phptests/Cache/CacheSwooleStoreConcurrencyTest.phptests/Cache/CacheSwooleStoreIntervalTest.phptests/Cache/CacheSwooleStoreTest.phptests/Cache/CreateSwooleTimersTest.phptests/Cache/FunnelUnsupportedStoresTest.php
💤 Files with no reviewable changes (1)
- src/cache/src/Listeners/CreateTimer.php
Tighten the Swoole cache follow-up fixes from the bot review pass. Store interval resolvers as serialized bytes so metadata serialization inside row-lock sections only handles scalar data. Resolver serialization now happens before the metadata lock is acquired, and resolver unserialization/invocation happens after the refresh claim lock is released. Preserve existing interval refresh metadata when an interval is re-registered. This keeps lastRefreshedAt and fresh refreshingAt claims intact while still allowing the resolver and refresh interval to be updated for future refreshes. Harden row-lock critical sections by using allowed_classes=false for increment payload unserialization, preventing object wakeups while the stripe is held and preserving the existing numeric failure behavior for non-numeric payloads. Make control-row detection explicit for interval metadata, interval indexes, and lock rows. Unknown raw rows now behave like cache data and can be removed by flush or eviction instead of being protected forever. Document the Swoole Table primitive limitation in SwooleTableState, including the requested @todo marker for future native CAS / set-if-absent / delete-if-current support. Update the implementation plans to match the final resolver-bytes and explicit-control-key design. Tests add regressions for object wakeups during increment, interval metadata serialization boundaries, interval re-registration state preservation, and raw legacy row cleanup. Validation: vendor/bin/phpunit tests/Cache/CacheSwooleStoreTest.php; vendor/bin/phpunit tests/Cache/CacheSwooleStoreIntervalTest.php; composer fix.
|
@coderabbitai @greptile-apps @copilot thanks for the reviews. I went through the inline comments and pushed the valid follow-up fixes in Fixed in that commit:
A few comments were intentionally left as no-code-change items and are answered inline:
Validation after the follow-up fixes: focused Swoole cache tests passed, |
|
I'll take a fresh look at the fixes in (=^・ω・^=) glad to see these tightened up ✏️ Learnings added
✅ Action performedReview finished.
|
|
All five items check out in
Resolver serialized before the lock — Re-registration preserves
The explanations for the intentional no-code-change items (all-stripe flush lock as a writer barrier, spinlock crash recovery limitation documented rather than unsafe timeout-reset, extra callback argument harmlessness, and test container reset via the global subscriber) are all reasonable. Everything looks good. The PR is in good shape. |
Record the signed-off follow-up plan for PR #414 after the bot review and maintainer review loop. The plan captures the concrete bugs being hardened: manager-process timer execution, over-eviction from the bounded heap, stale eviction candidates, interval stale-overwriter races, hot-key spin behavior, lock-free hit metadata trade-offs, and inconsistent multi-key write results. It also documents the rejected alternatives and the final testing strategy so the implementation can be audited against the decisions that were reviewed before coding.
Move Swoole cache timer registration from OnManagerStart to AfterWorkerStart and elect non-task worker 0 as the single timer owner. Interval refresh resolvers can use coroutine-backed framework services, so running them from the manager process is unsafe. The stale cleanup and eviction timer moves with the interval timer so Swoole cache maintenance has one worker-owned execution model. Keep the eviction callback pointed at evictRecords() directly so flushStaleRecords() still runs periodically even when the table is below the memory threshold. Update the Boost cache docs to describe the elected worker behavior and extend CreateSwooleTimers tests to cover worker 0, other workers, task workers, configured intervals, defaults, and callbacks.
Fix the bounded eviction heap so it rejects invalid limits and discards non-better candidates once full. This makes single-pass policy eviction honor eviction_proportion instead of allowing the candidate heap to grow beyond the intended batch size. Replace full-row eviction candidates with compact fingerprints that include a non-cryptographic value hash and every mutable metadata column. Candidate deletion now re-reads under the row lock and deletes only if the row still matches the scanned fingerprint, preserving stale-delete protection without copying serialized payloads into PHP memory under pressure. Guard interval refresh public-value commits by rechecking ownership after the resolver returns. The commit prep restamps the claim, lost claims return without writing or clearing, and the public write stays outside the metadata lock to avoid nested metadata-to-user row locking and eviction work inside the metadata critical section. Add a small failed-CAS backoff to striped Swoole table locks after a short spin burst. The uncontended path remains one successful CAS, while hot-key contention no longer burns CPU indefinitely. Keep cache hit metadata lock-free on the read hot path and document the accepted expired shell-row behavior. Add regression coverage for heap limits, single-pass LRU/LFU/TTL eviction, stale eviction candidates, lock-free hit shell rows, interval stale-overwriters, lost claims, commit-time cadence, stale-claim timeout selection, stderr exception fallback, and SwooleStore putMany failure aggregation.
Make fallback and native multi-key cache writes return true for empty batches, attempt every key, and return false if any attempted write fails. RetrievesMultipleKeys and StackStore now aggregate with put() first so later writes are not skipped after an earlier failure. Tests pin the ordering because reversing it would silently short-circuit the batch. DatabaseStore now treats non-empty upsert completion as success even when the affected-row count is zero, which is valid for identical-value upserts; real database failures surface as exceptions. Empty database batches now return true without issuing an upsert. TaggedCache had the same empty-batch aggregation bug on the finite-TTL path, so it now matches Repository::putManyForever([]) and the rest of the cache API. Add regression coverage for the shared trait, database upsert no-op writes, stack failure aggregation, memoized/failover delegation, and tagged empty batches.
|
@greptileai @coderabbitai Thanks for the review feedback. I pushed a follow-up hardening pass that addresses the SwooleStore issues and the adjacent cache write-result issues we found while tracing the same paths. What changed:
I also added regression coverage for the timer election, bounded heap behavior, single-pass LRU/LFU/TTL eviction, stale eviction candidate rechecks, interval stale-overwriter/lost-claim cases, lock-free hit shell rows, and the multi-key write result contract. Verification: |
|
I'll do a fresh pass over the new commit now. (=^・ω・^=) nice to see the hardening pass land ✏️ Learnings added
✅ Action performedReview finished.
|
|
The hardening pass looks solid overall. A few observations: Timer election — The
One remaining LRU edge case —
Interval two-phase commit — The restamp-on-commit approach ( The LRU new-entry issue is the only thing I'd flag as a functional concern. Everything else in the hardening pass is sound. |
Summary
This PR fixes several correctness issues in the Swoole table cache store under cross-worker access. The most important one is
add(): it previously checkedexists()and then wrote the value as two separate operations, so two workers could both observe a missing key and both returntrue. It also treated physically present but expired rows as existing. That breaks the normal cacheadd()contract, which is used as a single-winner primitive for dedupe, replay protection, and similar workflows. A concrete example is Fortify's two-factor TOTP replay protection: it writes a replay cache key with the cache repository'sadd()method so the same code cannot be accepted twice during the replay window. With the old Swoole store, two workers racing on the same replay key could both win.The same shared-row problem affected other cache operations.
increment(),decrement(), andtouch()could lose concurrent updates, cleanup and eviction could delete a row that another worker had just recreated, and the old read path wrote whole rows back while updating hit metadata, which could clobber a concurrent value or expiration change. This PR adds short shared row locks around the small critical sections that need atomicity, keeps normal live reads lock-free, changes hit metadata writes to narrow metadata updates, and rechecks cleanup/eviction candidates before deleting them.Performance is part of the fix. The old store wrote a full row back on every cache hit, even when the active eviction policy did not use hit metadata. It also ran stale cleanup after every successful
put()andadd()miss, which scanned the table and made write cost grow with table size. The new store skips hit-metadata writes for TTL/no-eviction reads, updates only the needed metadata for LRU/LFU reads, replaces per-write table scans with an O(1) memory-pressure check, avoids building a Carbon object for production timestamp reads, and removes a Collection allocation from the Swoole table write guard. Normal reads still do not take row locks.This PR also fixes interval cache refresh for Swoole. Interval metadata was stored in the shared table, but the list of registered interval keys was local PHP memory, so the manager process could not discover intervals registered by workers. The manager timer also only ran eviction. Interval registrations are now stored in shared internal metadata rows, a small sharded index lets the manager discover them, and a dedicated interval refresh timer refreshes due intervals without adding work to normal cache misses or hot reads.
The Swoole store now also implements the framework cache lock API. This is a natural extension of the same shared-memory machinery needed for the cache fixes above: row locks already had to exist for
add()and the other single-key mutation paths, so wiring them into a realLockProviderdoes not add overhead to normal reads or writes just to support locks.The design keeps the driver in the same role: a very fast in-memory cache backed by
Swoole\Table. It avoids global locks for ordinary single-key writes, avoids running user callbacks while a row lock is held, keeps interval index work on the manager timer instead of request paths, and maps logical keys through seededxxh128before touchingSwoole\Tableso long user keys cannot be truncated into the same physical row. Writes to different keys only contend when they land on the same lock stripe; same-key writes serialize because that is the required behavior for a shared key.Microbenchmarks
Median ops/sec over five CLI benchmark rounds on the same machine, PHP binary, and Swoole extension. These are raw
SwooleStoremethod calls, not cache repository or facade calls. Write benchmarks are shown at several live-row fill levels because0.4scanned the table on every successful write, while this PR uses an O(1) memory-pressure check and only scans under pressure.N/Arows are after-only because cache locks did not exist in0.4.get() hit, TTL policyget() hit, no-eviction policyget() hit, LRU policyget() hit, LFU policyincr()for hit count.put(), empty tableput(), 1k live rowsput(), 10k live rowsput(), 20k live rowsadd() miss, empty tableadd() miss, 1k live rowsadd() miss, 10k live rowsadd() miss, 20k live rowsadd() existing live keyincrement(), same keyincrement(), 1,024 rotating keystouch() existing keylock()->acquire()/release()The one slower benchmark is
add()for an already-live key. The old path was only a physicalexists()check; the new path has to check logical expiration under the row lock to enforce theadd()contract. It still runs at about 237k ops/sec in this CLI benchmark.Interval refresh runs on the manager timer, off request paths. In the same benchmark, one refresh tick costs about 66 microseconds with no intervals, 78 microseconds with one interval, and 917 microseconds with 100 intervals.
Row Concurrency and Locks
SwooleTableStateso each Swoole table has shared pre-fork row-lock stripes and a shared hash seed.Swoole\Table, avoiding Swoole key truncation and separating user rows from internal control rows.add()so it is atomic for a single key and treats expired rows as logically absent.put(),increment(),decrement(),touch(), andforget()by protecting their single-key mutations with row locks.get()lock-free and cheaper: TTL/no-eviction reads only read and unserialize, LRU reads update onlylast_used_at, and LFU reads use Swoole's numericincr()for the hit counter.microtime(true)while preservingCarbon::setTestNow()behavior for tests.flush()to delete user cache rows while preserving internal control rows for locks and interval metadata.SwooleLockand madeSwooleStoreimplementLockProviderandCanFlushLocks, including owner-aware release, force release, restore, refresh, remaining lifetime, andflushLocks().add(), concurrent increments, lock acquisition, long key hashing, eviction/flush behavior, LRU/LFU metadata updates, and lock API behavior.For more details, see:
docs/plans/2026-07-03-swoole-store-row-concurrency-and-locks.mdInterval Cache Refresh
i:control rows and registered those metadata keys in shardedx:index rows.refreshIntervalCaches()to discover intervals through the shared index, so the manager process can refresh intervals registered by workers.CreateSwooleTimers, which registers both the existing eviction timer and a dedicated interval refresh timer.interval_refresh_intervalconfig for the manager refresh cadence.get()only uses interval fallback for keys registered on the current store instance and never scans the shared interval index.refreshingAtso timer refreshes and local fallback do not run the same resolver at the same time.flush(), stale cleanup, and eviction while allowing the public cached value to be flushed and recreated by the next refresh.For more details, see:
docs/plans/2026-07-03-swoole-store-interval-cache-refresh.mdValidation
composer fixSummary by CodeRabbit
Release Notes
New Features
interval_refresh_interval.Bug Fixes / Improvements
putMany()result semantics across cache stores and tightened eviction/refresh correctness.Documentation
Tests