Skip to content

Commit fbd3ee2

Browse files
authored
Preload results in in-memory cache; Rename Cache -> FileCache (#8)
* Implement in-memory cache; Rename `Cache` -> `FileCache` * Warm cache in batch * 🤖 `pint` * Make `FileCache` a `readonly` class * Test `CacheProxy` * Test `ArrayCache` * Test `ArrayCache`, `CacheProxy` & `FileCache` implements `CacheInterface` * Fix typo
1 parent c87e31c commit fbd3ee2

File tree

11 files changed

+311
-21
lines changed

11 files changed

+311
-21
lines changed

src/MarkClosedPluginAsAbandoned.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use Composer\Composer;
99
use Composer\IO\IOInterface;
1010
use Composer\Package\CompletePackageInterface;
11-
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\Cache as ApiCache;
11+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\ArrayCache;
12+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\CacheProxy;
1213
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\Client;
14+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\FileCache;
1315
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\DownloadUrlParser;
1416
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\MultiUrlParser;
1517
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\SvnUrlParser;
@@ -23,8 +25,12 @@ public static function create(Composer $composer, IOInterface $io): self
2325

2426
$config = $composer->getConfig();
2527
$cachePath = "{$config->get('cache-dir')}/wp-org-closed-plugin";
26-
$cache = new Cache($io, $cachePath);
27-
$cache->setReadOnly($config->get('cache-read-only'));
28+
$composerCache = new Cache($io, $cachePath);
29+
$composerCache->setReadOnly($config->get('cache-read-only'));
30+
$cache = new CacheProxy(
31+
new ArrayCache,
32+
new FileCache($composerCache),
33+
);
2834

2935
return new self(
3036
new MultiUrlParser(
@@ -34,7 +40,7 @@ public static function create(Composer $composer, IOInterface $io): self
3440
new Client(
3541
$loop->getHttpDownloader(),
3642
$loop,
37-
new ApiCache($cache),
43+
$cache,
3844
),
3945
$io,
4046
);
@@ -46,6 +52,28 @@ public function __construct(
4652
private IOInterface $io,
4753
) {}
4854

55+
public function warmCache(CompletePackageInterface ...$packages): void
56+
{
57+
$slugs = array_map(
58+
fn (CompletePackageInterface $package): ?string => $this->slug(
59+
...$package->getDistUrls(),
60+
...$package->getSourceUrls(),
61+
),
62+
$packages,
63+
);
64+
$slugs = array_filter($slugs, static fn (?string $slug) => $slug !== null);
65+
66+
$slugCount = count($slugs);
67+
$message = sprintf(
68+
'Warming WordPress.org plugin status cache for %d %s',
69+
$slugCount,
70+
$slugCount === 1 ? 'slug' : 'slugs',
71+
);
72+
$this->io->debug($message);
73+
74+
$this->client->warmCache(...$slugs);
75+
}
76+
4977
public function __invoke(CompletePackageInterface $package): void
5078
{
5179
$this->io->debug(

src/Plugin.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,14 @@ public function uninstall(Composer $composer, IOInterface $io): void
4848
public static function getSubscribedEvents(): array
4949
{
5050
return [
51-
PackageEvents::PRE_PACKAGE_INSTALL => ['markClosedAsAbandoned', PHP_INT_MAX - 1000],
52-
PackageEvents::PRE_PACKAGE_UPDATE => ['markClosedAsAbandoned', PHP_INT_MAX - 1000],
51+
PackageEvents::PRE_PACKAGE_INSTALL => [
52+
['warmCache', PHP_INT_MAX - 500],
53+
['markClosedAsAbandoned', PHP_INT_MAX - 1000],
54+
],
55+
PackageEvents::PRE_PACKAGE_UPDATE => [
56+
['warmCache', PHP_INT_MAX - 500],
57+
['markClosedAsAbandoned', PHP_INT_MAX - 1000],
58+
],
5359

5460
ScriptEvents::POST_INSTALL_CMD => ['markClosedLockedPackagesIfNotAlready', PHP_INT_MAX - 1000],
5561
ScriptEvents::POST_UPDATE_CMD => ['markClosedLockedPackagesIfNotAlready', PHP_INT_MAX - 1000],
@@ -58,6 +64,22 @@ public static function getSubscribedEvents(): array
5864
];
5965
}
6066

67+
public function warmCache(PackageEvent $event): void
68+
{
69+
$packages = array_map(
70+
static fn ($operation) => match (true) {
71+
$operation instanceof InstallOperation => $operation->getPackage(),
72+
$operation instanceof UpdateOperation => $operation->getTargetPackage(),
73+
default => null,
74+
},
75+
$event->getOperations(),
76+
);
77+
$packages = array_filter($packages, static fn ($package) => $package instanceof CompletePackageInterface);
78+
$packages = array_filter($packages, static fn ($package) => ! $package->isAbandoned());
79+
80+
$this->markClosedAsAbandoned->warmCache(...$packages);
81+
}
82+
6183
public function markClosedAsAbandoned(PackageEvent $event): void
6284
{
6385
$operation = $event->getOperation();
@@ -95,6 +117,8 @@ public function markClosedLockedPackagesIfNotAlready(ScriptEvent $event): void
95117
fn ($package) => ! in_array($package->getPrettyName(), $this->marked, true),
96118
);
97119

120+
$this->markClosedAsAbandoned->warmCache(...$packages);
121+
98122
foreach ($packages as $package) {
99123
$this->marked[] = $package->getPrettyName();
100124
$this->markClosedAsAbandoned->__invoke($package);

src/WpOrg/Api/ArrayCache.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\WpOrgClosedPlugin\WpOrg\Api;
6+
7+
class ArrayCache implements CacheInterface
8+
{
9+
/** @var array<string, bool> */
10+
private array $data = [];
11+
12+
public function read(string $slug): ?bool
13+
{
14+
return $this->data[$slug] ?? null;
15+
}
16+
17+
public function write(string $slug, bool $isClosed): void
18+
{
19+
$this->data[$slug] = $isClosed;
20+
}
21+
}

src/WpOrg/Api/CacheInterface.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\WpOrgClosedPlugin\WpOrg\Api;
6+
7+
interface CacheInterface
8+
{
9+
public function read(string $slug): ?bool;
10+
11+
public function write(string $slug, bool $isClosed): void;
12+
}

src/WpOrg/Api/CacheProxy.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypistTech\WpOrgClosedPlugin\WpOrg\Api;
6+
7+
readonly class CacheProxy implements CacheInterface
8+
{
9+
public function __construct(
10+
private CacheInterface $fast,
11+
private CacheInterface $slow,
12+
) {}
13+
14+
public function read(string $slug): ?bool
15+
{
16+
$result = $this->fast->read($slug);
17+
if ($result !== null) {
18+
return $result;
19+
}
20+
21+
$result = $this->slow->read($slug);
22+
if ($result !== null) {
23+
$this->fast->write($slug, $result);
24+
}
25+
26+
return $result;
27+
}
28+
29+
public function write(string $slug, bool $isClosed): void
30+
{
31+
$this->fast->write($slug, $isClosed);
32+
$this->slow->write($slug, $isClosed);
33+
}
34+
}

src/WpOrg/Api/Client.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,22 @@
1515
public function __construct(
1616
private HttpDownloader $httpDownloader,
1717
private Loop $loop,
18-
private Cache $cache,
18+
private CacheInterface $cache,
1919
) {}
2020

21+
public function warmCache(string ...$slugs): void
22+
{
23+
$slugs = array_map('trim', $slugs);
24+
$slugs = array_filter($slugs, static fn (string $slug) => $slug !== '');
25+
26+
$promises = array_map(
27+
fn (string $slug) => $this->isClosedAsync($slug),
28+
$slugs,
29+
);
30+
31+
$this->loop->wait($promises);
32+
}
33+
2134
public function isClosed(string $slug): bool
2235
{
2336
$slug = trim($slug);
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
use Composer\Cache as ComposerCache;
88
use DateTimeInterface;
99

10-
class Cache
10+
readonly class FileCache implements CacheInterface
1111
{
1212
private const int TTL = 600;
1313

1414
public function __construct(
15-
private readonly ComposerCache $cache,
15+
private ComposerCache $cache,
1616
) {}
1717

1818
public function read(string $slug): ?bool

tests/Feature/WpOrg/Api/ClientTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use Composer\Util\HttpDownloader;
88
use Mockery;
9-
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\Cache;
9+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\CacheInterface;
1010
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\Client;
1111

1212
covers(Client::class);
@@ -49,7 +49,7 @@
4949
it('returns true if and only if the plugin is closed', function (string $slug, bool $expected): void {
5050
$loop = $this->loop();
5151
$httpDownloader = $loop->getHttpDownloader();
52-
$cache = Mockery::spy(Cache::class);
52+
$cache = Mockery::spy(CacheInterface::class);
5353

5454
$client = new Client($httpDownloader, $loop, $cache);
5555

@@ -61,7 +61,7 @@
6161
it('writes to cache', function (string $slug, bool $expected): void {
6262
$loop = $this->loop();
6363
$httpDownloader = $loop->getHttpDownloader();
64-
$cache = Mockery::spy(Cache::class);
64+
$cache = Mockery::spy(CacheInterface::class);
6565

6666
$client = new Client($httpDownloader, $loop, $cache);
6767

@@ -75,7 +75,7 @@
7575
$loop = $this->loop();
7676
$httpDownloader = Mockery::spy(HttpDownloader::class);
7777

78-
$cache = Mockery::spy(Cache::class);
78+
$cache = Mockery::spy(CacheInterface::class);
7979
$cache->expects()
8080
->read()
8181
->with($slug)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Unit\WpOrg\Api;
6+
7+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\ArrayCache;
8+
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\CacheInterface;
9+
10+
covers(ArrayCache::class);
11+
12+
describe(ArrayCache::class, static function (): void {
13+
it('implements CacheInterface', function (): void {
14+
$cache = new ArrayCache;
15+
16+
expect($cache)->toBeInstanceOf(CacheInterface::class);
17+
});
18+
19+
describe('::read()', static function (): void {
20+
test('hit', function (bool $expected): void {
21+
$cache = new ArrayCache;
22+
$cache->write('foo', $expected);
23+
24+
$actual = $cache->read('foo');
25+
26+
expect($actual)->toBe($expected);
27+
})->with([true, false]);
28+
29+
test('miss', function (): void {
30+
$cache = new ArrayCache;
31+
32+
$actual = $cache->read('foo');
33+
34+
expect($actual)->toBeNull();
35+
});
36+
});
37+
38+
describe('::write()', static function (): void {
39+
it('stores', function (bool $isClosed): void {
40+
$cache = new ArrayCache;
41+
42+
$cache->write('foo', $isClosed);
43+
44+
expect($cache->read('foo'))->toBe($isClosed);
45+
})->with([true, false]);
46+
47+
test('last write wins', function (bool $first, bool $last): void {
48+
$cache = new ArrayCache;
49+
50+
$cache->write('foo', $first);
51+
$cache->write('foo', $last);
52+
53+
expect($cache->read('foo'))->toBe($last);
54+
})->with([
55+
[true, false],
56+
[false, true],
57+
[true, true],
58+
[false, false],
59+
]);
60+
});
61+
});

0 commit comments

Comments
 (0)