MDL-64449 core: Added Session lock debugging option

This commit is contained in:
Trisha Milan 2019-04-02 23:17:18 +11:00
parent 280cfdf05c
commit fdb9c3217d
3 changed files with 378 additions and 2 deletions

View File

@ -614,6 +614,12 @@ $CFG->admin = 'admin';
//
// $CFG->expectedcronfrequency = 200;
//
// Session lock warning threshold. Long running pages should release the session using \core\session\manager::write_close().
// Set this threshold to any value greater than 0 to add developer warnings when a page locks the session for too long.
// The session should rarely be locked for more than 1 second. The input should be in seconds and may be a float.
//
// $CFG->debugsessionlock = 5;
//
//=========================================================================
// 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
//=========================================================================

View File

@ -26,6 +26,8 @@ namespace core\session;
defined('MOODLE_INTERNAL') || die();
use html_writer;
/**
* Session manager, this is the public Moodle API for sessions.
*
@ -55,13 +57,15 @@ class manager {
* Note: This is intended to be called only from lib/setup.php!
*/
public static function start() {
global $CFG, $DB;
global $CFG, $DB, $PERF;
if (isset(self::$sessionactive)) {
debugging('Session was already started!', DEBUG_DEVELOPER);
return;
}
// Grab the time before session lock starts.
$PERF->sessionlock['start'] = microtime(true);
self::load_handler();
// Init the session handler only if everything initialised properly in lib/setup.php file
@ -82,6 +86,9 @@ class manager {
throw new \core\session\exception(get_string('servererror'));
}
// Grab the time when session lock starts.
$PERF->sessionlock['gained'] = microtime(true);
$PERF->sessionlock['wait'] = $PERF->sessionlock['gained'] - $PERF->sessionlock['start'];
self::initialise_user_session($isnewsession);
self::$sessionactive = true; // Set here, so the session can be cleared if the security check fails.
self::check_security();
@ -109,6 +116,8 @@ class manager {
* @return array perf info
*/
public static function get_performance_info() {
global $CFG, $PERF;
if (!session_id()) {
return array();
}
@ -119,9 +128,26 @@ class manager {
$info = array();
$info['size'] = $size;
$info['html'] = "<span class=\"sessionsize\">Session ($handler): $size</span> ";
$info['html'] = html_writer::div("Session ($handler): $size", "sessionsize");
$info['txt'] = "Session ($handler): $size ";
if (!empty($CFG->debugsessionlock)) {
$sessionlock = self::get_session_lock_info();
if (!empty($sessionlock['held'])) {
// The page displays the footer and the session has been closed.
$sessionlocktext = "Session lock held: ".number_format($sessionlock['held'], 3)." secs";
} else {
// The session hasn't yet been closed and so we assume now with microtime.
$sessionlockheld = microtime(true) - $PERF->sessionlock['gained'];
$sessionlocktext = "Session lock open: ".number_format($sessionlockheld, 3)." secs";
}
$info['txt'] .= $sessionlocktext;
$info['html'] .= html_writer::div($sessionlocktext, "sessionlockstart");
$sessionlockwaittext = "Session lock wait: ".number_format($sessionlock['wait'], 3)." secs";
$info['txt'] .= $sessionlockwaittext;
$info['html'] .= html_writer::div($sessionlockwaittext, "sessionlockwait");
}
return $info;
}
@ -530,6 +556,19 @@ class manager {
* Unblocks the sessions, other scripts may start executing in parallel.
*/
public static function write_close() {
global $PERF;
if (self::$sessionactive) {
// Grab the time when session lock is released.
$PERF->sessionlock['released'] = microtime(true);
if (!empty($PERF->sessionlock['gained'])) {
$PERF->sessionlock['held'] = $PERF->sessionlock['released'] - $PERF->sessionlock['gained'];
}
$PERF->sessionlock['url'] = me();
self::update_recent_session_locks($PERF->sessionlock);
self::sessionlock_debugging();
}
if (version_compare(PHP_VERSION, '5.6.0', '>=')) {
// More control over whether session data
// is persisted or not.
@ -1024,4 +1063,134 @@ class manager {
}
return true;
}
/**
* Get the recent session locks array.
*
* @return array Recent session locks array.
*/
public static function get_recent_session_locks() {
global $SESSION;
if (!isset($SESSION->recentsessionlocks)) {
// This will hold the pages that blocks other page.
$SESSION->recentsessionlocks = array();
}
return $SESSION->recentsessionlocks;
}
/**
* Updates the recent session locks.
*
* This function will store session lock info of all the pages visited.
*
* @param array $sessionlock Session lock array.
*/
public static function update_recent_session_locks($sessionlock) {
global $CFG, $SESSION;
if (empty($CFG->debugsessionlock)) {
return;
}
$SESSION->recentsessionlocks = self::get_recent_session_locks();
array_push($SESSION->recentsessionlocks, $sessionlock);
self::cleanup_recent_session_locks();
}
/**
* Reset recent session locks array if there is a 10 seconds time gap.
*
* @return array Recent session locks array.
*/
public static function cleanup_recent_session_locks() {
global $SESSION;
$locks = self::get_recent_session_locks();
if (count($locks) > 2) {
for ($i = count($locks) - 1; $i > 0; $i--) {
// Calculate the gap between session locks.
$gap = $locks[$i]['released'] - $locks[$i - 1]['start'];
if ($gap >= 10) {
// Remove previous locks if the gap is 10 seconds or more.
$SESSION->recentsessionlocks = array_slice($locks, $i);
break;
}
}
}
}
/**
* Get the page that blocks other pages at a specific timestamp.
*
* Look for a page whose lock was gained before that timestamp, and released after that timestamp.
*
* @param float $time Time before session lock starts.
* @return array|null
*/
public static function get_locked_page_at($time) {
$recentsessionlocks = self::get_recent_session_locks();
foreach ($recentsessionlocks as $recentsessionlock) {
if ($time >= $recentsessionlock['gained'] &&
$time <= $recentsessionlock['released']) {
return $recentsessionlock;
}
}
}
/**
* Display the page which blocks other pages.
*
* @return string
*/
public static function display_blocking_page() {
global $PERF;
$page = self::get_locked_page_at($PERF->sessionlock['start']);
$output = "Script ".me()." was blocked for ";
$output .= number_format($PERF->sessionlock['wait'], 3);
if ($page != null) {
$output .= " second(s) by script: ";
$output .= $page['url'];
} else {
$output .= " second(s) by an unknown script.";
}
return $output;
}
/**
* Get session lock info of the current page.
*
* @return array
*/
public static function get_session_lock_info() {
global $PERF;
if (!isset($PERF->sessionlock)) {
return null;
}
return $PERF->sessionlock;
}
/**
* Display debugging info about slow and blocked script.
*/
public static function sessionlock_debugging() {
global $CFG, $PERF;
if (!empty($CFG->debugsessionlock)) {
if (isset($PERF->sessionlock['held']) && $PERF->sessionlock['held'] > $CFG->debugsessionlock) {
debugging("Script ".me()." locked the session for ".number_format($PERF->sessionlock['held'], 3)
." seconds, it should close the session using \core\session\manager::write_close().", DEBUG_NORMAL);
}
if (isset($PERF->sessionlock['wait']) && $PERF->sessionlock['wait'] > $CFG->debugsessionlock) {
$output = self::display_blocking_page();
debugging($output, DEBUG_DEVELOPER);
}
}
}
}

View File

@ -653,4 +653,205 @@ class core_session_manager_testcase extends advanced_testcase {
$this->assertEquals($real, $user1);
$this->assertSame($_SESSION['REALUSER'], $real);
}
/**
* Session lock info on pages.
*
* @return array
*/
public function pages_sessionlocks() {
return [
[
'url' => '/good.php',
'start' => 1500000001.000,
'gained' => 1500000002.000,
'released' => 1500000003.000,
'wait' => 1.0,
'held' => 1.0
],
[
'url' => '/bad.php?wait=5',
'start' => 1500000003.000,
'gained' => 1500000005.000,
'released' => 1500000007.000,
'held' => 2.0,
'wait' => 2.0
]
];
}
/**
* Test to get recent session locks.
*/
public function test_get_recent_session_locks() {
global $CFG;
$this->resetAfterTest();
$CFG->debugsessionlock = 5;
$pages = $this->pages_sessionlocks();
// Recent session locks must be empty at first.
$recentsessionlocks = \core\session\manager::get_recent_session_locks();
$this->assertEmpty($recentsessionlocks);
// Add page to the recentsessionlocks array.
\core\session\manager::update_recent_session_locks($pages[0]);
$recentsessionlocks = \core\session\manager::get_recent_session_locks();
// Make sure we are getting the first page we added.
$this->assertEquals($pages[0], $recentsessionlocks[0]);
// There should be 1 page in the array.
$this->assertCount(1, $recentsessionlocks);
// Add second page to the recentsessionlocks array.
\core\session\manager::update_recent_session_locks($pages[1]);
$recentsessionlocks = \core\session\manager::get_recent_session_locks();
// Make sure we are getting the second page we added.
$this->assertEquals($pages[1], $recentsessionlocks[1]);
// There should be 2 pages in the array.
$this->assertCount(2, $recentsessionlocks);
}
/**
* Test to update recent session locks.
*/
public function test_update_recent_session_locks() {
global $CFG;
$this->resetAfterTest();
$CFG->debugsessionlock = 5;
$pages = $this->pages_sessionlocks();
\core\session\manager::update_recent_session_locks($pages[0]);
\core\session\manager::update_recent_session_locks($pages[1]);
$recentsessionlocks = \core\session\manager::get_recent_session_locks();
// There should be 2 pages in the array.
$this->assertCount(2, $recentsessionlocks);
// Make sure the last page is added at the end of the array.
$this->assertEquals($pages[1], end($recentsessionlocks));
}
/**
* Test to get session lock info.
*/
public function test_get_session_lock_info() {
global $PERF;
$this->resetAfterTest();
$pages = $this->pages_sessionlocks();
$PERF->sessionlock = $pages[0];
$sessionlock = \core\session\manager::get_session_lock_info();
$this->assertEquals($pages[0], $sessionlock);
}
/**
* Session lock info on some pages to serve as history.
*
* @return array
*/
public function sessionlock_history() {
return [
[
'url' => '/good.php',
'start' => 1500000001.000,
'gained' => 1500000001.100,
'released' => 1500000001.500,
'wait' => 0.1
],
[
// This bad request doesn't release the session for 10 seconds.
'url' => '/bad.php',
'start' => 1500000012.000,
'gained' => 1500000012.200,
'released' => 1500000020.200,
'wait' => 0.2
],
[
// All subsequent requests are blocked and need to wait.
'url' => '/good.php?id=1',
'start' => 1500000012.900,
'gained' => 1500000020.200,
'released' => 1500000022.000,
'wait' => 7.29
],
[
'url' => '/good.php?id=2',
'start' => 1500000014.000,
'gained' => 1500000022.000,
'released' => 1500000025.000,
'wait' => 8.0
],
[
'url' => '/good.php?id=3',
'start' => 1500000015.000,
'gained' => 1500000025.000,
'released' => 1500000026.000,
'wait' => 10.0
],
[
'url' => '/good.php?id=4',
'start' => 1500000016.000,
'gained' => 1500000026.000,
'released' => 1500000027.000,
'wait' => 10.0
]
];
}
/**
* Data provider for test_get_locked_page_at function.
*
* @return array
*/
public function sessionlocks_info_provider() : array {
return [
[
'url' => null,
'time' => 1500000001.000
],
[
'url' => '/bad.php',
'time' => 1500000014.000
],
[
'url' => '/good.php?id=2',
'time' => 1500000022.500
],
];
}
/**
* Test to get locked page at a speficic timestamp.
*
* @dataProvider sessionlocks_info_provider
* @param array $url Session lock page url.
* @param array $time Session lock time.
*/
public function test_get_locked_page_at($url, $time) {
global $CFG, $SESSION;
$this->resetAfterTest();
$CFG->debugsessionlock = 5;
$SESSION->recentsessionlocks = $this->sessionlock_history();
$page = \core\session\manager::get_locked_page_at($time);
$this->assertEquals($url, $page['url']);
}
/**
* Test cleanup recent session locks.
*/
public function test_cleanup_recent_session_locks() {
global $CFG, $SESSION;
$this->resetAfterTest();
$CFG->debugsessionlock = 5;
$SESSION->recentsessionlocks = $this->sessionlock_history();
$this->assertCount(6, $SESSION->recentsessionlocks);
\core\session\manager::cleanup_recent_session_locks();
// Make sure the session history has been cleaned up and only has the latest page.
$this->assertCount(1, $SESSION->recentsessionlocks);
$this->assertEquals('/good.php?id=4', $SESSION->recentsessionlocks[0]['url']);
}
}