MDL-58018 core: Add support to open sessions without a lock

This commit is contained in:
Adam Eijdenberg 2019-07-30 14:58:00 +08:00 committed by Mark Nelson
parent 788dfb9c7d
commit 1c3b89b170
3 changed files with 114 additions and 21 deletions

View File

@ -34,6 +34,9 @@ defined('MOODLE_INTERNAL') || die();
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class handler {
/** @var boolean $haslock does the session need and/or have a lock? */
protected $haslock = false;
/**
* Start the session.
* @return bool success
@ -42,6 +45,49 @@ abstract class handler {
return session_start();
}
/**
* Write the session and release lock. If the session was not intentionally opened
* with a write lock, then we will abort the session instead if able.
*/
public function write_close() {
if ($this->has_writelock()) {
session_write_close();
$this->haslock = false;
} else {
$this->abort();
}
}
/**
* Release lock on the session without writing it.
* May not be possible in older versions of PHP. If so, session may be written anyway
* so that any locks are released.
*/
public function abort() {
session_abort();
$this->haslock = false;
}
/**
* This is called after init() and before start() to indicate whether the session
* opened should be writable or not. This is intentionally captured even if your
* handler doesn't support non-locking sessions, so that behavior (upon session close)
* matches closely between handlers.
* @param bool $needslock true if needs to be open for writing
*/
public function set_needslock($needslock) {
$this->haslock = $needslock;
}
/**
* Has this session been opened with a writelock? Your handler should call this during
* start() if you support read-only sessions.
* @return bool true if session is intended to have a write lock.
*/
public function has_writelock() {
return $this->haslock;
}
/**
* Init session handler.
*/

View File

@ -57,6 +57,28 @@ class manager {
/** @var string $logintokenkey Key used to get and store request protection for login form. */
protected static $logintokenkey = 'core_auth_login';
/**
* If the current session is not writeable, abort it, and re-open it
* requesting (and blocking) until a write lock is acquired.
* If current session was already opened with an intentional write lock,
* this call will not do anything.
* NOTE: Even when using a session handler that does not support non-locking sessions,
* if the original session was not opened with the explicit intention of being locked,
* this will still restart your session so that code behaviour matches as closely
* as practical across environments.
*/
public static function restart_with_write_lock() {
if (self::$sessionactive) {
if (!self::$handler->has_writelock()) {
@self::$handler->abort();
self::$sessionactive = false;
}
}
if (!self::$sessionactive) {
self::start_session(true);
}
}
/**
* Start user session.
*
@ -82,16 +104,49 @@ class manager {
return;
}
if (defined('REQUIRE_SESSION_LOCK') && defined('ENABLE_READ_ONLY_SESSIONS') && ENABLE_READ_ONLY_SESSIONS) {
$needslock = REQUIRE_SESSION_LOCK;
} else {
$needslock = true; // For backwards compatibility, we default to assuming that a lock is needed.
}
self::start_session($needslock);
}
/**
* Handles starting a session.
*
* @param bool $needslock If this is false then no write lock will be acquired,
* and the session will be read-only.
*/
private static function start_session(bool $needslock) {
global $PERF;
try {
self::$handler->init();
self::$handler->set_needslock($needslock);
self::prepare_cookies();
$isnewsession = empty($_COOKIE[session_name()]);
if (defined('DEBUG_SESSION_TIMING_MIN_TIME')) {
$starttime = microtime(true);
}
if (!self::$handler->start()) {
// Could not successfully start/recover session.
throw new \core\session\exception(get_string('servererror'));
}
// DEBUG_SESSION_TIMING_MIN_TIME if defined is a float, and we'll show a message for
// session aquisition that exceeds this amount in seconds.
if (defined('DEBUG_SESSION_TIMING_MIN_TIME')) {
$duration = microtime(true) - $starttime;
if ($duration > DEBUG_SESSION_TIMING_MIN_TIME) {
/* don't log the raw session ID, since the session ID itself is a secret */
error_log("slow_session_id:".hash('sha256', session_id())."|duration:".
round($duration, 3)."s|session_url:".$_SERVER['PHP_SELF']."|write_lock:".($needslock ? '1' : '0'));
}
}
// Grab the time when session lock starts.
$PERF->sessionlock['gained'] = microtime(true);
$PERF->sessionlock['wait'] = $PERF->sessionlock['gained'] - $PERF->sessionlock['start'];
@ -633,9 +688,7 @@ class manager {
$DB->delete_records('sessions', array('sid'=>$sid));
self::init_empty_session();
self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
session_write_close();
self::$sessionactive = false;
self::write_close();
self::append_samesite_cookie_attribute();
}
@ -657,25 +710,18 @@ class manager {
self::sessionlock_debugging();
}
if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
// More control over whether session data
// is persisted or not.
if (self::$sessionactive && session_id()) {
// Write session and release lock only if
// indication session start was clean.
session_write_close();
} else {
// Otherwise, if possibile lock exists want
// to clear it, but do not write session.
@session_abort();
}
// More control over whether session data
// is persisted or not.
if (self::$sessionactive && session_id()) {
// Write session and release lock only if
// indication session start was clean.
self::$handler->write_close();
} else {
// Any indication session was started, attempt
// to close it.
if (self::$sessionactive || session_id()) {
session_write_close();
}
// Otherwise, if possibile lock exists want
// to clear it, but do not write session.
@self::$handler->abort();
}
self::$sessionactive = false;
}

View File

@ -101,6 +101,8 @@ class memcached extends handler {
* @return bool success
*/
public function start() {
ini_set('memcached.sess_locking', $this->has_writelock() ? '1' : '0');
// NOTE: memcached before 2.2.0 expires session locks automatically after max_execution_time,
// this leads to major difference compared to other session drivers that timeout
// and stop the second request execution instead.
@ -148,7 +150,6 @@ class memcached extends handler {
ini_set('session.save_handler', 'memcached');
ini_set('session.save_path', $this->savepath);
ini_set('memcached.sess_prefix', $this->prefix);
ini_set('memcached.sess_locking', '1'); // Locking is required!
ini_set('memcached.sess_lock_expire', $this->lockexpire);
if (version_compare($version, '3.0.0-dev') >= 0) {