/** * Run scheduled tasks according to a cron spec. */ function elis_cron() { global $CFG; require $CFG->dirroot . '/elis/core/lib/tasklib.php'; $timenow = time(); // get all tasks that are (over-)due $tasks = get_recordset_select('elis_scheduled_tasks', 'nextruntime <= ' . $timenow, 'nextruntime ASC'); if (empty($tasks)) { return; } while ($task = rs_fetch_next_record($tasks)) { $starttime = microtime(); mtrace("Running {$task->callfunction}({$task->taskname}) from {$task->plugin}..."); if ($task->enddate !== null && $task->enddate < $timenow) { mtrace('* Cancelling task: past end date'); delete_records('elis_scheduled_tasks', 'id', $task->id); continue; } // FIXME: check for blocking tasks // FIXME: check if task is locked // See if some other cron has already run the function while we were // doing something else -- if so, skip it. $nextrun = get_field('elis_scheduled_tasks', 'nextruntime', 'id', $task->id); if ($nextrun > $timenow) { mtrace('* Skipped (someone else already ran it)'); continue; } // calculate the next run time $newtask = new stdClass(); $newtask->id = $task->id; $newtask->lastruntime = time(); $newtask->nextruntime = cron_next_run_time($newtask->lastruntime, (array) $task); // see if we have any runs left if ($task->runsremaining !== null) { $newtask->runsremaining = $task->runsremaining - 1; if ($newtask->runsremaining <= 0) { mtrace('* Cancelling task: no runs left'); delete_records('elis_scheduled_tasks', 'id', $task->id); } else { update_record('elis_scheduled_tasks', $newtask); } } else { update_record('elis_scheduled_tasks', $newtask); } // load the file and call the function if ($task->callfile) { $callfile = $CFG->dirroot . $task->callfile; if (!is_readable($callfile)) { mtrace('* Skipped (file not found)'); continue; } require_once $callfile; } call_user_func(unserialize($task->callfunction), $task->taskname); $difftime = microtime_diff($starttime, microtime()); mtrace("* {$difftime} seconds"); } }
if ($nextrun != $job->nextrun) { log_info("Too late to run core {$job->callfunction}; skipping."); cron_free($job, $start); continue; } log_info("Running core cron " . $job->callfunction); $function = $job->callfunction; try { $function(); } catch (Exception $e) { log_message($e->getMessage(), LOG_LEVEL_WARN, true, true, $e->getFile(), $e->getLine(), $e->getTrace()); $output = $e instanceof MaharaException ? $e->render_exception() : $e->getMessage(); echo "{$output}\n"; // Don't call handle_exception; try to update next run time and free the lock } $nextrun = cron_next_run_time($start, (array) $job); // update next run time set_field('cron', 'nextrun', db_format_timestamp($nextrun), 'id', $job->id); cron_free($job, $start); $now = $fake ? time() - ($realstart - $start) : time(); } } $finish = time(); //Time relative to fake cron time if (isset($argv[1])) { $diff = $realstart - $start; $finish = $finish - $diff; } log_info('---------- cron finished ' . date('r', $finish) . ' ----------'); function cron_next_run_time($lastrun, $job) {
/** * Validate that scheduled import is prevented if existing incomplete run exists. */ public function test_importpreventmultipleimports() { global $CFG, $DB; require_once $CFG->dirroot . '/local/datahub/lib.php'; require_once $CFG->dirroot . '/local/eliscore/lib/tasklib.php'; $DB->delete_records('local_eliscore_sched_tasks'); $DB->delete_records(RLIP_SCHEDULE_TABLE); $filepath = '/local_datahub_phpunit/'; $filename = 'userfile2.csv'; set_config('schedule_files_path', $filepath, 'dhimport_version1elis'); set_config('user_schedule_file', $filename, 'dhimport_version1elis'); // Set up the test directory. $testdir = $CFG->dataroot . $filepath; mkdir($testdir, 0777, true); copy(dirname(__FILE__) . "/fixtures/{$filename}", $testdir . $filename); // Create the job. $data = array('plugin' => 'dhimport_version1elis', 'period' => '5m', 'type' => 'dhimport'); $taskid1 = rlip_schedule_add_job($data); // Run the import with a time in the past so it stops immediately. $taskname1 = $DB->get_field('local_eliscore_sched_tasks', 'taskname', array('id' => $taskid1)); $result1 = run_ipjob($taskname1, -1); // Validate the first import run was started. $this->assertEquals(true, $result1); // Validate the state was saved. $config = $DB->get_field(RLIP_SCHEDULE_TABLE, 'config', array('id' => 1)); $this->assertRegExp('/s:5:"state";/', $config); // Create a duplicate job. $taskid2 = rlip_schedule_add_job($data); // Get the initial duplicate job lastruntime and nextruntime values. $initlastruntime = $DB->get_field('local_eliscore_sched_tasks', 'lastruntime', array('id' => $taskid2)); $initnextruntime = $DB->get_field('local_eliscore_sched_tasks', 'nextruntime', array('id' => $taskid2)); // Emulate the ELIS cron adjusting the job run times. $task = $DB->get_record('local_eliscore_sched_tasks', array('id' => $taskid2)); $task->lastruntime = time(); $nextruntime = cron_next_run_time($task->lastruntime, (array) $task); $task->nextruntime = $nextruntime; $DB->update_record('local_eliscore_sched_tasks', $task); // Attempt to do another import run. $taskname2 = $DB->get_field('local_eliscore_sched_tasks', 'taskname', array('id' => $taskid2)); $result2 = run_ipjob($taskname2); // Validate that the second import run attempt fails. $this->assertEquals(false, $result2); // Get the later lastruntime and nextruntime values. $lastruntime = $DB->get_field('local_eliscore_sched_tasks', 'lastruntime', array('id' => $taskid2)); $nextruntime = $DB->get_field('local_eliscore_sched_tasks', 'nextruntime', array('id' => $taskid2)); // Validate that the job run time values are back to initial values. $this->assertEquals($initlastruntime, $lastruntime); $this->assertEquals($initnextruntime, $nextruntime); }
/** * Run scheduled tasks according to a cron spec. * * uses $CFG, $DB */ function local_eliscore_cron() { global $CFG, $DB; require $CFG->dirroot . '/local/eliscore/lib/tasklib.php'; $timenow = time(); // get all tasks that are (over-)due $params = array('timenow' => $timenow); $tasks = $DB->get_recordset_select('local_eliscore_sched_tasks', 'nextruntime <= :timenow', $params, 'nextruntime ASC'); $numtasks = $DB->count_records_select('local_eliscore_sched_tasks', 'nextruntime <= :timenow', $params); // Check if the maximum cron run time is overridden $remtime = ELIS_TASKS_CRONSECS; if (isset($CFG->elistaskscronsecs) && is_int($CFG->elistaskscronsecs) && 0 < $CFG->elistaskscronsecs) { $remtime = $CFG->elistaskscronsecs; } if (empty($tasks) || !$tasks->valid()) { return; } foreach ($tasks as $task) { $starttime = microtime(); mtrace("Running {$task->callfunction}({$task->taskname}) from {$task->plugin}..."); if ($task->enddate !== null && $task->enddate < $timenow) { mtrace('* Cancelling task: past end date'); $DB->delete_records('local_eliscore_sched_tasks', array('id' => $task->id)); --$numtasks; continue; } // Check for blocking tasks. if (!empty($task->blocked) && $timenow < $task->blocked) { // Task is still running - do not start another instance of it. mtrace("{$task->plugin}: Previous {$task->taskname} process has not yet completed - aborting!"); continue; } // FIXME: check if task is locked // See if some other cron has already run the function while we were // doing something else -- if so, skip it. $nextrun = $DB->get_field('local_eliscore_sched_tasks', 'nextruntime', array('id' => $task->id)); if ($nextrun > $timenow) { mtrace('* Skipped (someone else already ran it)'); --$numtasks; continue; } // calculate the next run time $newtask = new stdClass(); $newtask->id = $task->id; $newtask->lastruntime = time(); $newtask->nextruntime = cron_next_run_time($newtask->lastruntime, (array) $task); // see if we have any runs left if ($task->runsremaining !== null) { $newtask->runsremaining = $task->runsremaining - 1; if ($newtask->runsremaining <= 0) { mtrace('* Cancelling task: no runs left'); $DB->delete_records('local_eliscore_sched_tasks', array('id' => $task->id)); } else { $DB->update_record('local_eliscore_sched_tasks', $newtask); } } else { $DB->update_record('local_eliscore_sched_tasks', $newtask); } // load the file and call the function if ($task->callfile) { $callfile = $CFG->dirroot . $task->callfile; if (!is_readable($callfile)) { mtrace('* Skipped (file not found)'); --$numtasks; continue; } require_once $callfile; } $starttask = time(); $denom = $numtasks > 0 ? $numtasks-- : 1; // prevent div by 0 $runtime = floor((double) $remtime / (double) $denom); call_user_func(unserialize($task->callfunction), $task->taskname, $runtime); $remtime -= time() - $starttask; $difftime = microtime_diff($starttime, microtime()); mtrace("* {$difftime} seconds"); // TBD: exit if over cron processing time if ($remtime <= 0) { break; } } }
/** * Validate cron_next_run_time() function for tasks with '00' hour and/or minute * @param array $job array of cron task parameters * @param array $expnextrun the expected next run time * @dataProvider elis_tasks_cron_next_run_time_data */ public function test_elis_tasks_cron_next_run_time($job, $expnextrun) { $lastrun = $job['lastruntime']; $this->assertEquals(make_timestamp($expnextrun[0], $expnextrun[1], $expnextrun[2], $expnextrun[3], $expnextrun[4]), cron_next_run_time(make_timestamp($lastrun[0], $lastrun[1], $lastrun[2], $lastrun[3], $lastrun[4]), $job)); }
public function finish() { global $USER, $DB; $data = $this->unserialize_data(array()); if (isset($data['schedule_user_id'])) { //userid was specifically persisted from the schedule record $userid = $data['schedule_user_id']; } else { //default to the current user $userid = $USER->id; } // Add timemodified to serialized data $data['timemodified'] = time(); $serialized_data = serialize($data); // Save to php_report_schedule - id (auto), userid (Moodle userid), report (shortname), config($data plus time() <= currenttime) $schedule = new object(); $schedule->userid = $userid; $schedule->report = $data['report']; $schedule->config = $serialized_data; if (isset($data['schedule_id'])) { $schedule->id = $data['schedule_id']; $DB->update_record('local_elisreports_schedule', $schedule); // Also find and remove any existing task record(s) for the old schedule $taskname = 'scheduled_' . $schedule->id; $DB->delete_records('local_eliscore_sched_tasks', array('taskname' => $taskname)); } else { $schedule->id = $DB->insert_record('local_elisreports_schedule', $schedule); } // Save to scheduled_tasks $component = 'local_elisreports'; $callfile = '/local/elisreports/runschedule.php'; $callfunction = serialize('run_schedule'); // Calculate nextruntime from schedule // Also calculate minute/hour/day/month/dayofweek $recurrencetype = $data['recurrencetype']; // Simple - hour/minute are from time modified if ($recurrencetype == scheduling_workflow::RECURRENCE_SIMPLE) { $minute = (int) strftime('%M', $data['timemodified']); $hour = (int) strftime('%H', $data['timemodified']); $day = '*'; $month = '*'; $dayofweek = '*'; } else { // Calendar $minute = $data['schedule']['minute']; $hour = $data['schedule']['hour']; $day = $data['schedule']['day']; $month = $data['schedule']['month']; $dayofweek = $data['schedule']['dayofweek']; } $runsremaining = empty($data['schedule']['runsremaining']) ? null : $data['schedule']['runsremaining']; // thou startdate is checked in runschedule.php, the confirm form // would display an incorrect 'will run next at' time (eg. current time) // if we don't calculate it - see below ... $startdate = empty($data['startdate']) ? $data['timemodified'] : $data['startdate']; $enddate = empty($data['schedule']['enddate']) ? null : $data['schedule']['enddate']; $task = new object(); $task->plugin = $component; $task->taskname = 'scheduled_' . $schedule->id; $task->callfile = $callfile; $task->callfunction = $callfunction; $task->lastruntime = 0; $task->blocking = 0; $task->minute = $minute; $task->hour = $hour; $task->day = $day; $task->month = $month; $task->dayofweek = $dayofweek; $task->timezone = $data['timezone']; $task->enddate = $enddate != null ? $enddate + DAYSECS - 1 : null; debug_error_log("schedulelib.php::finish() startdate = {$data['startdate']}; timemodified = {$data['timemodified']}"); // NOTE: if startdate not set then it already got set to time() // in get_submitted_values_for_step_schedule() // in which case we DO NOT want to add current time of day! // hence the messy check for: !(from_gmt() % DAYSECS) if (!empty($data['startdate']) && ($recurrencetype == scheduling_workflow::RECURRENCE_SIMPLE || $startdate < $data['timemodified']) && !(($orig_start = from_gmt($data['startdate'], $data['timezone'])) % DAYSECS)) { // they set a startdate, but, we should add current time of day! $time_offset = from_gmt($data['timemodified'], $data['timezone']); debug_error_log("schedulelib.php::finish() : time_offset = {$time_offset} - adjusting startdate = {$startdate} ({$orig_start}) => " . to_gmt($orig_start + $time_offset % DAYSECS, $data['timezone'])); $startdate = to_gmt($orig_start + $time_offset % DAYSECS, $data['timezone']); /** * TBD: if startdate earlier than today ??? **/ while ($startdate < $data['timemodified']) { $startdate += DAYSECS; debug_error_log("schedulelib.php::finish() advancing startdate + day => {$startdate}"); } } if ($recurrencetype == scheduling_workflow::RECURRENCE_SIMPLE) { $task->nextruntime = $startdate; } else { $task->nextruntime = cron_next_run_time($startdate - 100, (array) $task); // minus [arb. value] above from startdate required to not skip // first run! This was probably due to incorrect startdate calc // which is now corrected. } $task->runsremaining = $runsremaining; $DB->insert_record('local_eliscore_sched_tasks', $task); }
/** * We can not removed all event handlers in table, then add them again * because event handlers could be referenced by queued items * * Note that the absence of the db/events.php event definition file * will cause any queued events for the component to be removed from * the database. * * @param $component - examples: 'moodle', 'mod/forum', 'block/quiz_results' * @return boolean */ function elis_tasks_update_definition($component = 'moodle') { // load event definition from events.php $filetasks = elis_tasks_load_def($component); // load event definitions from db tables // if we detect an event being already stored, we discard from this array later // the remaining needs to be removed $cachedtasks = elis_tasks_get_cached($component); foreach ($filetasks as $filetask) { // preprocess file task $filetask['blocking'] = empty($filetask['blocking']) ? 0 : 1; $filetask['minute'] = empty($filetask['minute']) ? '*' : $filetask['minute']; $filetask['hour'] = empty($filetask['hour']) ? '*' : $filetask['hour']; $filetask['day'] = empty($filetask['day']) ? '*' : $filetask['day']; $filetask['month'] = empty($filetask['month']) ? '*' : $filetask['month']; $filetask['dayofweek'] = empty($filetask['dayofweek']) ? '*' : $filetask['dayofweek']; $callfunction = serialize($filetask['callfunction']); if (!empty($cachedtasks[$callfunction])) { $cachedtask =& $cachedtasks[$callfunction]; if ($cachedtask['customized']) { // task is customized by the administrator, so don't change it unset($cachedtask[$callfunction]); continue; } if ($cachedtask['callfile'] == $filetask['callfile'] && $cachedtask['blocking'] == $filetask['blocking'] && $cachedtask['minute'] == $filetask['minute'] && $cachedtask['hour'] == $filetask['hour'] && $cachedtask['day'] == $filetask['day'] && $cachedtask['month'] == $filetask['month'] && $cachedtask['dayofweek'] == $filetask['dayofweek']) { // exact same task already present in db, ignore this entry unset($cachedtasks[$callfunction]); continue; } else { // same task matches, this task has been updated, update the datebase $task = new object(); $task->id = $cachedtask['id']; $task->callfile = $filetask['callfile']; $task->callfunction = $callfunction; $task->blocking = $filetask['blocking']; $task->minute = $filetask['minute']; $task->hour = $filetask['hour']; $task->day = $filetask['day']; $task->month = $filetask['month']; $task->dayofweek = $filetask['dayofweek']; $task = addslashes_recursive($task); update_record('elis_scheduled_tasks', $task); unset($cachedtasks[$callfunction]); continue; } } else { // if we are here, this event handler is not present in db (new) // add it $task = new object(); $task->plugin = $component; $task->callfile = $filetask['callfile']; $task->callfunction = $callfunction; $task->blocking = $filetask['blocking']; $task->minute = $filetask['minute']; $task->hour = $filetask['hour']; $task->day = $filetask['day']; $task->month = $filetask['month']; $task->dayofweek = $filetask['dayofweek']; $task->timezone = 99; $task->nextruntime = cron_next_run_time(time(), (array) $task); $task = addslashes_recursive($task); insert_record('elis_scheduled_tasks', $task); } } // clean up the left overs, the entries in cachedtasks array at this points are deprecated event handlers // and should be removed, delete from db elis_tasks_cleanup($component, $cachedtasks); return true; }
foreach ($jobs as $job) { log_debug("Running core cron " . $job->callfunction); $function = $job->callfunction; $function(); $nextrun = cron_next_run_time($now, (array) $job); // update next run time set_field('cron', 'nextrun', db_format_timestamp($nextrun), 'id', $job->id); } } // and missed ones... if ($jobs = get_records_select_array('cron', 'nextrun < ? OR nextrun IS NULL', array(db_format_timestamp($now - MAXRUNAGE)))) { foreach ($jobs as $job) { if ($job->nextrun) { log_warn('core cronjob "' . $job->callfunction . '" didn\'t get run because the nextrun time was too old'); } $nextrun = cron_next_run_time($now, (array) $job); // update next run time set_field('cron', 'nextrun', db_format_timestamp($nextrun), 'id', $job->id); } } function cron_next_run_time($lastrun, $job) { $run_date = getdate($lastrun); // we don't care about seconds for cron $run_date['seconds'] = 0; // assert valid month if (!cron_valid_month($job, $run_date)) { cron_next_month($job, $run_date); cron_first_day($job, $run_date); cron_first_hour($job, $run_date); cron_first_minute($job, $run_date);