-
-
Notifications
You must be signed in to change notification settings - Fork 17
Make array cache request-local and add worker-array store #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
aae01b8
docs: plan array cache lifecycle split
binaryfire d13e06b
refactor(cache): make array store context backed
binaryfire 7ef51b8
feat(cache): add worker-array store
binaryfire e0aa84f
test(cache): cover array store lifetimes
binaryfire 21f434a
fix(reverb): use worker-array for message rate limits
binaryfire 7427f89
test: use worker-array for shared cache state
binaryfire d34101f
docs(cache): document array cache lifetimes
binaryfire ba98001
fix(cache): align array store touch key handling
binaryfire File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
1,371 changes: 1,371 additions & 0 deletions
1,371
docs/plans/2026-06-29-array-cache-coroutine-local-worker-array.md
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,298 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Hypervel\Cache; | ||
|
|
||
| use Hypervel\Contracts\Cache\CanFlushLocks; | ||
| use Hypervel\Contracts\Cache\LockProvider; | ||
| use Hypervel\Support\Carbon; | ||
| use Hypervel\Support\InteractsWithTime; | ||
| use RuntimeException; | ||
|
|
||
| abstract class AbstractArrayStore extends TaggableStore implements CanFlushLocks, LockProvider | ||
| { | ||
| use InteractsWithTime; | ||
| use RetrievesMultipleKeys; | ||
|
|
||
| /** | ||
| * Indicates if values are serialized within the store. | ||
| */ | ||
| protected bool $serializesValues; | ||
|
|
||
| /** | ||
| * The classes that should be allowed during unserialization. | ||
| */ | ||
| protected array|bool|null $serializableClasses; | ||
|
|
||
| /** | ||
| * Create a new array-family store. | ||
| */ | ||
| public function __construct(bool $serializesValues = false, array|bool|null $serializableClasses = null) | ||
| { | ||
| $this->serializesValues = $serializesValues; | ||
| $this->serializableClasses = $serializableClasses; | ||
| } | ||
|
|
||
| /** | ||
| * Get all of the cached values and their expiration times. | ||
| * | ||
| * @return array<string, array{value: mixed, expiresAt: float}> | ||
| */ | ||
| public function all(bool $unserialize = true): array | ||
| { | ||
| $storage = $this->getCacheItems(); | ||
|
|
||
| if ($unserialize === false || $this->serializesValues === false) { | ||
| return $storage; | ||
| } | ||
|
|
||
| foreach ($storage as $key => $data) { | ||
| $storage[$key] = [ | ||
| 'value' => $this->unserialize($data['value']), | ||
| 'expiresAt' => $data['expiresAt'], | ||
| ]; | ||
| } | ||
|
|
||
| return $storage; | ||
| } | ||
|
|
||
| /** | ||
| * Retrieve an item from the cache by key. | ||
| */ | ||
| public function get(string $key): mixed | ||
| { | ||
| $item = $this->getCacheItem($key); | ||
|
|
||
| if ($item === null) { | ||
| return null; | ||
| } | ||
|
|
||
| $expiresAt = $item['expiresAt']; | ||
|
|
||
| if ($expiresAt !== 0.0 && (now()->getPreciseTimestamp(3) / 1000) >= $expiresAt) { | ||
| $this->forget($key); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| return $this->serializesValues ? $this->unserialize($item['value']) : $item['value']; | ||
| } | ||
|
|
||
| /** | ||
| * Store an item in the cache for a given number of seconds. | ||
| */ | ||
| public function put(string $key, mixed $value, int $seconds): bool | ||
| { | ||
| $this->putCacheItem($key, [ | ||
| 'value' => $this->serializesValues ? serialize($value) : $value, | ||
| 'expiresAt' => $this->calculateExpiration($seconds), | ||
| ]); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Increment the value of an item in the cache. | ||
| */ | ||
| public function increment(string $key, int $value = 1): int | ||
| { | ||
| // When backed by WorkerArrayStore, this read/modify/write path is shared across coroutines; keep it non-yielding. | ||
| if (! is_null($existing = $this->get($key))) { | ||
| $incremented = ((int) $existing) + $value; | ||
|
|
||
| /** @var array{value: mixed, expiresAt: float} $item */ | ||
| $item = $this->getCacheItem($key); | ||
| $item['value'] = $this->serializesValues ? serialize($incremented) : $incremented; | ||
| $this->putCacheItem($key, $item); | ||
|
|
||
| return $incremented; | ||
| } | ||
|
|
||
| $this->forever($key, $value); | ||
|
|
||
| return $value; | ||
| } | ||
|
|
||
| /** | ||
| * Decrement the value of an item in the cache. | ||
| */ | ||
| public function decrement(string $key, int $value = 1): int | ||
| { | ||
| return $this->increment($key, $value * -1); | ||
| } | ||
|
|
||
| /** | ||
| * Store an item in the cache indefinitely. | ||
| */ | ||
| public function forever(string $key, mixed $value): bool | ||
| { | ||
| return $this->put($key, $value, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Adjust the expiration time of a cached item. | ||
| */ | ||
| public function touch(string $key, int $seconds): bool | ||
| { | ||
| $item = $this->getCacheItem($key); | ||
|
|
||
| if ($item === null) { | ||
| return false; | ||
| } | ||
|
|
||
| $item['expiresAt'] = $this->calculateExpiration($seconds); | ||
| $this->putCacheItem($key, $item); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Remove an item from the cache. | ||
| */ | ||
| public function forget(string $key): bool | ||
| { | ||
| return $this->forgetCacheItem($key); | ||
| } | ||
|
|
||
| /** | ||
| * Remove all items from the cache. | ||
| */ | ||
| public function flush(): bool | ||
| { | ||
| $this->clearCacheItems(); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Remove all locks from the store. | ||
| * | ||
| * @throws RuntimeException | ||
| */ | ||
| public function flushLocks(): bool | ||
| { | ||
| if (! $this->hasSeparateLockStore()) { | ||
| throw new RuntimeException('Flushing locks is only supported when the lock store is separate from the cache store.'); | ||
| } | ||
|
|
||
| $this->clearLockRecords(); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Get the cache key prefix. | ||
| */ | ||
| public function getPrefix(): string | ||
| { | ||
| return ''; | ||
| } | ||
|
|
||
| /** | ||
| * Get a lock instance. | ||
| */ | ||
| public function lock(string $name, int $seconds = 0, ?string $owner = null): ArrayLock | ||
| { | ||
| return new ArrayLock($this, $name, $seconds, $owner); | ||
| } | ||
|
|
||
| /** | ||
| * Restore a lock instance using the owner identifier. | ||
| */ | ||
| public function restoreLock(string $name, string $owner): ArrayLock | ||
| { | ||
| return $this->lock($name, 0, $owner); | ||
| } | ||
|
|
||
| /** | ||
| * Determine if the lock store is separate from the cache store. | ||
| */ | ||
| public function hasSeparateLockStore(): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Get the lock record for the given name. | ||
| * | ||
| * @return null|array{owner: ?string, expiresAt: ?Carbon} | ||
| */ | ||
| abstract public function getLockRecord(string $name): ?array; | ||
|
|
||
| /** | ||
| * Store the lock record for the given name. | ||
| * | ||
| * @param array{owner: ?string, expiresAt: ?Carbon} $record | ||
| */ | ||
| abstract public function putLockRecord(string $name, array $record): void; | ||
|
|
||
| /** | ||
| * Remove the lock record for the given name. | ||
| */ | ||
| abstract public function forgetLockRecord(string $name): void; | ||
|
|
||
| /** | ||
| * Remove all lock records. | ||
| */ | ||
| abstract public function clearLockRecords(): void; | ||
|
|
||
| /** | ||
| * Get the cached item for the given key. | ||
| * | ||
| * @return null|array{value: mixed, expiresAt: float} | ||
| */ | ||
| abstract protected function getCacheItem(string $key): ?array; | ||
|
|
||
| /** | ||
| * Store the cached item for the given key. | ||
| * | ||
| * @param array{value: mixed, expiresAt: float} $item | ||
| */ | ||
| abstract protected function putCacheItem(string $key, array $item): void; | ||
|
|
||
| /** | ||
| * Remove the cached item for the given key. | ||
| */ | ||
| abstract protected function forgetCacheItem(string $key): bool; | ||
|
|
||
| /** | ||
| * Remove all cached items. | ||
| */ | ||
| abstract protected function clearCacheItems(): void; | ||
|
|
||
| /** | ||
| * Get all cached items. | ||
| * | ||
| * @return array<string, array{value: mixed, expiresAt: float}> | ||
| */ | ||
| abstract protected function getCacheItems(): array; | ||
|
|
||
| /** | ||
| * Get the expiration time of the key. | ||
| */ | ||
| protected function calculateExpiration(int $seconds): float | ||
| { | ||
| return $this->toTimestamp($seconds); | ||
| } | ||
|
|
||
| /** | ||
| * Get the UNIX timestamp, with milliseconds, for the given number of seconds in the future. | ||
| */ | ||
| protected function toTimestamp(int $seconds): float | ||
| { | ||
| return $seconds > 0 ? (now()->getPreciseTimestamp(3) / 1000) + $seconds : 0; | ||
| } | ||
|
|
||
| /** | ||
| * Unserialize the given value. | ||
| */ | ||
| protected function unserialize(string $value): mixed | ||
| { | ||
| if ($this->serializableClasses !== null) { | ||
| return unserialize($value, ['allowed_classes' => $this->serializableClasses]); | ||
| } | ||
|
|
||
| return unserialize($value); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.