From d880638d52f2d030c109828ceddfd8a9e63fd36e Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 17 Mar 2017 16:33:17 +0100 Subject: [PATCH 1/2] Improve stale-while-revalidate code in HttpCache, add a (first?!?) test for it --- .../HttpKernel/HttpCache/HttpCache.php | 100 +++++++++++------- .../Tests/HttpCache/HttpCacheTest.php | 36 +++++++ .../Tests/HttpCache/HttpCacheTestCase.php | 4 + 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 941d4c6fa033d..0f0389e7aa4a0 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -549,49 +549,38 @@ protected function lock(Request $request, Response $entry) // try to acquire a lock to call the backend $lock = $this->store->lock($request); - // there is already another process calling the backend - if (true !== $lock) { - // check if we can serve the stale entry - if (null === $age = $entry->headers->getCacheControlDirective('stale-while-revalidate')) { - $age = $this->options['stale_while_revalidate']; - } - - if (abs($entry->getTtl()) < $age) { - $this->record($request, 'stale-while-revalidate'); + if (true === $lock) { + // we have the lock, call the backend + return false; + } - // server the stale response while there is a revalidation - return true; - } + // there is already another process calling the backend - // wait for the lock to be released - $wait = 0; - while ($this->store->isLocked($request) && $wait < 5000000) { - usleep(50000); - $wait += 50000; - } + // May we serve a stale response? + if ($this->mayServeStaleWhileRevalidate($entry)) { + $this->record($request, 'stale-while-revalidate'); + return true; + } - if ($wait < 5000000) { - // replace the current entry with the fresh one - $new = $this->lookup($request); - $entry->headers = $new->headers; - $entry->setContent($new->getContent()); - $entry->setStatusCode($new->getStatusCode()); - $entry->setProtocolVersion($new->getProtocolVersion()); - foreach ($new->headers->getCookies() as $cookie) { - $entry->headers->setCookie($cookie); - } - } else { - // backend is slow as hell, send a 503 response (to avoid the dog pile effect) - $entry->setStatusCode(503); - $entry->setContent('503 Service Unavailable'); - $entry->headers->set('Retry-After', 10); + // wait for the lock to be released + if ($this->waitForLock($request)) { + // replace the current entry with the fresh one + $new = $this->lookup($request); + $entry->headers = $new->headers; + $entry->setContent($new->getContent()); + $entry->setStatusCode($new->getStatusCode()); + $entry->setProtocolVersion($new->getProtocolVersion()); + foreach ($new->headers->getCookies() as $cookie) { + $entry->headers->setCookie($cookie); } - - return true; + } else { + // backend is slow as hell, send a 503 response (to avoid the dog pile effect) + $entry->setStatusCode(503); + $entry->setContent('503 Service Unavailable'); + $entry->headers->set('Retry-After', 10); } - // we have the lock, call the backend - return false; + return true; } /** @@ -710,4 +699,41 @@ private function record(Request $request, $event) } $this->traces[$request->getMethod().' '.$path][] = $event; } + + /** + * Checks whether the given (cached) response may be served as "stale" when a revalidation + * is currently in progress. + * + * @param Response $entry + * + * @return bool True when the stale response may be served, false otherwise. + */ + private function mayServeStaleWhileRevalidate(Response $entry) + { + $timeout = $entry->headers->getCacheControlDirective('stale-while-revalidate'); + + if ($timeout === null) { + $timeout = $this->options['stale_while_revalidate']; + } + + return abs($entry->getTtl()) < $timeout; + } + + /** + * Waits for the store to release a locked entry. + * + * @param Request $request The request to wait for + * + * @return bool True if the lock was released before the internal timeout was hit; false if the wait timeout was exceeded. + */ + private function waitForLock(Request $request) + { + $wait = 0; + while ($this->store->isLocked($request) && $wait < 5000000) { + usleep(50000); + $wait += 50000; + } + + return $wait < 5000000; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php index 7751f6e0ccfdc..c300a3afa6614 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php @@ -562,6 +562,42 @@ public function testHitsCachedResponseWithMaxAgeDirective() $this->assertEquals('Hello World', $this->response->getContent()); } + public function testDegradationWhenCacheLocked() + { + $this->cacheConfig['stale_while_revalidate'] = 10; + + // The prescence of Last-Modified makes this cacheable (because Response::isValidateable() then). + $this->setNextResponse(200, array('Cache-Control' => 'public, s-maxage=5', 'Last-Modified' => 'some while ago'), 'Old response'); + $this->request('GET', '/'); // warm the cache + + // Now, lock the cache + $concurrentRequest = Request::create('/', 'GET'); + $this->store->lock($concurrentRequest); + + /* + * After 10s, the cached response has become stale. Yet, we're still within the "stale_while_revalidate" + * timeout so we may serve the stale response. + */ + sleep(10); + + $this->request('GET', '/'); + $this->assertHttpKernelIsNotCalled(); + $this->assertEquals(200, $this->response->getStatusCode()); + $this->assertTraceContains('stale-while-revalidate'); + $this->assertEquals('Old response', $this->response->getContent()); + + /* + * Another 10s later, stale_while_revalidate is over. Resort to serving the old response, but + * do so with a "server unavailable" message. + */ + sleep(10); + + $this->request('GET', '/'); + $this->assertHttpKernelIsNotCalled(); + $this->assertEquals(503, $this->response->getStatusCode()); + $this->assertEquals('Old response', $this->response->getContent()); + } + public function testHitsCachedResponseWithSMaxAgeDirective() { $time = \DateTime::createFromFormat('U', time() - 5); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php index 96a66771a06bc..ed5c690d60bcb 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTestCase.php @@ -29,6 +29,10 @@ class HttpCacheTestCase extends TestCase protected $responses; protected $catch; protected $esi; + + /** + * @var Store + */ protected $store; protected function setUp() From 24f605affadb836f86a5b5797f988569f0b455a2 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 17 Mar 2017 17:12:44 +0100 Subject: [PATCH 2/2] fabbot.io --- src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php index 0f0389e7aa4a0..ef1068d568340 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php @@ -559,6 +559,7 @@ protected function lock(Request $request, Response $entry) // May we serve a stale response? if ($this->mayServeStaleWhileRevalidate($entry)) { $this->record($request, 'stale-while-revalidate'); + return true; }