Skip to content

Commit 57cabef

Browse files
Cache verified external payload fetches
1 parent de7d3ba commit 57cabef

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/V2/Support/ExternalPayloadStorage.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010

1111
final class ExternalPayloadStorage
1212
{
13+
private const MAX_CACHE_ENTRIES = 128;
14+
15+
private const MAX_CACHE_BYTES = 16777216;
16+
17+
/**
18+
* @var array<string, array{data: string, bytes: int, used_at: int}>
19+
*/
20+
private static array $verifiedCache = [];
21+
22+
private static int $verifiedCacheBytes = 0;
23+
24+
private static int $verifiedCacheSequence = 0;
25+
1326
public static function store(
1427
ExternalPayloadStorageDriver $driver,
1528
string $data,
@@ -24,6 +37,13 @@ public static function store(
2437

2538
public static function fetch(ExternalPayloadStorageDriver $driver, ExternalPayloadReference $reference): string
2639
{
40+
$cacheKey = self::cacheKey($reference);
41+
if (isset(self::$verifiedCache[$cacheKey])) {
42+
self::$verifiedCache[$cacheKey]['used_at'] = ++self::$verifiedCacheSequence;
43+
44+
return self::$verifiedCache[$cacheKey]['data'];
45+
}
46+
2747
$data = $driver->get($reference->uri);
2848

2949
if (strlen($data) !== $reference->sizeBytes) {
@@ -34,6 +54,73 @@ public static function fetch(ExternalPayloadStorageDriver $driver, ExternalPaylo
3454
throw new ExternalPayloadIntegrityException('External payload hash does not match its reference.');
3555
}
3656

57+
self::rememberVerified($cacheKey, $data);
58+
3759
return $data;
3860
}
61+
62+
public static function flushVerifiedCache(): void
63+
{
64+
self::$verifiedCache = [];
65+
self::$verifiedCacheBytes = 0;
66+
self::$verifiedCacheSequence = 0;
67+
}
68+
69+
private static function cacheKey(ExternalPayloadReference $reference): string
70+
{
71+
return implode("\n", [
72+
$reference->uri,
73+
$reference->sha256,
74+
(string) $reference->sizeBytes,
75+
$reference->codec,
76+
]);
77+
}
78+
79+
private static function rememberVerified(string $cacheKey, string $data): void
80+
{
81+
$bytes = strlen($data);
82+
if ($bytes > self::MAX_CACHE_BYTES) {
83+
return;
84+
}
85+
86+
if (isset(self::$verifiedCache[$cacheKey])) {
87+
self::$verifiedCacheBytes -= self::$verifiedCache[$cacheKey]['bytes'];
88+
}
89+
90+
self::$verifiedCache[$cacheKey] = [
91+
'data' => $data,
92+
'bytes' => $bytes,
93+
'used_at' => ++self::$verifiedCacheSequence,
94+
];
95+
self::$verifiedCacheBytes += $bytes;
96+
97+
self::evictVerifiedCache();
98+
}
99+
100+
private static function evictVerifiedCache(): void
101+
{
102+
while (
103+
count(self::$verifiedCache) > self::MAX_CACHE_ENTRIES
104+
|| self::$verifiedCacheBytes > self::MAX_CACHE_BYTES
105+
) {
106+
$oldestKey = null;
107+
$oldestUsedAt = PHP_INT_MAX;
108+
109+
foreach (self::$verifiedCache as $key => $entry) {
110+
if ($entry['used_at'] < $oldestUsedAt) {
111+
$oldestKey = $key;
112+
$oldestUsedAt = $entry['used_at'];
113+
}
114+
}
115+
116+
if ($oldestKey === null) {
117+
self::$verifiedCacheBytes = 0;
118+
119+
return;
120+
}
121+
122+
self::$verifiedCacheBytes -= self::$verifiedCache[$oldestKey]['bytes'];
123+
unset(self::$verifiedCache[$oldestKey]);
124+
}
125+
}
39126
}

tests/Unit/V2/ExternalPayloadStorageTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use League\Flysystem\Filesystem;
1010
use League\Flysystem\Local\LocalFilesystemAdapter;
1111
use PHPUnit\Framework\TestCase;
12+
use Workflow\V2\Contracts\ExternalPayloadStorageDriver;
1213
use Workflow\V2\Exceptions\ExternalPayloadIntegrityException;
1314
use Workflow\V2\Support\ExternalPayloadReference;
1415
use Workflow\V2\Support\ExternalPayloadStorage;
@@ -21,6 +22,8 @@ final class ExternalPayloadStorageTest extends TestCase
2122

2223
protected function tearDown(): void
2324
{
25+
ExternalPayloadStorage::flushVerifiedCache();
26+
2427
if ($this->storageRoot !== null) {
2528
$this->removeDirectory($this->storageRoot);
2629
$this->storageRoot = null;
@@ -75,6 +78,42 @@ public function testLocalFilesystemDriverStoresFetchesAndDeletesVerifiedBytes():
7578
$driver->get($reference->uri);
7679
}
7780

81+
public function testFetchCachesVerifiedBytesByReference(): void
82+
{
83+
$localDriver = new LocalFilesystemExternalPayloadStorage($this->makeStorageRoot());
84+
$driver = new class($localDriver) implements ExternalPayloadStorageDriver {
85+
public int $getCalls = 0;
86+
87+
public function __construct(
88+
private readonly ExternalPayloadStorageDriver $inner
89+
) {
90+
}
91+
92+
public function put(string $data, string $sha256, string $codec): string
93+
{
94+
return $this->inner->put($data, $sha256, $codec);
95+
}
96+
97+
public function get(string $uri): string
98+
{
99+
$this->getCalls++;
100+
101+
return $this->inner->get($uri);
102+
}
103+
104+
public function delete(string $uri): void
105+
{
106+
$this->inner->delete($uri);
107+
}
108+
};
109+
110+
$reference = ExternalPayloadStorage::store($driver, 'encoded-payload', 'avro');
111+
112+
$this->assertSame('encoded-payload', ExternalPayloadStorage::fetch($driver, $reference));
113+
$this->assertSame('encoded-payload', ExternalPayloadStorage::fetch($driver, $reference));
114+
$this->assertSame(1, $driver->getCalls);
115+
}
116+
78117
public function testFetchRejectsMutatedPayloadBytes(): void
79118
{
80119
$driver = new LocalFilesystemExternalPayloadStorage($this->makeStorageRoot());

0 commit comments

Comments
 (0)