Merge branch 'MDL-30026' of git://github.com/stronk7/moodle

This commit is contained in:
Eloy Lafuente (stronk7) 2011-11-13 19:35:13 +01:00
commit da2e3c12db
10 changed files with 382 additions and 62 deletions

View File

@ -433,6 +433,7 @@ $string['secretalreadyused'] = 'Change password confirmation link was already us
$string['sectionnotexist'] = 'This section does not exist';
$string['sendmessage'] = 'Send message';
$string['servicedonotexist'] = 'The service does not exist';
$string['sessionwaiterr'] = 'Timed out while waiting for session lock.<br />Wait for your current requests to finish and try again later.';
$string['sessioncookiesdisable'] = 'Incorrect use of require_key_login() - session cookies must be disabled!';
$string['sessiondiskfull'] = 'The session partition is full. It is not possible to login at this time.<br /><br />Please notify server administrator.';
$string['sessionerroruser'] = 'Your session has timed out. Please login again.';

View File

@ -2204,9 +2204,10 @@ abstract class moodle_database {
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid) {
public function get_session_lock($rowid, $timeout) {
$this->used_for_db_sessions = true;
}

View File

@ -1227,18 +1227,46 @@ s only returning name of SQL substring function, it now requires all parameters.
return true;
}
public function get_session_lock($rowid) {
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid, $timeout) {
if (!$this->session_lock_supported()) {
return;
}
parent::get_session_lock($rowid);
parent::get_session_lock($rowid, $timeout);
$timeoutmilli = $timeout * 1000;
$fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
$sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', 120000";
// There is one bug in PHP/freetds (both reproducible with mssql_query()
// and its mssql_init()/mssql_bind()/mssql_execute() alternative) for
// stored procedures, causing scalar results of the execution
// to be cast to boolean (true/fals). Here there is one
// workaround that forces the return of one recordset resource.
// $sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', $timeoutmilli";
$sql = "BEGIN
DECLARE @result INT
EXECUTE @result = sp_getapplock @Resource='$fullname',
@LockMode='Exclusive',
@LockOwner='Session',
@LockTimeout='$timeoutmilli'
SELECT @result
END";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = mssql_query($sql, $this->mssql);
$this->query_end($result);
if ($result) {
$row = mssql_fetch_row($result);
if ($row[0] < 0) {
throw new dml_sessionwait_exception();
}
}
$this->free_result($result);
}

View File

@ -1203,10 +1203,17 @@ class mysqli_native_moodle_database extends moodle_database {
return true;
}
public function get_session_lock($rowid) {
parent::get_session_lock($rowid);
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid, $timeout) {
parent::get_session_lock($rowid, $timeout);
$fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
$sql = "SELECT GET_LOCK('$fullname',120)";
$sql = "SELECT GET_LOCK('$fullname', $timeout)";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = $this->mysqli->query($sql);
$this->query_end($result);
@ -1218,8 +1225,7 @@ class mysqli_native_moodle_database extends moodle_database {
if (reset($arr) == 1) {
return;
} else {
// try again!
$this->get_session_lock($rowid);
throw new dml_sessionwait_exception();
}
}
}

View File

@ -1610,19 +1610,28 @@ class oci_native_moodle_database extends moodle_database {
return $this->dblocks_supported;
}
public function get_session_lock($rowid) {
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid, $timeout) {
if (!$this->session_lock_supported()) {
return;
}
parent::get_session_lock($rowid);
parent::get_session_lock($rowid, $timeout);
$fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
$sql = 'SELECT MOODLE_LOCKS.GET_LOCK(:lockname, :locktimeout) FROM DUAL';
$params = array('lockname' => $fullname , 'locktimeout' => 120);
$params = array('lockname' => $fullname , 'locktimeout' => $timeout);
$this->query_start($sql, $params, SQL_QUERY_AUX);
$stmt = $this->parse_query($sql);
$this->bind_params($stmt, $params);
$result = oci_execute($stmt, $this->commit_status);
if ($result === false) { // Any failure in get_lock() raises error, causing return of bool false
throw new dml_sessionwait_exception();
}
$this->query_end($result, $stmt);
oci_free_statement($stmt);
}

View File

