Skip to content

Commit 110274d

Browse files
committed
[HttpCache] Hit the backend only once after waiting for the cache lock
1 parent 6013f04 commit 110274d

File tree

4 files changed

+82
-12
lines changed

4 files changed

+82
-12
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\HttpKernel\HttpCache;
4+
5+
class CacheWasLockedException extends \Exception
6+
{
7+
}

src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,11 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
219219
$this->record($request, 'reload');
220220
$response = $this->fetch($request, $catch);
221221
} else {
222-
$response = $this->lookup($request, $catch);
222+
try {
223+
$response = $this->lookup($request, $catch);
224+
} catch (CacheWasLockedException) {
225+
$response = $this->lookup($request, $catch);
226+
}
223227
}
224228

225229
$this->restoreResponseBody($request, $response);
@@ -576,15 +580,7 @@ protected function lock(Request $request, Response $entry): bool
576580

577581
// wait for the lock to be released
578582
if ($this->waitForLock($request)) {
579-
// replace the current entry with the fresh one
580-
$new = $this->lookup($request);
581-
$entry->headers = $new->headers;
582-
$entry->setContent($new->getContent());
583-
$entry->setStatusCode($new->getStatusCode());
584-
$entry->setProtocolVersion($new->getProtocolVersion());
585-
foreach ($new->headers->getCookies() as $cookie) {
586-
$entry->headers->setCookie($cookie);
587-
}
583+
throw new CacheWasLockedException(); // unwind back to handle(), try again
588584
} else {
589585
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
590586
$entry->setStatusCode(503);

src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Symfony\Component\HttpKernel\Event\TerminateEvent;
1919
use Symfony\Component\HttpKernel\HttpCache\Esi;
2020
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
21+
use Symfony\Component\HttpKernel\HttpCache\Store;
2122
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
2223
use Symfony\Component\HttpKernel\HttpKernelInterface;
2324
use Symfony\Component\HttpKernel\Kernel;
@@ -717,6 +718,7 @@ public function testDegradationWhenCacheLocked()
717718
*/
718719
sleep(10);
719720

721+
$this->store = $this->createStore(); // create another store instance that does not hold the current lock
720722
$this->request('GET', '/');
721723
$this->assertHttpKernelIsNotCalled();
722724
$this->assertEquals(200, $this->response->getStatusCode());
@@ -735,6 +737,64 @@ public function testDegradationWhenCacheLocked()
735737
$this->assertEquals('Old response', $this->response->getContent());
736738
}
737739

740+
public function testHitBackendOnlyOnceWhenCacheWasLocked()
741+
{
742+
// Disable stale-while-revalidate, it circumvents waiting for the lock
743+
$this->cacheConfig['stale_while_revalidate'] = 0;
744+
745+
$this->setNextResponses([
746+
[
747+
'status' => 200,
748+
'body' => 'initial response',
749+
'headers' => [
750+
'Cache-Control' => 'public, no-cache',
751+
'Last-Modified' => 'some while ago',
752+
],
753+
],
754+
[
755+
'status' => 304,
756+
'body' => '',
757+
'headers' => [
758+
'Cache-Control' => 'public, no-cache',
759+
'Last-Modified' => 'some while ago',
760+
],
761+
],
762+
[
763+
'status' => 500,
764+
'body' => 'The backend should not be called twice during revalidation',
765+
'headers' => [],
766+
],
767+
]);
768+
769+
$this->request('GET', '/'); // warm the cache
770+
771+
// Use a store that simulates a cache entry being locked upon first attempt
772+
$this->store = new class(sys_get_temp_dir() . '/http_cache') extends Store {
773+
private bool $hasLock = false;
774+
775+
public function lock(Request $request): bool
776+
{
777+
$hasLock = $this->hasLock;
778+
$this->hasLock = true;
779+
780+
return $hasLock;
781+
}
782+
783+
public function isLocked(Request $request): bool
784+
{
785+
return false;
786+
}
787+
};
788+
789+
$this->request('GET', '/'); // hit the cache with simulated lock/concurrency block
790+
791+
$this->assertEquals(200, $this->response->getStatusCode());
792+
$this->assertEquals('initial response', $this->response->getContent());
793+
794+
$traces = $this->cache->getTraces();
795+
$this->assertSame(['stale', 'valid', 'store'], current($traces));
796+
}
797+
738798
public function testHitsCachedResponseWithSMaxAgeDirective()
739799
{
740800
$time = \DateTimeImmutable::createFromFormat('U', time() - 5);

src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ abstract class HttpCacheTestCase extends TestCase
3030
protected $responses;
3131
protected $catch;
3232
protected $esi;
33-
protected Store $store;
33+
protected ?Store $store = null;
3434

3535
protected function setUp(): void
3636
{
@@ -115,7 +115,9 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi =
115115

116116
$this->kernel->reset();
117117

118-
$this->store = new Store(sys_get_temp_dir().'/http_cache');
118+
if (! $this->store) {
119+
$this->store = $this->createStore();
120+
}
119121

120122
if (!isset($this->cacheConfig['debug'])) {
121123
$this->cacheConfig['debug'] = true;
@@ -183,4 +185,9 @@ public static function clearDirectory($directory)
183185

184186
closedir($fp);
185187
}
188+
189+
protected function createStore(): Store
190+
{
191+
return new Store(sys_get_temp_dir() . '/http_cache');
192+
}
186193
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy