Skip to content

Commit 0eb8d4d

Browse files
committed
chunks as linked list
1 parent 06b0d10 commit 0eb8d4d

File tree

2 files changed

+56
-37
lines changed

2 files changed

+56
-37
lines changed

src/Symfony/Component/HttpClient/CachingHttpClient.php

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ public function request(string $method, string $url, array $options = []): Respo
174174
$freshness = $this->evaluateCacheFreshness($cachedData);
175175

176176
if (Freshness::Fresh === $freshness) {
177-
return $this->createResponseFromCache($metadataKey, $cachedData, $method, $url, $options, $fullUrlTag);
177+
return $this->createResponseFromCache($cachedData, $method, $url, $options, $fullUrlTag);
178178
}
179179

180180
if (isset($cachedData['headers']['etag'])) {
@@ -206,15 +206,14 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
206206
$method,
207207
$options,
208208
): \Generator {
209-
static $chunkIndex = -1;
210-
static $varyFields;
209+
static $chunkKey = null;
211210

212211
if (null !== $chunk->getError() || $chunk->isTimeout()) {
213212
if (Freshness::StaleButUsable === $freshness) {
214213
// avoid throwing exception in ErrorChunk#__destruct()
215214
$chunk instanceof ErrorChunk && $chunk->didThrow(true);
216215
$context->passthru();
217-
$context->replaceResponse($this->createResponseFromCache($metadataKey, $cachedData, $method, $url, $options, $fullUrlTag));
216+
$context->replaceResponse($this->createResponseFromCache($cachedData, $method, $url, $options, $fullUrlTag));
218217

219218
return;
220219
}
@@ -234,10 +233,10 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
234233
}
235234

236235
$headers = $context->getHeaders();
237-
$cacheControl = self::parseCacheControlHeader($headers['cache-control'] ?? []);
238236

239237
if ($chunk->isFirst()) {
240238
$statusCode = $context->getStatusCode();
239+
$cacheControl = self::parseCacheControlHeader($headers['cache-control'] ?? []);
241240

242241
if (304 === $statusCode && null !== $freshness) {
243242
$maxAge = $this->determineMaxAge($headers, $cacheControl);
@@ -254,15 +253,15 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
254253
}, \INF);
255254

256255
$context->passthru();
257-
$context->replaceResponse($this->createResponseFromCache($metadataKey, $cachedData, $method, $url, $options, $fullUrlTag));
256+
$context->replaceResponse($this->createResponseFromCache($cachedData, $method, $url, $options, $fullUrlTag));
258257

259258
return;
260259
}
261260

262261
if ($statusCode >= 500 && $statusCode < 600) {
263262
if (Freshness::StaleButUsable === $freshness) {
264263
$context->passthru();
265-
$context->replaceResponse($this->createResponseFromCache($metadataKey, $cachedData, $method, $url, $options, $fullUrlTag));
264+
$context->replaceResponse($this->createResponseFromCache($cachedData, $method, $url, $options, $fullUrlTag));
266265

267266
return;
268267
}
@@ -275,6 +274,14 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
275274
}
276275
}
277276

277+
if (!$this->isServerResponseCacheable($statusCode, $options['normalized_headers'], $headers, $cacheControl)) {
278+
$context->passthru();
279+
280+
yield $chunk;
281+
282+
return;
283+
}
284+
278285
// recomputing vary fields in case it changed or for first request
279286
$varyFields = [];
280287
foreach ($headers['vary'] ?? [] as $vary) {
@@ -295,31 +302,17 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
295302
return;
296303
}
297304

