From 29e3a223f50677d8b7425a4b47d5cbbcaafc8751 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Tue, 30 Jul 2019 23:58:29 +1000 Subject: [PATCH 1/3] MDL-59594 core: Allow custom signal handlers --- lib/classes/shutdown_manager.php | 67 ++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/lib/classes/shutdown_manager.php b/lib/classes/shutdown_manager.php index 9475c92c43be7..101b4a1417bff 100644 --- a/lib/classes/shutdown_manager.php +++ b/lib/classes/shutdown_manager.php @@ -33,7 +33,9 @@ */ class core_shutdown_manager { /** @var array list of custom callbacks */ - protected static $callbacks = array(); + protected static $callbacks = []; + /** @var array list of custom signal callbacks */ + protected static $signalcallbacks = []; /** @var bool is this manager already registered? */ protected static $registered = false; @@ -66,7 +68,7 @@ public static function initialize() { * * @param int $signo The signal being handled */ - public static function signal_handler($signo) { + public static function signal_handler(int $signo) { // Note: There is no need to manually call the shutdown handler. // The fact that we are calling exit() in this script means that the standard shutdown handling is performed // anyway. @@ -92,7 +94,41 @@ public static function signal_handler($signo) { $exitcode = 1; } - exit ($exitcode); + // Normally we should exit unless a callback tells us to wait. + $shouldexit = true; + foreach (self::$signalcallbacks as $data) { + list($callback, $params) = $data; + try { + array_unshift($params, $signo); + $shouldexit = call_user_func_array($callback, $params) && $shouldexit; + } catch (Throwable $e) { + // @codingStandardsIgnoreStart + error_log('Exception ignored in signal function ' . get_callable_name($callback) . ': ' . $e->getMessage()); + // @codingStandardsIgnoreEnd + } + } + + if ($shouldexit) { + exit ($exitcode); + } + } + + /** + * Register custom signal handler function. + * + * If a handler returns false the signal will be ignored. + * + * @param callable $callback + * @param array $params + * @return void + */ + public static function register_signal_handler($callback, array $params = null): void { + if (!is_callable($callback)) { + // @codingStandardsIgnoreStart + error_log('Invalid custom signal function detected ' . var_export($callback, true)); + // @codingStandardsIgnoreEnd + } + self::$signalcallbacks[] = [$callback, $params ?? []]; } /** @@ -100,9 +136,15 @@ public static function signal_handler($signo) { * * @param callable $callback * @param array $params + * @return void */ - public static function register_function($callback, array $params = null) { - self::$callbacks[] = array($callback, $params); + public static function register_function($callback, array $params = null): void { + if (!is_callable($callback)) { + // @codingStandardsIgnoreStart + error_log('Invalid custom shutdown function detected '.var_export($callback, true)); + // @codingStandardsIgnoreEnd + } + self::$callbacks[] = [$callback, $params ?? []]; } /** @@ -115,20 +157,11 @@ public static function shutdown_handler() { foreach (self::$callbacks as $data) { list($callback, $params) = $data; try { - if (!is_callable($callback)) { - error_log('Invalid custom shutdown function detected '.var_export($callback, true)); - continue; - } - if ($params === null) { - call_user_func($callback); - } else { - call_user_func_array($callback, $params); - } - } catch (Exception $e) { - error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage()); + call_user_func_array($callback, $params); } catch (Throwable $e) { - // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5). + // @codingStandardsIgnoreStart error_log('Exception ignored in shutdown function '.get_callable_name($callback).': '.$e->getMessage()); + // @codingStandardsIgnoreEnd } } From 176b5202e0f367d467c7c4f1706418407e08c50a Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Wed, 31 Jul 2019 18:07:39 +1000 Subject: [PATCH 2/3] MDL-59594 cli: Introduce cli helpers for graceful exits \core\local\cli\shutdown::script_supports_graceful_exit(); Sets up interception of exit signals and keep the script running. \core\local\cli\shutdown::should_gracefully_exit(); Use this to check whether you should exit, often inside a long running loop. --- lang/en/admin.php | 2 + lib/classes/local/cli/shutdown.php | 81 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 lib/classes/local/cli/shutdown.php diff --git a/lang/en/admin.php b/lang/en/admin.php index d78836a471b11..ef9c31979df3a 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -120,6 +120,8 @@ $string['cleanup'] = 'Cleanup'; $string['clianswerno'] = 'n'; $string['cliansweryes'] = 'y'; +$string['cliexitgraceful'] = 'Exiting gracefully, please wait ...'; +$string['cliexitnow'] = 'Exiting right NOW'; $string['cliincorrectvalueerror'] = 'Error, incorrect value "{$a->value}" for "{$a->option}"'; $string['cliincorrectvalueretry'] = 'Incorrect value, please retry'; $string['clistatusdisabled'] = 'Status: disabled'; diff --git a/lib/classes/local/cli/shutdown.php b/lib/classes/local/cli/shutdown.php new file mode 100644 index 0000000000000..93f295cfd110c --- /dev/null +++ b/lib/classes/local/cli/shutdown.php @@ -0,0 +1,81 @@ +. + +/** + * CLI script shutdown helper class. + * + * @package core + * @copyright 2019 Brendan Heywood + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\local\cli; + +defined('MOODLE_INTERNAL') || die(); + +/** + * CLI script shutdown helper class. + * + * @package core + * @copyright 2019 Brendan Heywood + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class shutdown { + + /** @var bool Should we exit gracefully at the next opportunity? */ + protected static $cligracefulexit = false; + + /** + * Declares that this CLI script can gracefully handle signals + * + * @return void + */ + public static function script_supports_graceful_exit(): void { + \core_shutdown_manager::register_signal_handler('\core\local\cli\shutdown::signal_handler'); + } + + /** + * Should we gracefully exit? + * + * @return bool true if we should gracefully exit + */ + public static function should_gracefully_exit(): bool { + return self::$cligracefulexit; + } + + /** + * Handle the signal + * + * The first signal flags a graceful exit. If a second signal is received + * then it immediately exits. + * + * @param int $signo The signal number + * @return bool true if we should exit + */ + public static function signal_handler(int $signo): bool { + + if (self::$cligracefulexit) { + cli_heading(get_string('cliexitnow', 'admin')); + return true; + } + + cli_heading(get_string('cliexitgraceful', 'admin')); + self::$cligracefulexit = true; + return false; + } + +} + From b15c53f4fa012f2c505e1e30dc53a5b44cc2ed89 Mon Sep 17 00:00:00 2001 From: Brendan Heywood Date: Mon, 30 Dec 2019 12:51:14 +1100 Subject: [PATCH 3/3] MDL-59594 cron: Allow graceful exit of cron and adhoc task cli's --- admin/cli/cron.php | 2 ++ admin/tool/task/cli/adhoc_task.php | 2 ++ lib/cronlib.php | 6 ++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/admin/cli/cron.php b/admin/cli/cron.php index d7d16461dbe32..fe726830117cb 100644 --- a/admin/cli/cron.php +++ b/admin/cli/cron.php @@ -74,4 +74,6 @@ die; } +\core\local\cli\shutdown::script_supports_graceful_exit(); + cron_run(); diff --git a/admin/tool/task/cli/adhoc_task.php b/admin/tool/task/cli/adhoc_task.php index 755026b90fba1..1a04ba8bfdf8c 100644 --- a/admin/tool/task/cli/adhoc_task.php +++ b/admin/tool/task/cli/adhoc_task.php @@ -115,5 +115,7 @@ $humantimenow = date('r', time()); $keepalive = (int)$options['keep-alive']; +\core\local\cli\shutdown::script_supports_graceful_exit(); + mtrace("Server Time: {$humantimenow}\n"); cron_run_adhoc_tasks(time(), $keepalive, $checklimits); diff --git a/lib/cronlib.php b/lib/cronlib.php index 0adbcc91bc54f..f15fae8dc85d9 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -114,7 +114,8 @@ function cron_run_scheduled_tasks(int $timenow) { // Run all scheduled tasks. try { - while (!\core\task\manager::static_caches_cleared_since($timenow) && + while (!\core\local\cli\shutdown::should_gracefully_exit() && + !\core\task\manager::static_caches_cleared_since($timenow) && $task = \core\task\manager::get_next_scheduled_task($timenow)) { cron_run_inner_scheduled_task($task); unset($task); @@ -167,7 +168,8 @@ function cron_run_adhoc_tasks(int $timenow, $keepalive = 0, $checklimits = true) $taskcount = 0; // Run all adhoc tasks. - while (!\core\task\manager::static_caches_cleared_since($timenow)) { + while (!\core\local\cli\shutdown::should_gracefully_exit() && + !\core\task\manager::static_caches_cleared_since($timenow)) { if ($checklimits && (time() - $timenow) >= $maxruntime) { if ($waiting) {