-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Shutdown kernel for surrogates and handle as master request #34688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
becbd48
to
2db24b4
Compare
2db24b4
to
8d52622
Compare
i was wondering why this is like it currently is, but never asked... indeed it would seem more consistent and robust to me to create a new kernel. in terms of practical issues, what does this solve? it forces people to be clean when using the build-in ESI support and will make switching to Varnish actually work without nasty surprises. additionally, some of the session handling can be simplified. however, for the nasty surprise reason, i feel that this should be considered a BC break as the behaviour will change in a non-obvious way when you upgrade. as the method is protected, we could also start recommending to adjust the kernel accordingly. or even only adjust it in debug mode and during test runs to detect problems, but keep the shared kernel in prod mode for better performance? |
// Shut down the kernel for every forwarded request so services etc. are fresh | ||
$kernel = $this->getKernel(); | ||
if ($kernel instanceof KernelInterface) { | ||
$kernel->shutdown(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this makes sense: Kernel::handle()
calls Kernel::boot()
, which then calls $this->container->get('services_resetter')->reset()
, which is for that: resetting the container to a state that allows one request to not leak to the next one.
If there still is a leak, that's a bug with some services that aren't reset properly.
The proposed approach will kill performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Well, the performance kill is not really an argument for me, it's the same if you use Varnish (actually even worse because of the PHP startup time). Anyway, if resetting the services is the way to go then I'm fine with that. Can you explain to me why we have a $sessionUsageStack
in https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/EventListener/AbstractSessionListener.php#L67 then? As far as I could tell this is to count the usages across multiple master requests which should never happen except for the HttpCache
case? Does it? If not, then resetting the stack (which doesn't need to be a stack anymore then) would be more consistent with the rest of Symfony or am I wrong here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AbstractSessionListener doesn't know anything about HttpCache - and shouldn't. So it has to stack to keep being agnostic, isn't it?
Should the services resetter be called between virtual ESI requests? I think so :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree it shouldn't know about HttpCache. I don't know if it has to keep the stack to be agnostic. We could argue the same for every other service. Why is there the ResetInterface
in the first place then? :)
Should the services resetter be called between virtual ESI requests? I think so :)
Yes, it should. That's why I proposed to actually shutdown and boot the whole kernel again. I know it's not performant but in fact that's exactly what happens when you use Symfony behind Varnish. You have a completely separate PHP process for every single ESI request. ESI has to be chosen wisely, that's the nature of ESI fragments 😊
But I'm also fine calling the services resetter, although in #34688 (comment) you said it's already done?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't trust me, trust the code :)
I'm not sure we get inside this "if", so maybe we should do a direct call to the resetter.
if (!$this->requestStackSize && $this->resetServices) { |
Shutting-down the kernel is no-go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, so here are my findings. I did test a simple site with one fragment, so this:
/about-us
└── /_fragment?_controller=controller1......&_hash=supersecure1
Here's what's happening:
Kernel::handle()
for/about-us
Kernel::boot()
for/about-us
.$this->requestStackSize
is0
and$this->resetServices
isfalse
. Note that afterboot()
$this->resetServices
is set totrue
so the next time when we boot, it will betrue
.AbstractSessionListener::onKernelResponse()
detects asessionUsageIndex
of10
in my case. Compared toend($this->sessionUsageIndex)
which equals0
it differs, so the response is turned into aprivate
response correctly.- The ESI fragment is detected so
HttpCache::forward()
is called which callsKernel::boot()
. Because of$this->resetServices
being set totrue
in step 2, services are now correctly reset and the service resetter is called.$this->resetServices
is set tofalse
so it's not done again. - We are now at
Kernel::handle()
again because that's called byHttpCache::forward()
after callingKernel::boot()
in step 4. The services are not reset because$this->resetServices
is nowfalse
but it's set totrue
again for the nexthandle()
call. AbstractSessionListener::onKernelResponse()
detects asessionUsageIndex
of16
now in my case. Compared toend($this->sessionUsageIndex)
which equals10
now, it still differs, so this response is turned into aprivate
response as well.
So to sum up I think we can state the following things here:
- This PR is not required because services are correctly reset and we don't need to reboot.
- The
sessionStack
logic in theAbstractServiceListener
could be implemented without the complexity it has now by using theResetInterface
like all the other services in Symfony for reasons of consistency. I even think, counting theusageIndex
(which again brings a lot of complexity in my opinion) is not needed. The$session->hasBeenStarted()
implementation would've been enough imho. I don't know why exactly this was implemented. Maybe there are other reasons that go beyond resetting between surrogate requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The $session->hasBeenStarted() implementation would've been enough
i have some very vagure recollections that in the beginning of symfony 2, there were problems where people accidentally started the session e.g. by just checking if there is anything, as that auto-created the session. at least at the time, the answer was to first check if a session exists at all. maybe this behaviour is a DX to help badly programmed applications that accidentally start a session to not break all caching? (though if that is the case, a phpdoc on the usageIndex to explain this would be a good idea)
does git blame or history somehow help figuring out when this was introduced?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Except supposed complexity, is there anything to fix then?
Otherwise, not sure we should care :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this behaviour is a DX to help badly programmed applications that accidentally start a session to not break all caching?
no that's not related, you still need to check Request::hasPreviousSession()
for that.
I don't know why exactly this was implemented. Maybe there are other reasons that go beyond resetting between surrogate requests.
As far as I noticed, the counter is also increased if the security token storage is accessed, even if the session is not started.
Except supposed complexity, is there anything to fix then?
Otherwise, not sure we should care :)
Well there's so much complexity, we (@Toflar, me and other Contao) devs are banging our head at why this isn't working as expected and why it's so complex. Also, tracking the security token storage state inside a session is completely non-obvious and makes no sense except for it works that way.
I don't think there's an urgent fix needed here, but I'd love to see some DX improvements. Using the well-known/very clear kernel.reset
to check the session usage is certainly one step in that direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we can close this because functionality-wise it's fine. Just DX is horrible tbh.
Thanks for the discussion, closing then. |
Thanks for the discussion too! I appreciate the fact that you're trying to understand our issues :) |
I feel like the current surrogate handling is incorrect.
The main issue here really is about surrogate requests. So let's think about a page structure that contains two ESI fragments like so:
So let's see what
HttpCache
does:/about-us
(real HTTP request, Symfony treats it as master request)./_fragment?_controller=controller1
(Internal surrogate request, Symfony treats it as sub request)./_fragment?_controller=controller2
(Internal surrogate request, Symfony treats it as sub request).So we have only 1 PHP process that is started. Things might be shared across fragments.
Now let's think about what Varnish does:
/about-us
(real HTTP request, Symfony treats it as master request)./_fragment?_controller=controller1
(real HTTP request, Symfony treats it as master request)./_fragment?_controller=controller2
(real HTTP request, Symfony treats it as master request).So we have 3 separate PHP processes, nothing can be shared!
Because of that, if we had no special handling for e.g. the request stack and also the session, the fragment requests would not be isolated. And especially for the session handling, this turns out to be pretty complicated. The
SessionListener
(orAbstractSessionListener
to be specific) tries to keep track of every request and response, increasing and decreasing the stack etc.However, apart from the fact that it is very complicated to understand, Symfony also behaves differently when running behind
HttpCache
or Varnish.If we would reset the whole kernel for every surrogate request and actually execute them as master requests, it would behave the same as if Varnish requested the fragments (provided we reset and shutdown everything correctly).
This of course has effects on every listener that checks for
if (!$event->isMasterRequest())
such as firewall listeners etc.But wouldn't it be actually correct? Because that's exactly what happens when Symfony runs behind Varnish. Wouldn't we want to try and behave exactly the same, no matter what kind of reverse proxy is put in front of the app?
This is just a draft PR and again, no idea about the implications but I would like to get some feedback on this 😊