Skip to content

Commit

Permalink
fix: improve cache warmup by creating required directories
Browse files Browse the repository at this point in the history
Calling the mapper warmup will now go through all caches layers and run
a warmup process. When using `FileSystemCache` that is provided by the
library, the directories required by this cache will be created.

This should fix an issue that could occur when race conditions happened
in an app with a lot of traffic.
  • Loading branch information
romm committed Aug 19, 2023
1 parent 12af3ed commit 8f6faba
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 26 deletions.
13 changes: 11 additions & 2 deletions src/Cache/ChainCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
* @internal
*
* @template EntryType
* @implements CacheInterface<EntryType>
* @implements WarmupCache<EntryType>
*/
final class ChainCache implements CacheInterface
final class ChainCache implements WarmupCache
{
/** @var array<CacheInterface<EntryType>> */
private array $delegates;
Expand All @@ -29,6 +29,15 @@ public function __construct(CacheInterface ...$delegates)
$this->count = count($delegates);
}

public function warmup(): void
{
foreach ($this->delegates as $delegate) {
if ($delegate instanceof WarmupCache) {
$delegate->warmup();
}
}
}

public function get($key, $default = null): mixed
{
foreach ($this->delegates as $i => $delegate) {
Expand Down
28 changes: 20 additions & 8 deletions src/Cache/Compiled/CompiledPhpFileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
use CuyZ\Valinor\Cache\Exception\CacheDirectoryNotWritable;
use CuyZ\Valinor\Cache\Exception\CompiledPhpCacheFileNotWritten;
use CuyZ\Valinor\Cache\Exception\CorruptedCompiledPhpCacheFile;
use CuyZ\Valinor\Cache\WarmupCache;
use DateInterval;
use DateTime;
use Error;
use FilesystemIterator;
use Psr\SimpleCache\CacheInterface;
use Traversable;

use function bin2hex;
Expand All @@ -30,9 +30,9 @@
* @internal
*
* @template EntryType
* @implements CacheInterface<EntryType>
* @implements WarmupCache<EntryType>
*/
final class CompiledPhpFileCache implements CacheInterface
final class CompiledPhpFileCache implements WarmupCache
{
private const TEMPORARY_DIR_PERMISSION = 510;

Expand All @@ -46,6 +46,11 @@ public function __construct(
private CacheCompiler $compiler
) {}

public function warmup(): void
{
$this->createTemporaryDir();
}

public function has($key): bool
{
$filename = $this->path($key);
Expand Down Expand Up @@ -74,11 +79,7 @@ public function set($key, $value, $ttl = null): bool

$code = $this->compile($value, $ttl);

$tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp';

if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) {
throw new CacheDirectoryNotWritable($this->cacheDir);
}
$tmpDir = $this->createTemporaryDir();

/** @infection-ignore-all */
$tmpFilename = $tmpDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));
Expand Down Expand Up @@ -228,6 +229,17 @@ private function getFile(string $filename): PhpCacheFile
return $this->files[$filename];
}

private function createTemporaryDir(): string
{
$tmpDir = $this->cacheDir . DIRECTORY_SEPARATOR . '.valinor.tmp';

if (! is_dir($tmpDir) && ! @mkdir($tmpDir, self::TEMPORARY_DIR_PERMISSION, true)) {
throw new CacheDirectoryNotWritable($this->cacheDir);
}

return $tmpDir;
}

private function path(string $key): string
{
/** @infection-ignore-all */
Expand Down
13 changes: 11 additions & 2 deletions src/Cache/FileSystemCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
* @api
*
* @template EntryType
* @implements CacheInterface<EntryType>
* @implements WarmupCache<EntryType>
*/
final class FileSystemCache implements CacheInterface
final class FileSystemCache implements WarmupCache
{
/** @var array<string, CacheInterface<EntryType>> */
private array $delegates;
Expand All @@ -39,6 +39,15 @@ public function __construct(string $cacheDir = null)
];
}

public function warmup(): void
{
foreach ($this->delegates as $delegate) {
if ($delegate instanceof WarmupCache) {
$delegate->warmup();
}
}
}