@ -1153,7 +1153,13 @@ class pgsql_native_moodle_database extends moodle_database {
return true;
}
public function get_session_lock($rowid) {
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid, $timeout) {
// NOTE: there is a potential locking problem for database running
// multiple instances of moodle, we could try to use pg_advisory_lock(int, int),
// luckily there is not a big chance that they would collide
@ -1161,9 +1167,40 @@ class pgsql_native_moodle_database extends moodle_database {
return;
}
parent::get_session_lock($rowid);
parent::get_session_lock($rowid, $timeout);
$timeoutmilli = $timeout * 1000;
$sql = "SET statement_timeout TO $timeoutmilli";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = pg_query($this->pgsql, $sql);
$this->query_end($result);
if ($result) {
pg_free_result($result);
}
$sql = "SELECT pg_advisory_lock($rowid)";
$this->query_start($sql, null, SQL_QUERY_AUX);
$start = time();
$result = pg_query($this->pgsql, $sql);
$end = time();
try {
$this->query_end($result);
} catch (dml_exception $ex) {
if ($end - $start >= $timeout) {
throw new dml_sessionwait_exception();
} else {
throw $ex;
}
}
if ($result) {
pg_free_result($result);
}
$sql = "SET statement_timeout TO DEFAULT";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = pg_query($this->pgsql, $sql);
$this->query_end($result);

View File

@ -4137,6 +4137,50 @@ class dml_test extends UnitTestCase {
$DB2->dispose();
}
public function test_session_locks() {
$DB = $this->tdb;
$dbman = $DB->get_manager();
// Open second connection
$cfg = $DB->export_dbconfig();
if (!isset($cfg->dboptions)) {
$cfg->dboptions = array();
}
$DB2 = moodle_database::get_driver_instance($cfg->dbtype, $cfg->dblibrary);
$DB2->connect($cfg->dbhost, $cfg->dbuser, $cfg->dbpass, $cfg->dbname, $cfg->prefix, $cfg->dboptions);
// Testing that acquiring a lock efectively locks
// Get a session lock on connection1
$rowid = rand(100, 200);
$timeout = 1;
$DB->get_session_lock($rowid, $timeout);
// Try to get the same session lock on connection2
try {
$DB2->get_session_lock($rowid, $timeout);
$DB2->release_session_lock($rowid); // Should not be excuted, but here for safety
$this->fail('An Exception is missing, expected due to session lock acquired.');
} catch (exception $e) {
$this->assertTrue($e instanceof dml_sessionwait_exception);
$DB->release_session_lock($rowid); // Release lock on connection1
}
// Testing that releasing a lock efectively frees
// Get a session lock on connection1
$rowid = rand(100, 200);
$timeout = 1;
$DB->get_session_lock($rowid, $timeout);
// Release the lock on connection1
$DB->release_session_lock($rowid);
// Get the just released lock on connection2
$DB2->get_session_lock($rowid, $timeout);
// Release the lock on connection2
$DB2->release_session_lock($rowid);
$DB2->dispose();
}
public function test_bound_param_types() {
$DB = $this->tdb;
$dbman = $DB->get_manager();

View File

@ -1286,17 +1286,33 @@ class sqlsrv_native_moodle_database extends moodle_database {
return true;
}
public function get_session_lock($rowid) {
/**
* Obtain session lock
* @param int $rowid id of the row with session record
* @param int $timeout max allowed time to wait for the lock in seconds
* @return bool success
*/
public function get_session_lock($rowid, $timeout) {
if (!$this->session_lock_supported()) {
return;
}
parent::get_session_lock($rowid);
parent::get_session_lock($rowid, $timeout);
$timeoutmilli = $timeout * 1000;
$fullname = $this->dbname.'-'.$this->prefix.'-session-'.$rowid;
$sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', 120000";
$sql = "sp_getapplock '$fullname', 'Exclusive', 'Session', $timeoutmilli";
$this->query_start($sql, null, SQL_QUERY_AUX);
$result = sqlsrv_query($this->sqlsrv, $sql);
$this->query_end($result);
if ($result) {
$row = sqlsrv_fetch_array($result);
if ($row[0] < 0) {
throw new dml_sessionwait_exception();
}
}
$this->free_result($result);
}

View File

@ -78,6 +78,18 @@ class dml_connection_exception extends dml_exception {
}
}
/**
* DML db session wait exception - triggered when session lock request times out.
*/
class dml_sessionwait_exception extends dml_exception {
/**
* Constructor
*/
function __construct() {
parent::__construct('sessionwaiterr');
}
}
/**
* DML read exception - triggered by SQL syntax errors, missing tables, etc.
*/

View File

@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@ -25,6 +24,14 @@
defined('MOODLE_INTERNAL') || die();
if (!defined('SESSION_ACQUIRE_LOCK_TIMEOUT')) {
/**
* How much time to wait for session lock before displaying error (in seconds),
* 2 minutes by default should be a reasonable time before telling users to wait and refresh browser.
*/
define('SESSION_ACQUIRE_LOCK_TIMEOUT', 60*2);
}
/**
* Factory method returning moodle_session object.
* @return moodle_session
@ -39,6 +46,7 @@ function session_get_instance() {
$CFG->sessiontimeout = 7200;
}
try {
if (defined('SESSION_CUSTOM_CLASS')) {
// this is a hook for webservices, key based login, etc.
if (defined('SESSION_CUSTOM_FILE')) {
@ -55,11 +63,17 @@ function session_get_instance() {
// legacy limited file based storage - some features and auth plugins will not work, sorry
$session = new legacy_file_session();
}
} catch (Exception $ex) {
// prevent repeated inits
$session = new emergency_session();
throw $ex;
}
}
return $session;
}
/**
* Moodle session abstraction
*
@ -90,6 +104,53 @@ interface moodle_session {
public function session_exists($sid);
}
/**
* Fallback session handler when standard session init fails.
* This prevents repeated attempts to init faulty handler.
*
* @package core
* @subpackage session
* @copyright 2011 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class emergency_session implements moodle_session {
public function __construct() {
// session not used at all
$_SESSION = array();
$_SESSION['SESSION'] = new stdClass();
$_SESSION['USER'] = new stdClass();
}
/**
* Terminate current session
* @return void
*/
public function terminate_current() {
return;
}
/**
* No more changes in session expected.
* Unblocks the sessions, other scripts may start executing in parallel.
* @return void
*/
public function write_close() {
return;
}
/**
* Check for existing session with id $sid
* @param unknown_type $sid
* @return boolean true if session found.
*/
public function session_exists($sid) {
return false;
}
}
/**
* Class handling all session and cookies related stuff.
*
@ -138,7 +199,8 @@ abstract class session_stub implements moodle_session {
}
/**
* Terminates active moodle session
* Terminate current session
* @return void
*/
public function terminate_current() {
global $CFG, $SESSION, $USER, $DB;
@ -329,11 +391,12 @@ abstract class session_stub implements moodle_session {
}
/**
* Inits session storage.
* Init session storage.
*/
protected abstract function init_session_storage();
}
/**
* Legacy moodle sessions stored in files, not recommended any more.
*
@ -343,6 +406,9 @@ abstract class session_stub implements moodle_session {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class legacy_file_session extends session_stub {
/**
* Init session storage.
*/
protected function init_session_storage() {
global $CFG;
@ -382,6 +448,7 @@ class legacy_file_session extends session_stub {
}
}
/**
* Recommended moodle session storage.
*
@ -391,9 +458,15 @@ class legacy_file_session extends session_stub {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class database_session extends session_stub {
/** @var stdClass $record session record */
protected $record = null;
/** @var moodle_database $database session database */
protected $database = null;
/** @var bool $failed session read/init failed, do not write back to DB */
protected $failed = false;
public function __construct() {
global $DB;
$this->database = $DB;
@ -409,6 +482,11 @@ class database_session extends session_stub {
}
}
/**
* Check for existing session with id $sid
* @param unknown_type $sid
* @return boolean true if session found.
*/
public function session_exists($sid){
global $CFG;
try {
@ -422,6 +500,9 @@ class database_session extends session_stub {
}
}
/**
* Init session storage.
*/
protected function init_session_storage() {
global $CFG;
@ -441,31 +522,57 @@ class database_session extends session_stub {
}
}
/**
* Open session handler
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* @param string $save_path
* @param string $session_name
* @return bool success
*/
public function handler_open($save_path, $session_name) {
return true;
}
/**
* Close session handler
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* @return bool success
*/
public function handler_close() {
if (isset($this->record->id)) {
try {
$this->database->release_session_lock($this->record->id);
} catch (Exception $ex) {
// ignore any problems
}
}
$this->record = null;
return true;
}
/**
* Read session handler
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* @param string $sid
* @return string
*/
public function handler_read($sid) {
global $CFG;
if ($this->record and $this->record->sid != $sid) {
error_log('Weird error reading database session - mismatched sid');
$this->failed = true;
return '';
}
try {
if ($record = $this->database->get_record('sessions', array('sid'=>$sid))) {
$this->database->get_session_lock($record->id);
} else {
if (!$record = $this->database->get_record('sessions', array('sid'=>$sid))) {
$record = new stdClass();
$record->state = 0;
$record->sid = $sid;
@ -474,14 +581,25 @@ class database_session extends session_stub {
$record->timecreated = $record->timemodified = time();
$record->firstip = $record->lastip = getremoteaddr();
$record->id = $this->database->insert_record_raw('sessions', $record);
$this->database->get_session_lock($record->id);
}
} catch (dml_exception $ex) {
} catch (Exception $ex) {
// do not rethrow exceptions here, we need this to work somehow before 1.9.x upgrade and during install
error_log('Can not read or insert database sessions');
$this->failed = true;
return '';
}
try {
$this->database->get_session_lock($record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
} catch (Exception $ex) {
// This is a fatal error, better inform users.
// It should not happen very often - all pages that need long time to execute
// should close session soon after access control checks
error_log('Can not obtain session lock');
$this->failed = true;
throw $ex;
}
// verify timeout
if ($record->timemodified + $CFG->sessiontimeout < time()) {
$ignoretimeout = false;
@ -504,9 +622,11 @@ class database_session extends session_stub {
$record->timemodified = time();
try {
$this->database->update_record('sessions', $record);
} catch (dml_exception $ex) {
} catch (Exception $ex) {
// very unlikely error
error_log('Can not refresh database session');
return '';
$this->failed = true;
throw $ex;
}
} else {
//time out session
@ -517,9 +637,11 @@ class database_session extends session_stub {
$record->firstip = $record->lastip = getremoteaddr();
try {
$this->database->update_record('sessions', $record);
} catch (dml_exception $ex) {
} catch (Exception $ex) {
// very unlikely error
error_log('Can not time out database session');
return '';
$this->failed = true;
throw $ex;
}
}
}
@ -532,11 +654,28 @@ class database_session extends session_stub {
return $data;
}
/**
* Write session handler.
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* NOTE: Do not write to output or throw any exceptions!
* Hopefully the next page is going to display nice error or it recovers...
*
* @param string $sid
* @param string $session_data
* @return bool success
*/
public function handler_write($sid, $session_data) {
global $USER;
// TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
if ($this->failed) {
// do not write anything back - we failed to start the session properly
return false;
}
$userid = 0;
if (!empty($USER->realuser)) {
$userid = $USER->realuser;
@ -563,16 +702,20 @@ class database_session extends session_stub {
try {
$this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
} catch (Exception $ignored) {
}
error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
} else {
error_log('Can not write database session');
}
return false;
} catch (Exception $ex) {
error_log('Can not write database session');
return false;
}
} else {
// session already destroyed
// fresh new session
try {
$record = new stdClass();
$record->state = 0;
$record->sid = $sid;
@ -583,33 +726,56 @@ class database_session extends session_stub {
$record->id = $this->database->insert_record_raw('sessions', $record);
$this->record = $record;
try {
$this->database->get_session_lock($this->record->id);
} catch (dml_exception $ex) {
error_log('Can not write new database session');
$this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
} catch (Exception $ex) {
// this should not happen
error_log('Can not write new database session or acquire session lock');
$this->failed = true;
return false;
}
}
return true;
}
/**
* Destroy session handler
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* @param string $sid
* @return bool success
*/
public function handler_destroy($sid) {
session_kill($sid);
if (isset($this->record->id) and $this->record->sid === $sid) {
try {
$this->database->release_session_lock($this->record->id);
} catch (Exception $ex) {
// ignore problems
}
$this->record = null;
}
return true;
}
/**
* GC session handler
*
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
*
* @param int $ignored_maxlifetime moodle uses special timeout rules
* @return bool success
*/
public function handler_gc($ignored_maxlifetime) {
session_gc();
return true;
}
}
/**
* returns true if legacy session used.
* @return bool true if legacy(==file) based session used