You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

179 lines
7.2 KiB

<?php
/**
* Manage background operations that should be executed at intervals.
*
* This script may be executed by a suitable Ajax request, by a cron job, or both.
*
* When called from cron, optinal args are [site] [service] [force]
* @param site to specify a specific site, 'default' used if omitted
* @param service to specify a specific service, 'all' used if omitted
* @param force '1' to ignore specified wait interval, '0' to honor wait interval
*
* The same parameters can be accessed via Ajax using the $_POST variables
* 'site', 'background_service', and 'background_force', respectively.
*
* For both calling methods, this script guarantees that each active
* background service function: (1) will not be called again before it has completed,
* and (2) will not be called any more frequently than at the specified interval
* (unless the force execution flag is used). A service function that is already running
* will not be called a second time even if the force execution flag is used.
*
* Notes for the default background behavior:
* 1. If the Ajax method is used, services will only be checked while
* Ajax requests are being received, which is currently only when users are
* logged in.
* 2. All services are checked and called sequentially in the order specified
* by the sort_order field in the background_services table. Service calls that are "slow"
* should be given a higher sort_order value.
* 3. The actual interval between two calls to a given background service may be
* as long as the time to complete that service plus the interval between
* n+1 calls to this script where n is the number of other services preceding it
* in the array, even if the specified minimum interval is shorter, so plan
* accordingly. Example: with a 5 min cron interval, the 4th service on the list
* may not be started again for up to 20 minutes after it has completed if
* services 1, 2, and 3 take more than 15, 10, and 5 minutes to complete,
* respectively.
*
* Returns a count of due messages for current user.
*
* @package OpenEMR
* @link https://www.open-emr.org
* @author EMR Direct <https://www.emrdirect.com/>
* @author Brady Miller <brady.g.miller@gmail.com>
* @copyright Copyright (c) 2013 EMR Direct <https://www.emrdirect.com/>
* @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
* @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
*/
use OpenEMR\Common\Csrf\CsrfUtils;
//ajax param should be set by calling ajax scripts
$isAjaxCall = isset($_POST['ajax']);
//if false ajax and this is a called from command line, this is a cron job and set up accordingly
if (!$isAjaxCall && (php_sapi_name() === 'cli')) {
$ignoreAuth = 1;
//process optional arguments when called from cron
$_GET['site'] = $argv[1] ?? 'default';
if (isset($argv[2]) && $argv[2] != 'all') {
$_GET['background_service'] = $argv[2];
}
if (isset($argv[3]) && $argv[3] == '1') {
$_GET['background_force'] = 1;
}
//an additional require file can be specified for each service in the background_services table
// Since from command line, set $sessionAllowWrite since need to set site_id session and no benefit to set to false
$sessionAllowWrite = true;
require_once(__DIR__ . "/../../interface/globals.php");
} else {
//an additional require file can be specified for each service in the background_services table
require_once(__DIR__ . "/../../interface/globals.php");
// not calling from cron job so ensure passes csrf check
if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"])) {
CsrfUtils::csrfNotVerified();
}
}
//Remove time limit so script doesn't time out
set_time_limit(0);
//Safety in case one of the background functions tries to output data
ignore_user_abort(1);
/**
* Execute background services
* This function reads a list of available services from the background_services table
* For each service that is not already running and is due for execution, the associated
* background function is run.
*
* Note: Each service must do its own logging, as appropriate, and should disable itself
* to prevent continued service calls if an error condition occurs which requires
* administrator intervention. Any service function return values and output are ignored.
*/
function execute_background_service_calls()
{
/**
* Note: The global $service_name below is set to the name of the service currently being
* processed before the actual service function call, and is unset after normal
* completion of the loop. If the script exits abnormally, the shutdown_function
* uses the value of $service_name to do any required clean up.
*/
global $service_name;
$single_service = isset($_REQUEST['background_service']) ? $_REQUEST['background_service'] : '';
$force = (isset($_REQUEST['background_force']) && $_REQUEST['background_force']);
$sql = 'SELECT * FROM background_services WHERE ' . ($force ? '1' : 'execute_interval > 0');
if ($single_service != "") {
$services = sqlStatementNoLog($sql . ' AND name=?', array($single_service));
} else {
$services = sqlStatementNoLog($sql . ' ORDER BY sort_order');
}
while ($service = sqlFetchArray($services)) {
$service_name = $service['name'];
if (!$service['active'] || $service['running'] == 1) {
continue;
}
$interval = (int)$service['execute_interval'];
//leverage locking built-in to UPDATE to prevent race conditions
//will need to assess performance in high concurrency setting at some point
$sql = 'UPDATE background_services SET running = 1, next_run = NOW()+ INTERVAL ?'
. ' MINUTE WHERE running < 1 ' . ($force ? '' : 'AND NOW() > next_run ') . 'AND name = ?';
if (sqlStatementNoLog($sql, array($interval,$service_name)) === false) {
continue;
}
$acquiredLock = generic_sql_affected_rows();
if ($acquiredLock < 1) {
continue; //service is already running or not due yet
}
if ($service['require_once']) {
require_once($GLOBALS['fileroot'] . $service['require_once']);
}
if (!function_exists($service['function'])) {
continue;
}
//use try/catch in case service functions throw an unexpected Exception
try {
$service['function']();
} catch (Exception $e) {
//do nothing
}
$sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
$res = sqlStatementNoLog($sql, array($service_name));
}
}
/**
* Catch unexpected failures.
*
* if the global $service_name is still set, then a die() or exit() occurred during the execution
* of that service's function call, and we did not complete the foreach loop properly,
* so we need to reset the is_running flag for that service before quitting
*/
function background_shutdown()
{
global $service_name;
if (isset($service_name)) {
$sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
$res = sqlStatementNoLog($sql, array($service_name));
}
}
register_shutdown_function('background_shutdown');
execute_background_service_calls();
unset($service_name);