3 * Manage background operations that should be executed at intervals.
5 * This script may be executed by a suitable Ajax request, by a cron job, or both.
7 * When called from cron, optinal args are [site] [service] [force]
8 * @param site to specify a specific site, 'default' used if omitted
9 * @param service to specify a specific service, 'all' used if omitted
10 * @param force '1' to ignore specified wait interval, '0' to honor wait interval
12 * The same parameters can be accessed via Ajax using the $_POST variables
13 * 'site', 'background_service', and 'background_force', respectively.
15 * For both calling methods, this script guarantees that each active
16 * background service function: (1) will not be called again before it has completed,
17 * and (2) will not be called any more frequently than at the specified interval
18 * (unless the force execution flag is used). A service function that is already running
19 * will not be called a second time even if the force execution flag is used.
21 * Notes for the default background behavior:
22 * 1. If the Ajax method is used, services will only be checked while
23 * Ajax requests are being received, which is currently only when users are
25 * 2. All services are checked and called sequentially in the order specified
26 * by the sort_order field in the background_services table. Service calls that are "slow"
27 * should be given a higher sort_order value.
28 * 3. The actual interval between two calls to a given background service may be
29 * as long as the time to complete that service plus the interval between
30 * n+1 calls to this script where n is the number of other services preceding it
31 * in the array, even if the specified minimum interval is shorter, so plan
32 * accordingly. Example: with a 5 min cron interval, the 4th service on the list
33 * may not be started again for up to 20 minutes after it has completed if
34 * services 1, 2, and 3 take more than 15, 10, and 5 minutes to complete,
37 * Returns a count of due messages for current user.
40 * @link https://www.open-emr.org
41 * @author EMR Direct <https://www.emrdirect.com/>
42 * @author Brady Miller <brady.g.miller@gmail.com>
43 * @copyright Copyright (c) 2013 EMR Direct <https://www.emrdirect.com/>
44 * @copyright Copyright (c) 2018 Brady Miller <brady.g.miller@gmail.com>
45 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
49 //ajax param should be set by calling ajax scripts
50 $isAjaxCall = isset($_POST['ajax']);
52 //if false ajax and this is a called from command line, this is a cron job and set up accordingly
53 if (!$isAjaxCall && (php_sapi_name() === 'cli')) {
55 //process optional arguments when called from cron
56 $_GET['site'] = (isset($argv[1])) ?
$argv[1] : 'default';
57 if (isset($argv[2]) && $argv[2]!='all') {
58 $_GET['background_service'] = $argv[2];
61 if (isset($argv[3]) && $argv[3]=='1') {
62 $_GET['background_force'] = 1;
65 //an additional require file can be specified for each service in the background_services table
66 require_once(dirname(__FILE__
) . "/../../interface/globals.php");
68 //an additional require file can be specified for each service in the background_services table
69 require_once(dirname(__FILE__
) . "/../../interface/globals.php");
71 // not calling from cron job so ensure passes csrf check
72 if (!verifyCsrfToken($_POST["csrf_token_form"])) {
77 //Remove time limit so script doesn't time out
80 //Release session lock to prevent freezing of other scripts
81 session_write_close();
83 //Safety in case one of the background functions tries to output data
87 * Execute background services
88 * This function reads a list of available services from the background_services table
89 * For each service that is not already running and is due for execution, the associated
90 * background function is run.
92 * Note: Each service must do its own logging, as appropriate, and should disable itself
93 * to prevent continued service calls if an error condition occurs which requires
94 * administrator intervention. Any service function return values and output are ignored.
97 function execute_background_service_calls()
100 * Note: The global $service_name below is set to the name of the service currently being
101 * processed before the actual service function call, and is unset after normal
102 * completion of the loop. If the script exits abnormally, the shutdown_function
103 * uses the value of $service_name to do any required clean up.
105 global $service_name;
107 $single_service = isset($_REQUEST['background_service']) ?
$_REQUEST['background_service'] : '';
108 $force = (isset($_REQUEST['background_force']) && $_REQUEST['background_force']);
110 $sql = 'SELECT * FROM background_services WHERE ' . ($force ?
'1' : 'execute_interval > 0');
111 if ($single_service!="") {
112 $services = sqlStatementNoLog($sql.' AND name=?', array($single_service));
114 $services = sqlStatementNoLog($sql.' ORDER BY sort_order');
117 while ($service = sqlFetchArray($services)) {
118 $service_name = $service['name'];
119 if (!$service['active'] ||
$service['running'] == 1) {
123 $interval=(int)$service['execute_interval'];
125 //leverage locking built-in to UPDATE to prevent race conditions
126 //will need to assess performance in high concurrency setting at some point
127 $sql='UPDATE background_services SET running = 1, next_run = NOW()+ INTERVAL ?'
128 . ' MINUTE WHERE running < 1 ' . ($force ?
'' : 'AND NOW() > next_run ') . 'AND name = ?';
129 if (sqlStatementNoLog($sql, array($interval,$service_name))===false) {
133 $acquiredLock = generic_sql_affected_rows();
134 if ($acquiredLock<1) {
135 continue; //service is already running or not due yet
138 if ($service['require_once']) {
139 require_once($GLOBALS['fileroot'] . $service['require_once']);
142 if (!function_exists($service['function'])) {
146 //use try/catch in case service functions throw an unexpected Exception
148 $service['function']();
149 } catch (Exception
$e) {
153 $sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
154 $res = sqlStatementNoLog($sql, array($service_name));
159 * Catch unexpected failures.
161 * if the global $service_name is still set, then a die() or exit() occurred during the execution
162 * of that service's function call, and we did not complete the foreach loop properly,
163 * so we need to reset the is_running flag for that service before quitting
166 function background_shutdown()
168 global $service_name;
169 if (isset($service_name)) {
170 $sql = 'UPDATE background_services SET running = 0 WHERE name = ?';
171 $res = sqlStatementNoLog($sql, array($service_name));
175 register_shutdown_function('background_shutdown');
176 execute_background_service_calls();
177 unset($service_name);