Skip to content

Commit df064b0

Browse files
mpdudenicolas-grekas
authored andcommitted
[HttpCache] Hit the backend only once after waiting for the cache lock
1 parent 383eede commit df064b0

File tree

4 files changed

+96
-12
lines changed

4 files changed

+96
-12
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\HttpCache;
13+
14+
/**
15+
* @internal
16+
*/
17+
class CacheWasLockedException extends \Exception
18+
{
19+
}

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,13 @@ 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+
$response = null;
223+
do {
224+
try {
225+
$response = $this->lookup($request, $catch);
226+
} catch (CacheWasLockedException) {
227+
}
228+
} while (null === $response);
223229
}
224230

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

577583
// wait for the lock to be released
578584
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-
}
585+
throw new CacheWasLockedException(); // unwind back to handle(), try again
588586
} else {
589587
// backend is slow as hell, send a 503 response (to avoid the dog pile effect)
590588
$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