h4kuna / critical-cache
Only one process can to write or to delete to cache.
Requires
- php: >=8.2
- h4kuna/data-type: ^3.0.15
- h4kuna/memoize: ^0.1.7
- psr/clock: ^1.0
- psr/simple-cache: ^3.0
Requires (Dev)
- beste/clock: ^3.0
- h4kuna/dir: ^0.1.7
- malkusch/lock: ^3.0
- nette/caching: ^3.3.1
- nette/tester: ^2.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- tracy/tracy: ^2.9
Suggests
- h4kuna/dir: malkusch/lock and nette/cache need to create directories.
- malkusch/lock: As default implementation for Lock system.
- nette/caching: As default implementation for Cache for PSR-16.
Provides
None
Conflicts
None
Replaces
None
README
Part of the h4kuna PHP libraries, see the overview of all packages.
The library extends PSR-16 with locking, so only one process at a time can write to or delete from the cache.
Installation to project
Requires PHP 8.2 or newer.
composer require h4kuna/critical-cache
Optional, needed by the default implementations:
composer require h4kuna/dir malkusch/lock nette/caching beste/clock
How to use
The easiest way to start is the prepared factory CacheLockingFactory. If an optional dependency is missing, the factory throws an exception that tells you which package to install. The dependencies are not mandatory, because everything can be replaced by your own implementation.
use h4kuna\CriticalCache\PSR16\Locking\CacheLockingFactory; $cacheFactory = new CacheLockingFactory('/my/temp'); $cache = $cacheFactory->create(); assert($cache instanceof Psr\SimpleCache\CacheInterface); $data = $cache->load('foo', fn () => 'done'); echo $data; // done
The method load() first tries to read from the cache. If the data is not null, it is returned. Otherwise, a critical section is created by the lock system (mutex) and the cache is read again, because a parallel process could have been faster. If the data is found now, the lock is released and the data is returned. If not, the callback is called, its result is saved to the cache, the lock is released and the data is returned.
The callback receives h4kuna\CriticalCache\Utils\Dependency, the inner cache and the key. Set $dependency->ttl if the value should expire.
use h4kuna\CriticalCache\Utils\Dependency; $data = $cache->load('foo', function (Dependency $dependency): string { $dependency->ttl = 3600; // seconds return 'done'; });
Methods set(), delete() and clear() are locked as well. Use synchronized() if you need your own critical section.
Pool
Multi-level cache implementing CacheInterface. Caches are used in order Memory -> Filesystem.
use h4kuna\CriticalCache\Nette\NetteCacheFactory; use h4kuna\CriticalCache\PSR16\Pool\CachePoolFactory; use h4kuna\Dir\TempDir; $cacheFactory = new NetteCacheFactory((new TempDir('/my/temp'))->dir('h4kuna/cache')); // dir() creates the directory, nette/caching needs an existing one $cachePoolFactory = new CachePoolFactory($cacheFactory); $cache = $cachePoolFactory->create(); // by default MemoryCache and filesystem cache $cache->set('foo', 1); // write to memory and filesystem $cache1 = $cachePoolFactory->create(); // try to load from memory (not found), then from filesystem (found), save it to memory and return the result echo $cache1->get('foo'); // 1
You can pass your own list of caches (for example Redis or Memcached) to create().
use h4kuna\CriticalCache\PSR16\MemoryCache; $cache = $cachePoolFactory->create([new MemoryCache(), $redisCache]);
Lock
By default, malkusch/lock is used. If you implement the LockOriginal and Lock interfaces, you can use a different library.
use h4kuna\CriticalCache\PSR16\Locking\CacheLockingFactory; $cacheFactory = new CacheLockingFactory($myPSR16CacheFactory, $myLockOriginal);
The default mutex is FlockMutex, which is why h4kuna/dir is needed. If you use a different mutex, you don't need it.
Cache
By default, nette/caching is used with its PSR-16 adapter.
Clock PSR-20
CachePoolFactory uses beste/clock if you don't pass your own Psr\Clock\ClockInterface.
Services
UseOneTimeService
The service stores a value, for example a token, which can be read only once.
/** @var \h4kuna\CriticalCache\Services\UseOneTimeService $useOneTimeService */ $timeToLive = 900; // seconds $useOneTimeService->set('foo', 'token', $timeToLive); // the value is removed from the cache after 900 seconds or after the first call of get() $useOneTimeService->get('foo'); // token $useOneTimeService->get('foo'); // null
ValidityAwareCache
The service tells you whether something is valid. You can choose the time window in which it is valid.
/** @var \h4kuna\CriticalCache\Caching\ValidityAwareCache $validityAwareCache */ $validityAwareCache->set('foo', new DateTime('tomorrow midnight')); // valid from now to tomorrow midnight $item = $validityAwareCache->get('foo'); // TimeRangeItem $item->isValid(); // true if now is in the range, false if it is out of the range $item->value(); // stored value (empty string by default) if valid, null if invalid $item->from; // null means unlimited, otherwise DateTimeImmutable $item->to; // null means the key does not exist, otherwise DateTimeImmutable $validityAwareCache->set('bar', 3600, 300, 'lorem'); // 'lorem' is valid in 5 minutes, for one hour $validityAwareCache->delete('bar');
TokenService
The service generates a token and keeps it for a defined time. The token can be used only once.
/** @var \h4kuna\CriticalCache\Services\TokenService $tokenService */ $token = $tokenService->make(); // string token, uuid v4 by default $tokenService->isEqual($token); // true, then the token is removed // if you want to compare the value yourself, use get() $token = $tokenService->make(value: 'lorem'); $value = $tokenService->get($token); // lorem $tokenService->isEqual($token, $value); // false, because get() already removed the token
UniqueHashQueueService
The service generates unique values, which are checked against a source, for example a database. It creates a lock for the critical section and takes one unique value from the queue.
For the example, we use RandomGeneratorMock from the tests, which generates the alphabet: A, B, C, D ... Z, AA ... In production use RandomGenerator (uuid v4) or your own implementation of RandomGeneratorInterface.
// implement UniqueValueServiceInterface or extend UniqueValueServiceAbstract $checkUniqueValue = new class extends \h4kuna\CriticalCache\Services\UniqueValueServiceAbstract { public function __construct() { parent::__construct(new \h4kuna\CriticalCache\Tests\Mock\RandomGeneratorMock(), queueSize: 5); } public function check(array $data, ?object $dataSet = null): iterable { // example: $data = ['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E']; // SELECT unique_column FROM foo WHERE unique_column IN ('A', 'B', 'C', 'D', 'E'); // return the values which already exist, for example B, C return ['B', 'C']; } }; /** @var \h4kuna\CriticalCache\Services\UniqueHashQueueService $uniqueHash */ $value = $uniqueHash->execute($checkUniqueValue); // unique value, E $value = $uniqueHash->execute($checkUniqueValue); // unique value, D $value = $uniqueHash->execute($checkUniqueValue); // unique value, A
PauseAfterUse
The service runs a task and then waits a few seconds before it allows the next run.
use h4kuna\CriticalCache\Services\PauseService; /** @var \h4kuna\CriticalCache\Services\PauseAfterUse $pauseAfterUse */ /** @var \Psr\Clock\ClockInterface $clock */ $pauseService = new class ($clock, 3) extends PauseService { protected function run(): void { var_dump('hello'); } }; $pauseAfterUse->execute($pauseService); // execute run() $pauseAfterUse->execute($pauseService); // sleep 3 seconds and execute run()