public function has($key): bool
{
foreach ($this->delegates as $delegate) {
Expand Down
11 changes: 9 additions & 2 deletions src/Cache/FileWatchingCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
*
* @phpstan-type TimestampsArray = array<string, int>
* @template EntryType
* @implements CacheInterface<EntryType|TimestampsArray>
* @implements WarmupCache<EntryType|TimestampsArray>
*/
final class FileWatchingCache implements CacheInterface
final class FileWatchingCache implements WarmupCache
{
/** @var array<string, TimestampsArray> */
private array $timestamps = [];
Expand All @@ -41,6 +41,13 @@ public function __construct(
private CacheInterface $delegate
) {}

public function warmup(): void
{
if ($this->delegate instanceof WarmupCache) {
$this->delegate->warmup();
}
}

public function has($key): bool
{
foreach ($this->timestamps($key) as $fileName => $timestamp) {
Expand Down
11 changes: 9 additions & 2 deletions src/Cache/KeySanitizerCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
* @internal
*
* @template EntryType
* @implements CacheInterface<EntryType>
* @implements WarmupCache<EntryType>
*/
final class KeySanitizerCache implements CacheInterface
final class KeySanitizerCache implements WarmupCache
{
private static string $version;

Expand All @@ -38,6 +38,13 @@ public function __construct(
$this->sanitize = static fn (string $key) => sha1("$key." . self::$version ??= PHP_VERSION . '/' . Package::version());
}

public function warmup(): void
{
if ($this->delegate instanceof WarmupCache) {
$this->delegate->warmup();
}
}

public function get($key, $default = null): mixed
{
return $this->delegate->get(($this->sanitize)($key), $default);
Expand Down
16 changes: 15 additions & 1 deletion src/Cache/Warmup/RecursiveCacheWarmupService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
namespace CuyZ\Valinor\Cache\Warmup;

use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\Cache\WarmupCache;
use CuyZ\Valinor\Definition\Repository\ClassDefinitionRepository;
use CuyZ\Valinor\Mapper\Object\Factory\ObjectBuilderFactory;
use CuyZ\Valinor\Mapper\Tree\Builder\ObjectImplementations;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\CompositeType;
use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
use CuyZ\Valinor\Type\Parser\TypeParser;
use CuyZ\Valinor\Type\Type;
use CuyZ\Valinor\Type\ClassType;
use CuyZ\Valinor\Type\Types\InterfaceType;
use Psr\SimpleCache\CacheInterface;

use function in_array;

Expand All @@ -23,15 +25,27 @@ final class RecursiveCacheWarmupService
/** @var list<class-string> */
private array $classesWarmedUp = [];

private bool $warmupWasDone = false;

public function __construct(
private TypeParser $parser,
/** @var CacheInterface<mixed> */
private CacheInterface $cache,
private ObjectImplementations $implementations,
private ClassDefinitionRepository $classDefinitionRepository,
private ObjectBuilderFactory $objectBuilderFactory
) {}

public function warmup(string ...$signatures): void
{
if (! $this->warmupWasDone) {
$this->warmupWasDone = true;

if ($this->cache instanceof WarmupCache) {
$this->cache->warmup();
}
}

foreach ($signatures as $signature) {
try {
$this->warmupType($this->parser->parse($signature));
Expand Down
16 changes: 16 additions & 0 deletions src/Cache/WarmupCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace CuyZ\Valinor\Cache;

use Psr\SimpleCache\CacheInterface;

/**
* @internal
*
* @template T
* @extends CacheInterface<T>
*/
interface WarmupCache extends CacheInterface
{
public function warmup(): void;
}
1 change: 1 addition & 0 deletions src/Library/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public function __construct(Settings $settings)

RecursiveCacheWarmupService::class => fn () => new RecursiveCacheWarmupService(
$this->get(TypeParser::class),
$this->get(CacheInterface::class),
$this->get(ObjectImplementations::class),
$this->get(ClassDefinitionRepository::class),
$this->get(ObjectBuilderFactory::class)
Expand Down
65 changes: 65 additions & 0 deletions tests/Fake/Cache/FakeCacheWithWarmup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Fake\Cache;

use CuyZ\Valinor\Cache\WarmupCache;

/**
* @implements WarmupCache<mixed>
*/
final class FakeCacheWithWarmup implements WarmupCache
{
private int $warmupCount = 0;

public function timesWarmupWasCalled(): int
{
return $this->warmupCount;
}

public function warmup(): void
{
$this->warmupCount++;
}

public function get($key, $default = null): mixed
{
return null;
}

public function set($key, $value, $ttl = null): bool
{
return false;
}

public function delete($key): bool
{
return false;
}

public function clear(): bool
{
return false;
}

public function getMultiple($keys, $default = null): iterable
{
return [];
}

public function setMultiple($values, $ttl = null): bool
{
return false;
}

public function deleteMultiple($keys): bool
{
return false;
}

public function has($key): bool
{
return false;
}
}
21 changes: 21 additions & 0 deletions tests/Integration/Cache/CacheWarmupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use CuyZ\Valinor\Cache\Exception\InvalidSignatureToWarmup;
use CuyZ\Valinor\MapperBuilder;
use CuyZ\Valinor\Tests\Fake\Cache\FakeCache;
use CuyZ\Valinor\Tests\Fake\Cache\FakeCacheWithWarmup;
use CuyZ\Valinor\Tests\Integration\IntegrationTest;
use DateTimeInterface;

Expand All @@ -24,6 +25,26 @@ protected function setUp(): void
$this->mapper = (new MapperBuilder())->withCache($this->cache);
}

public function test_cache_warmup_is_called_only_once(): void
{
$cache = new FakeCacheWithWarmup();
$mapper = (new MapperBuilder())->withCache($cache);

$mapper->warmup();
$mapper->warmup();

self::assertSame(1, $cache->timesWarmupWasCalled());
}

/**
* @doesNotPerformAssertions
*/
public function test_cache_warmup_does_not_call_delegate_warmup_if_not_handled(): void
{
$mapper = new MapperBuilder(); // no cache registered
$mapper->warmup();
}

public function test_will_warmup_type_parser_cache_for_object_with_properties(): void
{
$this->mapper->warmup(ObjectToWarmupWithProperties::class);
Expand Down
9 changes: 9 additions & 0 deletions tests/Unit/Cache/Compiled/CompiledPhpFileCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ protected function setUp(): void
$this->cache = new CompiledPhpFileCache(vfsStream::url('cache-dir'), new FakeCacheCompiler());
}

public function test_warmup_creates_temporary_dir(): void
{
self::assertFalse($this->files->hasChild('.valinor.tmp'));

$this->cache->warmup();

self::assertTrue($this->files->hasChild('.valinor.tmp'));
}

public function test_set_cache_sets_cache(): void
{
self::assertFalse($this->cache->has('foo'));
Expand Down
Loading

0 comments on commit 8f6faba

Please sign in to comment.