Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 47c2da7

Browse files
committed
feature #46763 [HttpCache] Do not call terminate() on cache hit (Toflar)
This PR was merged into the 6.2 branch. Discussion ---------- [HttpCache] Do not call terminate() on cache hit | Q | A | ------------- | --- | Branch? | 6.2 | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | - | License | MIT | Doc PR | - Currently, `HttpCache` always calls the `kernel.terminate` events, even if the response is coming from cache. Why is this a problem? `kernel.terminate` events are used to do things after the response has been sent to the visitor. At least that happens if you have support for `fastcgi_finish_request()`. For Contao for example, we use this to dispatch a message to update the search index but you can imagine a lot of stuff being done there, like sending e-mails etc. According [to the docs](https://symfony.com/doc/current/components/http_kernel.html#8-the-kernel-terminate-event), it says "perform some heavy action". This means that currently, the system is basically always booted even when the response is coming from `HttpCache` because dispatching the `TerminateEvent` causes the container to be booted etc. which makes the system slower than it needs to be. We don't need to update the search index when the response is coming from the cache because it already is up to date. And there are no e-mails or other "heavy actions" to perform because by definition, nothing could've happened as the system was not booted (or should not have been). Also, imagine if you used a "real" reverse proxy like Varnish. There's no call to the back end either when there's a cache hit so it's actually an inconsistency. You cannot "perform some heavy action" there either. If you wanted to, it would have to be implemented in the proxy itself. So Varnish would need to trigger that heavy action. HttpCache should behave just the same. You could e.g. use the `EventDispatchingHttpCache` from https://github.com/FriendsOfSymfony/FOSHttpCache, if you need something like that. Commits ------- 662eb17 [HttpCache] Do not call terminate() on cache hit
2 parents 190ba4b + 662eb17 commit 47c2da7

File tree

4 files changed

+129
-7
lines changed

4 files changed

+129
-7
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
7878
* the cache can serve a stale response when an error is encountered (default: 60).
7979
* This setting is overridden by the stale-if-error HTTP Cache-Control extension
8080
* (see RFC 5861).
81+
*
82+
* * terminate_on_cache_hit Specifies if the kernel.terminate event should be dispatched even when the cache
83+
* was hit (default: true).
84+
* Unless your application needs to process events on cache hits, it is recommended
85+
* to set this to false to avoid having to bootstrap the Symfony framework on a cache hit.
8186
*/
8287
public function __construct(HttpKernelInterface $kernel, StoreInterface $store, SurrogateInterface $surrogate = null, array $options = [])
8388
{
@@ -98,6 +103,7 @@ public function __construct(HttpKernelInterface $kernel, StoreInterface $store,
98103
'stale_if_error' => 60,
99104
'trace_level' => 'none',
100105
'trace_header' => 'X-Symfony-Cache',
106+
'terminate_on_cache_hit' => true,
101107
], $options);
102108

103109
if (!isset($options['trace_level'])) {
@@ -238,6 +244,15 @@ public function handle(Request $request, int $type = HttpKernelInterface::MAIN_R
238244
*/
239245
public function terminate(Request $request, Response $response)
240246
{
247+
// Do not call any listeners in case of a cache hit.
248+
// This ensures identical behavior as if you had a separate
249+
// reverse caching proxy such as Varnish and the like.
250+
if ($this->options['terminate_on_cache_hit']) {
251+
trigger_deprecation('symfony/http-kernel', '6.2', 'Setting "terminate_on_cache_hit" to "true" is deprecated and will be changed to "false" in Symfony 7.0.');
252+
} elseif (\in_array('fresh', $this->traces[$this->getTraceKey($request)] ?? [], true)) {
253+
return;
254+
}
255+
241256
if ($this->getKernel() instanceof TerminableInterface) {
242257
$this->getKernel()->terminate($request, $response);
243258
}

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

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111

1212
namespace Symfony\Component\HttpKernel\Tests\HttpCache;
1313

14+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
1416
use Symfony\Component\HttpFoundation\Request;
1517
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
1619
use Symfony\Component\HttpKernel\HttpCache\Esi;
1720
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
18-
use Symfony\Component\HttpKernel\HttpCache\Store;
1921
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
2022
use Symfony\Component\HttpKernel\HttpKernelInterface;
2123
use Symfony\Component\HttpKernel\Kernel;
@@ -25,6 +27,8 @@
2527
*/
2628
class HttpCacheTest extends HttpCacheTestCase
2729
{
30+
use ExpectDeprecationTrait;
31+
2832
public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
2933
{
3034
$storeMock = $this->getMockBuilder(StoreInterface::class)
@@ -33,7 +37,7 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
3337

3438
// does not implement TerminableInterface
3539
$kernel = new TestKernel();
36-
$httpCache = new HttpCache($kernel, $storeMock);
40+
$httpCache = new HttpCache($kernel, $storeMock, null, ['terminate_on_cache_hit' => false]);
3741
$httpCache->terminate(Request::create('/'), new Response());
3842

3943
$this->assertFalse($kernel->terminateCalled, 'terminate() is never called if the kernel class does not implement TerminableInterface');
@@ -47,10 +51,108 @@ public function testTerminateDelegatesTerminationOnlyForTerminableInterface()
4751
$kernelMock->expects($this->once())
4852
->method('terminate');
4953

50-
$kernel = new HttpCache($kernelMock, $storeMock);
54+
$kernel = new HttpCache($kernelMock, $storeMock, null, ['terminate_on_cache_hit' => false]);
5155
$kernel->terminate(Request::create('/'), new Response());
5256
}
5357

58+
public function testDoesNotCallTerminateOnFreshResponse()
59+
{
60+
$terminateEvents = [];
61+
62+
$eventDispatcher = $this->createMock(EventDispatcher::class);
63+
$eventDispatcher
64+
->expects($this->any())
65+
->method('dispatch')
66+
->with($this->callback(function ($event) use (&$terminateEvents) {
67+
if ($event instanceof TerminateEvent) {
68+
$terminateEvents[] = $event;
69+
}
70+
71+
return true;
72+
}));
73+
74+
$this->setNextResponse(
75+
200,
76+
[
77+
'ETag' => '1234',
78+
'Cache-Control' => 'public, s-maxage=60',
79+
],
80+
'Hello World',
81+
null,
82+
$eventDispatcher
83+
);
84+
85+
$this->request('GET', '/');
86+
$this->assertHttpKernelIsCalled();
87+
$this->assertEquals(200, $this->response->getStatusCode());
88+
$this->assertTraceContains('miss');
89+
$this->assertTraceContains('store');
90+
$this->cache->terminate($this->request, $this->response);
91+
92+
sleep(2);
93+
94+
$this->request('GET', '/');
95+
$this->assertHttpKernelIsNotCalled();
96+
$this->assertEquals(200, $this->response->getStatusCode());
97+
$this->assertTraceContains('fresh');
98+
$this->assertEquals(2, $this->response->headers->get('Age'));
99+
$this->cache->terminate($this->request, $this->response);
100+
101+
$this->assertCount(1, $terminateEvents);
102+
}
103+
104+
/**
105+
* @group legacy
106+
*/
107+
public function testDoesCallTerminateOnFreshResponseIfConfigured()
108+
{
109+
$this->expectDeprecation('Since symfony/http-kernel 6.2: Setting "terminate_on_cache_hit" to "true" is deprecated and will be changed to "false" in Symfony 7.0.');
110+
111+
$terminateEvents = [];
112+
113+
$eventDispatcher = $this->createMock(EventDispatcher::class);
114+
$eventDispatcher
115+
->expects($this->any())
116+
->method('dispatch')
117+
->with($this->callback(function ($event) use (&$terminateEvents) {
118+
if ($event instanceof TerminateEvent) {
119+
$terminateEvents[] = $event;
120+
}
121+
122+
return true;
123+
}));
124+
125+
$this->setNextResponse(
126+
200,
127+
[
128+
'ETag' => '1234',
129+
'Cache-Control' => 'public, s-maxage=60',
130+
],
131+
'Hello World',
132+
null,
133+
$eventDispatcher
134+
);
135+
$this->cacheConfig['terminate_on_cache_hit'] = true;
136+
137+
$this->request('GET', '/');
138+
$this->assertHttpKernelIsCalled();
139+
$this->assertEquals(200, $this->response->getStatusCode());
140+
$this->assertTraceContains('miss');
141+
$this->assertTraceContains('store');
142+
$this->cache->terminate($this->request, $this->response);
143+
144+
sleep(2);
145+
146+
$this->request('GET', '/');
147+
$this->assertHttpKernelIsNotCalled();
148+
$this->assertEquals(200, $this->response->getStatusCode());
149+
$this->assertTraceContains('fresh');
150+
$this->assertEquals(2, $this->response->headers->get('Age'));
151+
$this->cache->terminate($this->request, $this->response);
152+
153+
$this->assertCount(2, $terminateEvents);
154+
}
155+
54156
public function testPassesOnNonGetHeadRequests()
55157
{
56158
$this->setNextResponse(200);

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpKernel\Tests\HttpCache;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\EventDispatcher\EventDispatcher;
1516
use Symfony\Component\HttpFoundation\Request;
1617
use Symfony\Component\HttpKernel\HttpCache\Esi;
1718
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
@@ -124,6 +125,10 @@ public function request($method, $uri = '/', $server = [], $cookies = [], $esi =
124125
$this->cacheConfig['debug'] = true;
125126
}
126127

128+
if (!isset($this->cacheConfig['terminate_on_cache_hit'])) {
129+
$this->cacheConfig['terminate_on_cache_hit'] = false;
130+
}
131+
127132
$this->esi = $esi ? new Esi() : null;
128133
$this->cache = new HttpCache($this->kernel, $this->store, $this->esi, $this->cacheConfig);
129134
$this->request = Request::create($uri, $method, [], $cookies, [], $server);
@@ -145,9 +150,9 @@ public function getMetaStorageValues()
145150
}
146151

147152
// A basic response with 200 status code and a tiny body.
148-
public function setNextResponse($statusCode = 200, array $headers = [], $body = 'Hello World', \Closure $customizer = null)
153+
public function setNextResponse($statusCode = 200, array $headers = [], $body = 'Hello World', \Closure $customizer = null, EventDispatcher $eventDispatcher = null)
149154
{
150-
$this->kernel = new TestHttpKernel($body, $statusCode, $headers, $customizer);
155+
$this->kernel = new TestHttpKernel($body, $statusCode, $headers, $customizer, $eventDispatcher);
151156
}
152157

153158
public function setNextResponses($responses)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ class TestHttpKernel extends HttpKernel implements ControllerResolverInterface,
2929
protected $catch = false;
3030
protected $backendRequest;
3131

32-
public function __construct($body, $status, $headers, \Closure $customizer = null)
32+
public function __construct($body, $status, $headers, \Closure $customizer = null, EventDispatcher $eventDispatcher = null)
3333
{
3434
$this->body = $body;
3535
$this->status = $status;
3636
$this->headers = $headers;
3737
$this->customizer = $customizer;
3838

39-
parent::__construct(new EventDispatcher(), $this, null, $this, true);
39+
parent::__construct($eventDispatcher ?? new EventDispatcher(), $this, null, $this, true);
4040
}
4141

4242
public function assert(\Closure $callback)

0 commit comments

Comments
 (0)