298-
$metadataKey = self::getMetadataKey($requestHash, $options['normalized_headers'], $varyFields);
299-
300-
yield $chunk;
301-
302-
return;
303-
}
304-
305-
if (!$this->isServerResponseCacheable($context->getStatusCode(), $options['normalized_headers'], $headers, $cacheControl)) {
306-
$context->passthru();
307-
308-
yield $chunk;
309-
310-
return;
311-
}
312-
313-
if ($chunk->isLast()) {
314305
$this->cache->get($varyKey, static function (ItemInterface $item) use ($varyFields, $expiresAt, $fullUrlTag): array {
315306
$item->tag($fullUrlTag)->expiresAt($expiresAt);
316307

317308
return $varyFields;
318309
}, \INF);
319310

311+
$metadataKey = self::getMetadataKey($requestHash, $options['normalized_headers'], $varyFields);
320312
$maxAge = $this->determineMaxAge($headers, $cacheControl);
313+
$chunkKey = "{$metadataKey}_chunk_".bin2hex(random_bytes(8));
321314

322-
$this->cache->get($metadataKey, static function (ItemInterface $item) use ($context, $headers, $maxAge, $expiresAt, $fullUrlTag, $chunkIndex): array {
315+
$this->cache->get($metadataKey, static function (ItemInterface $item) use ($context, $headers, $maxAge, $expiresAt, $fullUrlTag, $chunkKey): array {
323316
$item->tag($fullUrlTag)->expiresAt($expiresAt);
324317

325318
return [
@@ -328,7 +321,7 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
328321
'initial_age' => (int) ($headers['age'][0] ?? 0),
329322
'stored_at' => time(),
330323
'expires_at' => self::calculateExpiresAt($maxAge),
331-
'chunks_count' => $chunkIndex,
324+
'first_chunk' => $chunkKey,
332325
];
333326
}, \INF);
334327

@@ -337,13 +330,31 @@ function (ChunkInterface $chunk, AsyncContext $context) use (
337330
return;
338331
}
339332

340-
++$chunkIndex;
341-
$chunkKey = "{$metadataKey}_chunk_{$chunkIndex}";
342-
$this->cache->get($chunkKey, static function (ItemInterface $item) use ($expiresAt, $fullUrlTag, $chunk): string {
333+
if ($chunk->isLast()) {
334+
$this->cache->get($chunkKey, static function (ItemInterface $item) use ($expiresAt, $fullUrlTag, $chunk): array {
335+
$item->tag($fullUrlTag)->expiresAt($expiresAt);
336+
337+
return [
338+
'content' => $chunk->getContent(),
339+
'next_chunk' => null,
340+
];
341+
}, \INF);
342+
343+
yield $chunk;
344+
345+
return;
346+
}
347+
348+
$nextChunkKey = "{$metadataKey}_chunk_".bin2hex(random_bytes(8));
349+
$this->cache->get($chunkKey, static function (ItemInterface $item) use ($expiresAt, $fullUrlTag, $chunk, $nextChunkKey): array {
343350
$item->tag($fullUrlTag)->expiresAt($expiresAt);
344351

345-
return $chunk->getContent();
352+
return [
353+
'content' => $chunk->getContent(),
354+
'next_chunk' => $nextChunkKey,
355+
];
346356
}, \INF);
357+
$chunkKey = $nextChunkKey;
347358

348359
yield $chunk;
349360
}
@@ -689,19 +700,27 @@ private function hasExplicitExpiration(array $headers, array $cacheControl): boo
689700
* response headers and content. The constructed MockResponse is then
690701
* returned.
691702
*
692-
* @param array{chunks_count: int, status_code: int, initial_age: int, headers: array<string, string|string[]>, stored_at: int} $cachedData
703+
* @param array{first_chunk: string, status_code: int, initial_age: int, headers: array<string, string|string[]>, stored_at: int} $cachedData
693704
*/
694-
private function createResponseFromCache(string $key, array $cachedData, string $method, string $url, array $options, string $fullUrlTag): MockResponse
705+
private function createResponseFromCache(array $cachedData, string $method, string $url, array $options, string $fullUrlTag): MockResponse
695706
{
696707
$cache = $this->cache;
697708
$callback = static function (ItemInterface $item) use ($cache, $fullUrlTag): never {
698709
$cache->invalidateTags([$fullUrlTag]);
699710

700711
throw new ChunkCacheItemNotFoundException(\sprintf('Missing cache item for chunk with key "%s". This indicates an internal cache inconsistency.', $item->getKey()));
701712
};
702-
$body = static function () use ($cache, $key, $cachedData, $callback): \Generator {
703-
for ($i = 0; $i <= $cachedData['chunks_count']; ++$i) {
704-
yield $cache->get("{$key}_chunk_{$i}", $callback, 0);
713+
$body = static function () use ($cache, $cachedData, $callback): \Generator {
714+
$chunk = $cache->get($cachedData['first_chunk'], $callback, 0);
715+
716+
yield $chunk['content'];
717+
718+
while (null !== $chunk['next_chunk']) {
719+
$chunk = $cache->get($chunk['next_chunk'], $callback, 0);
720+
721+
if ('' !== $chunk['content']) {
722+
yield $chunk['content'];
723+
}
705724
}
706725
};
707726

src/Symfony/Component/HttpClient/Tests/CachingHttpClientTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ public function testItCanStreamBoth()
631631
public function testMultipleChunksResponse()
632632
{
633633
$mockClient = new MockHttpClient([
634-
new MockResponse(['chunk1', 'chunk2'], ['http_code' => 200, 'response_headers' => ['Cache-Control' => 'max-age=5']]),
634+
new MockResponse(['chunk1', 'chunk2', 'chunk3'], ['http_code' => 200, 'response_headers' => ['Cache-Control' => 'max-age=5']]),
635635
]);
636636

637637
$client = new CachingHttpClient($mockClient, $this->cacheAdapter);
@@ -641,14 +641,14 @@ public function testMultipleChunksResponse()
641641
foreach ($client->stream($response) as $chunk) {
642642
$content .= $chunk->getContent();
643643
}
644-
self::assertSame('chunk1chunk2', $content);
644+
self::assertSame('chunk1chunk2chunk3', $content);
645645

646646
$response = $client->request('GET', 'http://example.com/multi-chunk');
647647
$content = '';
648648
foreach ($client->stream($response) as $chunk) {
649649
$content .= $chunk->getContent();
650650
}
651-
self::assertSame('chunk1chunk2', $content);
651+
self::assertSame('chunk1chunk2chunk3', $content);
652652
}
653653

654654
public function testConditionalCacheableStatusCodeWithoutExpiration()

0 commit comments

Comments
 (0)