Problem/Motivation
This issue was filed in order to help reintroducing #2239969: Session of (UI) test runner leaks into web tests which was reverted because it was incompatible with #1289536: Switch Watchdog to a PSR-3 logging framework.
The session_manager
service is currently persisting when rebuilding the container. This is in order to keep the session open during kernel rebuilds. The problem is that SessionManager::initialize()
is not reentrant for anonymous sessions: If no user is logged in, a LogicException: Cannot change the ID of an active session
is thrown when the method is called more than once.
During test-setup, the container is rebuilt quite often. Since #1289536: Switch Watchdog to a PSR-3 logging framework the current user service is used every time a module is installed. Because the cookie authenticator triggers session initialization whenever it is used, this may lead to the exception being thrown when tests are executed via UI. The command line test runner is not affected, because the session manager guards against starting a session when running from the CLI.
Proposed resolution
Close and save the session before the container is rebuilt, and restart it afterwards if necessary.
Remaining tasks
User interface changes
API changes
Comment | File | Size | Author |
---|---|---|---|
#51 | drupal8.rebuild-session-save.51.patch | 520 bytes | sun |
#42 | 2272987-no-persist-session-manager-42.diff | 10.59 KB | znerol |
#27 | 2272987.27.patch | 12.92 KB | alexpott |
#27 | 17-27-interdiff.txt | 4.83 KB | alexpott |
#17 | interdiff.txt | 1.49 KB | znerol |
Comments
Comment #1
znerol CreditAttribution: znerol commentedComment #3
znerol CreditAttribution: znerol commentedTest failures caused by the PHP warning: SessionHandler::read(): The session id is too long or contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,' in
Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy->read()
indicate two things:NULL
is passed instead of a session handler object. This is the case uponNativeSessionStorage::__construct()
as we currently use it from withinDrupal\Core\Session\SessionManager::__construct()
. The real session save handler is only set from withinSessionManager::initialize()
.SessionManager
was initialized properly.I think we should explicitly set the session save handler from within construct in order to absolutely prevent that we fall back to the default native PHP session handler.
Comment #5
znerol CreditAttribution: znerol commentedLet's call
SessionManager::initialize()
when the service is constructed.Comment #6
znerol CreditAttribution: znerol commentedComment #7
znerol CreditAttribution: znerol commentedComment #9
znerol CreditAttribution: znerol commentedSeems like #5 is one of the key-changes here. With
SessionManager::initialize()
being called upon service construction, we can get rid of some manual session-handling in the installer, introduced in the first patch.Comment #10
sunAll of these calls look like "magic" now — instead of calling
initialize()
manually (giving sense to the calling code), you rely on the service container to call it for you.I could get more behind that if the constructor would internally call
initialize()
, but that's not the case.FWIW, the issue title is misleading — you're not really removing persistence, all you're doing is to replace the 'persist' mechanism with some manual operations (that are executed in the same spot as the service persistence).
I dislike that this hard-codes even more things into kernel/container (re)build process... what do we actually gain from that?
Still part of the public interface + actively called, shouldn't be removed.
Comment #11
znerol CreditAttribution: znerol commentedComment #12
znerol CreditAttribution: znerol commented@sun thank you for looking at this. Please note that persisting a service means keeping a reference to an object built by an older container. This patch reduces the amount of state which is necessary to be kept during the container rebuild from a whole object with uncontrollable references to a simple boolean. This is a whole lot better than
persist
and helps with resolving real problems.Probably we should introduce a
StatefulService
interface and let services implement their own persisting logic at some point.Comment #13
znerol CreditAttribution: znerol commentedRe #10.1 and #10.3: How about that:
SessionManager::initialize()
toSessionManager::startLazy()
SessionStorageInterface::start()
for consistency reasonsSessionManager:startLazy()
reentrant (just likeSessionStorageInterface::start()
)Comment #14
alexpottThis looks odd to me - like it would you could call startLazy twice and the first time it returns FALSE and sets
$this->startedLazy
to TRUE and then the next time it returns TRUE even though a session has not been started.Comment #15
znerol CreditAttribution: znerol commentedRe #14: True, I think the most useful thing to return is (
$this->started
).Comment #16
jibrandouble ;
Comment #17
znerol CreditAttribution: znerol commentedUh, thanks @jibran.
Comment #18
znerol CreditAttribution: znerol commentedComment #19
cosmicdreams CreditAttribution: cosmicdreams commentedThis feels wrong. What I think this code is saying is that the DrupalKernel is managing the state of the SessionManager. Which is putting too much responsibility on the DrupalKernel.
SessionManager should manage the state of the SessionManager.
As a sign that this should be extracted to the SessionManager is that the logic is duplicated in the WebTestBase.
Comment #20
znerol CreditAttribution: znerol commentedRemoving the
persist
flag from thesession_manager
service means, that the session manager instance is not retained anymore across container rebuilds. Therefore it is not possible to keep the state inside the session manager instance, because it will be a different one before and after the rebuild.The best way to solve this problem is to completely get rid of the current
persist
logic and instead implement a mechanism allowing us to record and restore state (instead of service instances): #2281903: Refactor DrupalKernel::initializeContainer in preparation of a replacement for persist servicesUpdate: Clarified first paragraph.
Comment #21
cosmicdreams CreditAttribution: cosmicdreams commentedCool so this is an intermediate step? If so everything else looked good
Comment #22
cosmicdreams CreditAttribution: cosmicdreams commentedCool so this is an intermediate step? If so everything else looked good
Comment #24
znerol CreditAttribution: znerol commented17: 2272987-no-persist-session-manager-17.diff queued for re-testing.
Comment #25
znerol CreditAttribution: znerol commentedFailure in #23 was bogus.
Comment #26
alexpottComment #27
alexpottFor my tastes there is way too much session manager logic going on here. Let's encapsulate this on the SessionManager.
Comment #28
znerol CreditAttribution: znerol commentedThis came up before but as I explained in #20 I think this is not really possible. With #2281903: Refactor DrupalKernel::initializeContainer in preparation of a replacement for persist services and #2285621: Remove persist-tag and replace it with a container.state_recorder service it will be possible to solve that in a really clean way.
In my opinion, no service should encapsulate the logic about how it needs to be persisted across container rebuilds.
Comment #29
alexpott@znerol looking #2285621: Remove persist-tag and replace it with a container.state_recorder service - I don't get the benefit of creating separate state recorders of the services that need persistent state - this looks overly complex. Can't we just have an interface on each service that needs state persisting with the methods getState() and restoreState().
Comment #30
catchI like the idea of closing the session and starting it again across container rebuilds, it's special after all.
I do wonder a bit whether we really need to maintain the exact state though - we have lazy session initialization for a reason, so if the session has been closed then started lazily again, it ought to just work if it gets accessed again after it's closed. That would save a lot of the logic here in the session manager.
Comment #31
znerol CreditAttribution: znerol commentedThe problem is that currently it is not save to call
SessionManager::initialize()
multiple times for anonymous users. If we call it after a rebuild, and the cookie authentication provider calls it again, we attempt to set the anonymous session-id a second time. This will result in an exception. #2238561: Use the default PHP session ID instead of generating a custom one might help also with that.Comment #32
catchI hadn't noticed that issue but it looks sensible. I'd be tempted to bump this issue to critical and that one to major?
The workaround here seems OK to me if we think we can remove it via that issue.
Comment #33
alexpottSo how about we move forward with the patch in #27 to unblock the #2239969: Session of (UI) test runner leaks into web tests and a few other patches?
Comment #34
neclimdulSo, I agree with persisting objects being a problem, but I really do not like this approach. Our implementation is very complex and we're adding a new level of complexity. Specifically:
Having objects with state is hard and complicates how everything interacts with it. Every state has to be handled by the calling code which means we're moving directly _away_ from Symfony's session objects and into a stand alone system. SessionStorage already has state as well in SessionStorageInterface::isStarted() so we're either adding a second state or just saying "that state isn't valid, you have to use our method" which again has the previous problem of us moving away from the interface's we're trying to move towards.
Comment #35
znerol CreditAttribution: znerol commentedUh, I did not notice that #27 actually moved state handling into the interface/implementation. I totally agree with @neclimdul, state must be maintained outside. Can we just go with #17?
Comment #36
alexpottIn my opinion #17 is just as bad - hard coding state assumptions about the SessionManager in WebTestBase and DrupalKernel? These have no business maintaining that. So we're still maintaining a second state....
The goal has to be to completely remove state and service persistence so if #17 is going to get us there quicker then let's go with that. Actually neither patch moves us nearer or further imho this just unblocks patches that might eventually get us there i.e. the removal all other service persistence, #2239969: Session of (UI) test runner leaks into web tests and #2284103: Remove the request from the container
Comment #37
znerol CreditAttribution: znerol commentedThere is no way around maintaining state of some selected services. We cannot possibly start out with an empty request stack after the container has been rebuilt for example. With the issues linked from #28 we get exactly there: maintain minimal state of selected services across container rebuilds.
Comment #38
alexpottI think container rebuilding mid request is a mistake - as @fabpot says after a container rebuild we should just do a new request - therefore no need to maintain the request stack or session - but doing that now is going to cause us heaps of pain.
As I said let's get #17 in and see if we can inch towards maintaining a minimal state or selected services across rebuilds.
rtbc for #17
Comment #39
catchFine with doing #17 (I'm actually fine with either as long as we get the issues following this one done).
Comment #40
neclimdulI don't like either of these personally and see it as a leap backwards in terms of complexity we have to refactor to really fix the problem.
At least lets rebase this on #2016629: Refactor bootstrap to better utilize the kernel which changes the kernel methods we're touching similar to #2281903: Refactor DrupalKernel::initializeContainer in preparation of a replacement for persist services which is related to this issue.
Comment #42
znerol CreditAttribution: znerol commentedReroll of #17. No interdiff because there were tons of conflicts. There is one notable difference: The hunk in
WebTestBase
is not necessary anymore due toDrupalKernel::rebuildContainer
introduced by #2016629: Refactor bootstrap to better utilize the kernel.Comment #43
alexpottYeah I noticed that the hunk in
WebTestBase
would not be needed - which is great and a sign we're on the right path albeit it seems a little windy :)Thanks @znerol for sticking with this!
Comment #44
catch@neclimdul, I don't think it's complexity we'll need to refactor, it's more a case of temporary hacks we can just delete once it's working properly.
Comment #45
sunI think that would be a much better idea.
Would help to eliminate these fugly lines:
…and have something along the lines of the following instead:
I'm primarily concerned that this patch is hard-coding this facility for selected core services without giving (core + contrib) modules any way to do the same.
Comment #46
catch@sun - did you see #30/#31?
1. The session is unique (or almost unique) in terms of needing to maintaining state at the moment. We specifically do not want any services to do this at all.
2. Even session shouldn't need to maintain state if we can close and open it for anonymous users without throwing an exception due to session ID issues.
Comment #47
znerol CreditAttribution: znerol commented@sun: The stateful service was a bad idea, this would be much better: #2285621: Remove persist-tag and replace it with a container.state_recorder service. There is currently no consensus on whether something like this will be necessary or not.
Comment #48
sunSorry, it wasn't my intention to hold up this patch. Let's move forward here.
Comment #49
catchCommitted/pushed to 8.x, thanks!
Comment #51
sunThis change caused the following fatal error to appear when hitting
rebuild.php
:Attached patch fixes the problem.
Comment #52
sunAny chance to get this quick follow-up in? Would love to use rebuild.php without fatal errors again.
Comment #53
catchComment #54
alexpottCommitted b314a3e and pushed to 8.x. Thanks!