aboutsummaryrefslogtreecommitdiff
path: root/lib/models/CronjobSchedule.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/models/CronjobSchedule.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/models/CronjobSchedule.php')
-rw-r--r--lib/models/CronjobSchedule.php272
1 files changed, 272 insertions, 0 deletions
diff --git a/lib/models/CronjobSchedule.php b/lib/models/CronjobSchedule.php
new file mode 100644
index 0000000..08a18d3
--- /dev/null
+++ b/lib/models/CronjobSchedule.php
@@ -0,0 +1,272 @@
+<?php
+// +---------------------------------------------------------------------------+
+// This file is part of Stud.IP
+// CronjobSchedule.php
+//
+// Copyright (C) 2013 Jan-Hendrik Willms <tleilax+studip@gmail.com>
+// +---------------------------------------------------------------------------+
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or any later version.
+// +---------------------------------------------------------------------------+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+// +---------------------------------------------------------------------------+
+
+/**
+ * CronjobSchedule - Model for the database table "cronjobs_schedules"
+ *
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2
+ * @category Stud.IP
+ * @since 2.4
+ *
+ * @property string $id alias column for schedule_id
+ * @property string $schedule_id database column
+ * @property string $task_id database column
+ * @property int $active database column
+ * @property string|null $title database column
+ * @property string|null $description database column
+ * @property string|null $parameters database column
+ * @property int|null $minute database column
+ * @property int|null $hour database column
+ * @property int|null $day database column
+ * @property int|null $month database column
+ * @property int|null $day_of_week database column
+ * @property int $next_execution database column
+ * @property int|null $last_execution database column
+ * @property string|null $last_result database column
+ * @property int $execution_count database column
+ * @property int $mkdate database column
+ * @property int $chdate database column
+ * @property SimpleORMapCollection|CronjobLog[] $logs has_many CronjobLog
+ * @property CronjobTask $task belongs_to CronjobTask
+ */
+
+class CronjobSchedule extends SimpleORMap
+{
+ protected static function configure($config = [])
+ {
+ $config['db_table'] = 'cronjobs_schedules';
+
+ $config['belongs_to']['task'] = [
+ 'class_name' => CronjobTask::class,
+ 'foreign_key' => 'task_id',
+ ];
+ $config['has_many']['logs'] = [
+ 'class_name' => CronjobLog::class,
+ 'on_delete' => 'delete',
+ 'on_store' => 'store',
+ ];
+
+ $config['registered_callbacks']['before_store'][] = 'cbJsonifyParameters';
+ $config['registered_callbacks']['after_store'][] = 'cbJsonifyParameters';
+ $config['registered_callbacks']['after_initialize'][] = 'cbJsonifyParameters';
+
+ parent::configure($config);
+ }
+
+ /**
+ * replaces title with task name if title is empty.
+ *
+ * @return string the title or the task name
+ */
+ public function getTitle()
+ {
+ return $this->content['title'] ?: $this->task->name ?? '';
+ }
+
+ protected function cbJsonifyParameters($type)
+ {
+ if ($type === 'before_store' && !is_string($this->parameters)) {
+ $this->parameters = json_encode($this->parameters ?: null);
+ }
+ if (in_array($type, ['after_initialize', 'after_store']) && is_string($this->parameters)) {
+ $parameters = json_decode($this->parameters, true) ?: [];
+ if ($this->task && $this->task->valid) {
+ $default_parameters = $this->task->extractDefaultParameters();
+ foreach ($default_parameters as $key => $value) {
+ if (!isset($parameters[$key])) {
+ $parameters[$key] = $value;
+ }
+ }
+ }
+ $this->parameters = $parameters;
+ }
+ }
+
+ /**
+ * Stores the schedule in database. Will bail out with an exception if
+ * the provided task does not exists. Will also nullify the title if it
+ * matches the task name (see CronjobSchedule::getTitle()).
+ *
+ * @return CronjobSchedule Returns itself to allow chaining
+ */
+ public function store()
+ {
+ if ($this->task === null) {
+ $message = sprintf('A task with the id "%s" does not exist.', $this->task_id);
+ throw new InvalidArgumentException($message);
+ }
+
+ // Remove title if it is the default (task's name)
+ if ($this->title === $this->task->name) {
+ $this->title = null;
+ }
+
+ parent::store();
+
+ return $this;
+ }
+
+ /**
+ * Activates this schedule.
+ *
+ * @return CronjobSchedule Returns itself to allow chaining
+ */
+ public function activate(bool $run_immediately = false)
+ {
+ $next_execution = $run_immediately ? strtotime('-1 minute') : $this->calculateNextExecution();
+
+ $this->active = true;
+ $this->next_execution = $next_execution;
+ $this->store();
+
+ return $this;
+ }
+
+ /**
+ * Deactivates this schedule.
+ *
+ * @return CronjobSchedule Returns itself to allow chaining
+ */
+ public function deactivate()
+ {
+ $this->active = false;
+ $this->store();
+
+ return $this;
+ }
+
+ /**
+ * Executes this schedule.
+ *
+ * @param bool $force Pass true to force execution of the schedule even
+ * if it's not activated
+ * @return mixed The result of the execution
+ * @throws RuntimeException When either the schedule or the according is
+ * not activated
+ */
+ public function execute($force = false)
+ {
+ if (!$force && !$this->active) {
+ throw new RuntimeException('Execution aborted. Schedule is not active');
+ }
+ if (!$this->task->active) {
+ throw new RuntimeException('Execution aborted. Associated task is not active');
+ }
+
+ $this->last_execution = time();
+ $this->execution_count += 1;
+ $this->next_execution = $this->calculateNextExecution();
+ $this->store();
+
+ $this->task->execution_count += 1;
+ $this->task->store();
+
+ $result = $this->task->engage($this->last_result, $this->parameters);
+
+ $this->last_result = $result;
+ $this->store();
+
+ return $result;
+ }
+
+ /**
+ * Determines whether the schedule should execute given the provided
+ * timestamp.
+ *
+ * @param mixed $now Defines the temporal fix point
+ * @return bool Whether the schedule should execute or not.
+ */
+ public function shouldExecute($now = null)
+ {
+ return ($now ?: time()) >= $this->next_execution;
+ }
+
+ /**
+ * Calculates the next execution for this schedule.
+ *
+ * The next execution is calculated by increasing the current timestamp
+ * and testing whether all conditions match. This is not the best method
+ * to test and should be optimized sooner or later.
+ *
+ * @param mixed $now Defines the temporal fix point
+ *
+ * @return int Timestamp of calculated next execution
+ * @throws RuntimeException When calculation takes too long (you should
+ * check the conditions for validity in that case)
+ */
+ public function calculateNextExecution($now = null)
+ {
+ $now = $now ?: time();
+
+ $result = $now;
+ $result -= $result % 60;
+
+ $i = 366 * 24 * 60; // Maximum: A year
+ $offset = 60;
+
+ do {
+ $result += $offset;
+
+ // TODO: Performance - Adjust result according to conditions
+ // See http://coderzone.org/library/PHP-PHP-Cron-Parser-Class_1084.htm
+ $valid = $this->testTimestamp($result, $this->minute, 'i')
+ && $this->testTimestamp($result, $this->hour, 'H')
+ && $this->testTimestamp($result, $this->day, 'd')
+ && $this->testTimestamp($result, $this->month, 'm')
+ && $this->testTimestamp($result, $this->day_of_week, 'N');
+
+ } while (!$valid && $i-- > 0);
+
+ if ($i <= 0) {
+ throw new RuntimeException('No result, current: ' . date('d.m.Y H:i', $result));
+ }
+
+ $this->next_execution = $result;
+ return $result;
+ }
+
+ /**
+ * Tests a timestamp against the passed condition.
+ *
+ * @param int $timestamp The timestamp to test
+ * @param mixed $condition Can be either null for "don't care", a positive
+ * number for an exact moment or a negative number
+ * for a repeating moment
+ * @param String $format Format for date() to extract a portion of the
+ * timestamp
+ */
+ protected function testTimestamp($timestamp, $condition, $format)
+ {
+ if ($condition === null) {
+ return true;
+ }
+
+ $probe = (int) date($format, $timestamp);
+ $condition = (int) $condition;
+
+ if ($condition < 0) {
+ return ($probe % abs($condition)) === 0;
+ }
+
+ return $probe === $condition;
+ }
+}