Fix and streamline page cache and session handling
Damien Tournoud - May 31, 2009 - 14:30
| Project: | Drupal |
| Version: | 7.x-dev |
| Component: | base system |
| Category: | bug report |
| Priority: | critical |
| Assigned: | Unassigned |
| Status: | needs work |
| Issue tags: | Favorite-of-Dries, Needs Documentation |
Description
This patch is a big clean-up of the page cache process and session handling, inspired by #370454: Simplify page caching.
This patch:
- Adds a drupal_page_cacheable() method to determine (and force) page cacheability,
- Starts an output buffer unconditionally in DRUPAL_BOOTSTRAP_PAGE_CACHE, in order to simplify the page cache workflow,
- Streamlines the session management: because we have an output buffer enabled, we don't need the drupal_(get|set)_session() magic wrappers anymore. We simply start or cleanup the session on demand, at the end of the page workflow, in drupal_page_footer(),
- Rename and clean-up a few related functions, to get them a clearer name and a clearer responsibility.

#1
#2
Using drupal_static() instead of the static in drupal_page_cacheable(), thanks chx.
#3
I think this is very very nice. Biggest positives are the removal of drupal_set_session and the anonymous session destroy. Once this is in, we can get back to the page cache simplify which will be a LOT easier to accomplish after this.
#4
Very nice work.
+ session_id(md5(uniqid()));+ // session is only started on demand in drupal_commit_session(), makingAFAICT drupal_session_commit() can be written in a more compact and readable way with identical behaviour like this:
function drupal_session_commit() {
global $user;
if (empty($user->uid) && empty($_SESSION)) {
if (drupal_session_started()) {
// Destroy empty anonymous sessions.
session_destroy();
}
}
else {
if (!drupal_session_started()) {
drupal_session_start();
}
session_write_close();
}
}
+function drupal_session_start() {+ global $user;
if (!isset($_SESSION['openid'])) {- drupal_set_session('openid', array());
+ $_SESSION['openid'] = array();
}
$_SESSION['openid']['values'] = $form_state['values'];
$anonymous = drupal_session_count(0, TRUE);$authenticated = drupal_session_count(0, FALSE);
- $this->assertEqual($anonymous + $authenticated, $this->session_count, t('Correctly counted @count total sessions.', array('@count' => $this->session_count)), t('Session'));
+ $this->assertEqual($anonymous + $authenticated, $session_count['anonymous'] + $session_count['authenticated'], t('Correctly counted @count total sessions.', array('@count' => 1)), t('Session'));
During testing I got the following error, but now I cannot reproduce it, so it may be caused by something else: Fatal error: session_start(): Failed to initialize storage module: user (path: /tmp) in /home/chsc/www/drupal7/includes/session.inc on line 196
#0 drupal_session_start() called at [/home/chsc/www/drupal7/includes/session.inc:172]
#1 drupal_session_initialize() called at [/home/chsc/www/drupal7/includes/bootstrap.inc:1366]
#2 _drupal_bootstrap(4) called at [/home/chsc/www/drupal7/includes/bootstrap.inc:1302]
#3 drupal_bootstrap(9) called at [/home/chsc/www/drupal7/index.php:21]
I wonder whether it would be more efficient to replace
print drupal_render_page($return);in index.php with$output = drupal_render_page($return);and then pass that to drupal_page_footer(), and then addob_end_flush(); print $outputto the bottom of drupal_page_footer(). The whole page has already been generated in one string, and this change prevents the string from being put into the output buffer before being sent to the client. I haven't benchmarked it, though. The same approach can be used by drupal_serve_page_from_cache().#5
I saw this error message too (but wasn't able to reproduce it either), and it seems related to: PHP Bug #45363. My gut feeling is that moving the session_set_save_handler() just before session_start() should prevent the issue from happening.
#6
#7
Here is a new version, taking care of c960657 remarks:
- use uniqid('', TRUE) instead of uniqid(), which is slower on some systems
- fix comments
- refactor drupal_session_commit() for clearness
- removed a useless $user
- revert useless isset() constructs all over the place
- remove the array in session.test
- move session_set_save_handler() just before session_start()
I haven't made the suggested change to how we handle output buffers. I don't think it would make a big difference, and it can wait for another patch!
#8
The last submitted patch failed testing.
#9
Erm.
Calling session_set_save_handler() apparently resets the session handler state, for some (internal PHP) reasons. We cannot move session_set_save_handler() into drupal_session_start(), because we already called some session_* functions at this time. This patch should pass the test bot.
#10
The last submitted patch failed testing.
#11
I accidentally introduced a few E_ALL warnings. Lets fix those.
#12
I gave this patch another good look, and it is a very solid clean-up. Committed to CVS HEAD so we can keep making progress. Great work!
#13
I still get this error when logging out (using PHP 5.2.0):
Fatal error: session_start(): Failed to initialize storage module: user (path: /tmp) in /home/chsc/www/drupal7/includes/session.inc on line 196
#14
I can confirm the fatal error on a clean install of HEAD on a clean db.
For whatever reason, the only page that produces this error for me is the Clean URLs settings page. To produce:
And I get that php error.
Reverting to the June 2, 2009 revision (i.e., -D "2 June 2009") removes the problem.
Test server has been removed.
Since some people had problems reproducing the error, I've set up a test server for this:http://477944.jamesan.ca
User: 477944
Pass: 477944
There's a primary link to the Clean URLs settings page at the top-right corner of the page.
#15
I've pinpointed the problem to be something to do with assigning a value in to the $_SESSION in system_clean_url_settings():
$_SESSION['clean_url'] = TRUE;.We can replace the use of $_SESSION['clean_url'] with the variable_get() and variable_set() with 'clean_url', which is already used to store whether clean urls are enabled.
The patch makes that change, although I don't think this fix addresses the real problem.
#16
That sounds like a race condition somewhere. That would explain why it is hard to reproduce.
#17
Here's the PHP stack trace before the fatal error:
#0 drupal_session_start() called at [includes/session.inc:173]
#1 drupal_session_initialize() called at [includes/bootstrap.inc:1362]
#2 _drupal_bootstrap(4) called at [includes/bootstrap.inc:1298]
#3 drupal_bootstrap(9) called at [index.php:21]
Warning: session_start(): Cannot send session cache limiter - headers already sent (output started at includes/session.inc:196) in includes/session.inc on line 197
Warning: Cannot modify header information - headers already sent by (output started at includes/session.inc:196) in includes/bootstrap.inc on line 854
Warning: Cannot modify header information - headers already sent by (output started at includes/session.inc:196) in includes/bootstrap.inc on line 854
Warning: Cannot modify header information - headers already sent by (output started at includes/session.inc:196) in includes/bootstrap.inc on line 854
Warning: Cannot modify header information - headers already sent by (output started at includes/session.inc:196) in includes/bootstrap.inc on line 854
#0 drupal_session_start() called at [includes/session.inc:223]
#1 drupal_session_commit() called at [includes/common.inc:330]
#2 drupal_goto() called at [modules/user/user.pages.inc:146]
#3 user_logout()
#4 call_user_func_array(user_logout, Array ()) called at [includes/menu.inc:402]
#5 menu_execute_active_handler() called at [index.php:22]
Fatal error: session_start(): Failed to initialize storage module: user (path: /var/lib/php5) in includes/session.inc on line 197
#18
It's not hard to reproduce, it just so happens to happen *exactly once* on a fresh install. JamesAn nails it exactly in #14. You MUST start with a fresh install. Immediately after finishing the install, try to logout. Drupal throws the session error but does actually log you out. Refresh the page and see that you're now logged out. After the first time logging out, you can login/logout without any further trouble.
#19
I confirm I'm getting the same thing, somewhat randomly. quicksketch sees it too.
Info from XDebug which looks pretty identical to what JamesAn posted.
Fatal error: session_start(): Failed to initialize storage module: user (path: /Applications/MAMP/tmp/php) in /Users/webchick/Sites/core/includes/session.inc on line 196Call Stack:
0.0006 72664 1. {main}() /Users/webchick/Sites/core/index.php:0
0.1544 9654224 2. menu_execute_active_handler() /Users/webchick/Sites/core/index.php:22
0.1580 9811928 3. call_user_func_array() /Users/webchick/Sites/core/includes/menu.inc:402
0.1580 9811928 4. user_logout() /Users/webchick/Sites/core/includes/menu.inc:0
0.1649 9827136 5. drupal_goto() /Users/webchick/Sites/core/modules/user/user.pages.inc:146
0.1650 9827544 6. drupal_session_commit() /Users/webchick/Sites/core/includes/common.inc:330
0.1651 9827732 7. drupal_session_start() /Users/webchick/Sites/core/includes/session.inc:222
0.1651 9827988 8. session_start() /Users/webchick/Sites/core/includes/session.inc:196
#20
Oh, slow on the draw. :)
#21
What happens, I think, is this:
The test actually triggers the bug, but it isn't noticed because DrupalWebTestCase::drupalLogout() simply discards the output from user/logout.
This patch makes user_logout() reset $_SESSION. Also it was necessary to reset $this->loggedInUser when changing session cookies in order to prevent drupalLogin() from calling drupalLogout() first, even if the new session cookie did not represent a logged in user. The patch doesn't go further to workaround the PHP bug - I'm not sure whether this is necessary.
#22
The problem seems fixed when I apply this patch. Hopefully testbot will come back with happy results.
#23
Nice catch, c960657. I wasn't able to understand the scenario that made us start a session twice.
@JamesAn: does that solves the issue you were seeing on the clean-url page?
#24
@Damien Tournoud: yes, indeed.
#25
At the very least we should document why session_destroy() is not doing what it says on the tin. This part:
+ // Destroy the current session.session_destroy();
+ $_SESSION = array();
But it feels like we're only masking something deeper.
#26
Doing a bit of digging, apparently session_destroy() does not unset $_SESSION.
From http://ca.php.net/session_destroy
and again: Johan 20-Nov-2004 02:00
So we do need to manually clear $_SESSION like it's done in #21. We can't unset it "as this will disable the registering of session variables through the $_SESSION superglobal." (http://ca2.php.net/manual/en/function.session-unset.php)
Should we document that session_destroy() doesn't clear $_SESSION, since it's not obvious?
#27
Ok, well then my question is, "Is this actually desirable?" :) Because that $_SESSION = array() line has not existed for the entire history of Drupal (or at least since 4.6) in that function. Why do we need to add it now?
#28
Ok, apparently this is not very clear:
Because we don't want to restart a session in that case, the correct fix is indeed to nuke $_SESSION data.
#29
Oops. Forgot that Drupal changes the session storage functions.
The bug seems to only rear its head when code manually sets $_SESSION keys, like the clean url settings page. Perhaps we haven't seen it before as modules have historically not fiddled around in $_SESSION, as $_SESSION values are transient and settings are permanently saved in the db.
Does this need to be backported?
#30
This needs to be better documented in the code, IMO.
#31
Added a comment above the resetting of $_SESSION.
#32
The comment works for me. I think this patch is good to go since it's fixed the problem on my test.
#33
Further clean-up (and an additional test): we don't want to start a session when drupal_save_session() is FALSE.
I moved cleaning $_SESSION and unsetting the cookie in _sess_destroy_sid(), where it makes more sense.
#34
This looks good. I like that more logic is moved from the user module into the session handler.
function _sess_destroy_sid($sid) {...
+ if ($sid == session_id()) {
+ $this->pass($this->drupalGetHeader('Set-Cookie'));In drupal_session_commit(), should we - for symmetry - avoid destroying empty anonymous sessions, if drupal_save_session() is FALSE?
+ // Remove $_SESSION data, and restore the user object.+ $_SESSION = array();
+ $user = drupal_anonymous_user();
Reset $_SESSION and $user to prevent a new session from being started in drupal_session_commit().+ setcookie(session_name(), '', time() - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);#35
For anyone on the infrastructure team looking at this issue, the only commit to HEAD referencing this issue is this:
http://vcs.fourkitchens.com/drupal/7-all-history/revision/10220
Does anyone know if any of the cleanup patches have made it in?
#36
Here is an updated patch.
No. This function can be called for a session ID that is not the one of the current user. Added a comment.
Removed.
Indeed. Fixed.
Ok.
Done.
#37
"Always use httponly cookies." is an inaccurate comment. That configuration only changes the HTTP-only status for *session* cookies.
"Destroy the current session, it will also reload the anonymous user." is a comma splice.
"Destroy the current session, it will also reset $user to the anonymous
+ // user." is also a comma splice.
Change both comma splice issues to "Destroy the current session and reset $user to the anonymous user."
#38
Done.
#39
Better yet.
#40
Looks good to me.
#41
When is the function called with a session other than the one of the current user? It isn't documented explicitly in the PHP manual for session_set_save_handler() that this wont happen, but _sess_read() and _sess_write() assume this (they use the global $user object). So I think _sess_destroy_sid() should make the same assumption, unless there is a reason not to do so.
(For further consistency, _sess_destroy_sid() should probably be renamed to just _sess_destroy(), and $key should be renamed to $sid a few places in session.inc, but we can do that in another issue)
I think the flow in drupal_session_commit() would be easier to follow if drupal_save_session() is called only once in the benning of the function, i.e.
if (!drupal_save_session()) { return; }.+ // Reset $_SESSION and $user to prevent a new session from being started
+ // in drupal_session_commit()
Sentence does not end with a period.
#42
Subscribing. Berdir adviced me to look at this issue since this patch might also solve #500680: Fatal error on logout. I will test is whenever I find the time to do it.
#43
Marked #500680: Fatal error on logout as duplicate. The current issue will indeed solve this bug if the session data are destroyed when the session is destroyed. For the moment, I only tested Damien Tournoud's patch to see if it would solve the bug I spotted and it does solve it.
#44
Here are further cleanups following #41:
- _sess_*() are now renamed _drupal_session_*(). We don't do abbreviations, and those were hurting my eyes.
- $key is now consistently $sid
- _drupal_session_destroy() now assume that $sid is the one of the user (I agree with c960657 that it makes sense)
- refactored drupal_session_commit() code flow so that we only call drupal_save_session() once, and added a few comments to ease readability
Untested patch, but the test bot is there for us, right? ;)
#45
The last submitted patch failed testing.
#46
And a non broken one.
#47
Tests pass and it looks like a fairly straight-forward but important clean-up. Committed.
#48
Automatically closed -- issue fixed for 2 weeks with no activity.
#49
This is entirely undocumented. Worse, the upgrade guide still states drupal_set_session() in http://drupal.org/node/224333#drupal_set_session, which does not exist anymore.
I had to dig through 4-5 issues + look at each committed patch to find the one that is guilty.
Furthermore, the in-code API documentation is a bit sparse... At least drupal_session_start() should clarify Drupal's default session handling, IMHO. In addition to that, the first function that developers are looking at is drupal_session_initialize(), but that literally has zero documentation about what it does and what it doesn't do, neither any mentioning of drupal_session_start()...