Commit ec16bcef authored by Carsten  Rose's avatar Carsten Rose
Browse files

Feature 4250 / autocron: First implementation

parent 73562bc2
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 8/18/17
* Time: 8:49 AM
*/
namespace qfq;
use qfq;
//require_once(__DIR__ . '/../qfq/QuickFormQuery.php');
require_once(__DIR__ . '/../qfq/store/Store.php');
require_once(__DIR__ . '/../qfq/Constants.php');
require_once(__DIR__ . '/../qfq/database/Database.php');
require_once(__DIR__ . '/../qfq/exceptions/ShellException.php');
class AutoCron {
/**
* @var Database instantiated class
*/
protected $db = null;
/**
* @var Store
*/
protected $store = null;
/**
* @var bool
*/
private $phpUnit = false;
public function __construct($phpUnit = false) {
$this->phpUnit = $phpUnit;
mb_internal_encoding("UTF-8");
// set_error_handler("\\qfq\\ErrorHandler::exception_error_handler");
$this->store = Store::getInstance();
$this->db = new Database();
// $this->eval = new Evaluate($this->store, $this->db);
}
/**
* Check if there started cronJobs, older than $ageMaxMinutes
*
* @param int $ageMaxMinutes
* @throws CodeException
* @throws DbException
*/
private function checkForOldJobs($ageMaxMinutes) {
$sql = "SELECT c.id, ' Started at: ', c.inProgress FROM Cron AS c WHERE DATE_ADD(c.inProgress, INTERVAL $ageMaxMinutes MINUTE)<NOW()";
$this->db->sql($sql, ROW_EXPECT_0);
}
/**
* @param array $job
* @return array
* @throws CodeException
* @throws DbException
*/
private function calcNextRun(array $job) {
// If nextRun is already in the future, do nothing.
if (date('Y-m-d H:i:s') < $job[AUTOCRON_NEXT_RUN]) {
return $job;
}
// With no frequency: stop future repeating by setting nextRun=0
if ($job[AUTOCRON_FREQUENCY] == '') {
$job[AUTOCRON_NEXT_RUN] = 0;
return $job;
}
// '1 DAY', '15 MINUTE', '6 MONTH', or empty
$job[AUTOCRON_FREQUENCY] = trim($job[AUTOCRON_FREQUENCY]);
$arr = explode(' ', $job[AUTOCRON_FREQUENCY]);
$count = empty($arr[0]) ? '1' : $arr[0];
$unit = empty($arr[1]) ? 'DAY' : $arr[1];
// Regular: nextRun is in the past and +frequency is in the future
// Late: nextRun is so much behind that +frequency is still in the past. Calculate nextRun for the future with respect to the given shedule/frequency (skip lost past actions)
// Do the calculation with MySQL to stay in the MySQL date/time calculation world.
$sql = "SELECT DATE_ADD('" . $job[AUTOCRON_NEXT_RUN] . "', INTERVAL CEILING( ( 1 + TIMESTAMPDIFF($unit, '" .
$job[AUTOCRON_NEXT_RUN] . "', NOW() ) ) / $count ) * " . $job[AUTOCRON_FREQUENCY] . ") AS " . AUTOCRON_NEXT_RUN;
$row = $this->db->sql($sql); // frequency, nextRun, unit, count
$job[AUTOCRON_NEXT_RUN] = $row[AUTOCRON_NEXT_RUN];
return $job;
}
/**
* @param array $job
* @return array
*/
private function doJobWebsite(array $job) {
$job[AUTOCRON_LAST_STATUS] = 'nothing done';
$job[AUTOCRON_CONTENT] = trim($job[AUTOCRON_CONTENT]);
if (!empty($job[AUTOCRON_CONTENT])) {
if (substr($job[AUTOCRON_CONTENT], 0, 4) != 'http') {
// We need to prefix
$baseUrl = $this->store->getVar(SYSTEM_BASE_URL_PRINT, STORE_SYSTEM);
$job[AUTOCRON_CONTENT] = $baseUrl . $job[AUTOCRON_CONTENT];
}
// Download page
$page = file_get_contents($job[AUTOCRON_CONTENT]);
if ($page === false) {
$job[AUTOCRON_LAST_STATUS] = 'Error: failed to fetch ' . $job[AUTOCRON_CONTENT];
} else {
$job[AUTOCRON_LAST_STATUS] = 'OK: ' . substr($page, 0, 30);
}
}
return $job;
}
/**
* @throws CodeException
* @throws DbException
*/
public function process() {
$sql = "SELECT c.id, c.type, c.content, c.frequency, c.nextRun FROM Cron AS c WHERE c.status='enable' AND c.nextRun < NOW() AND c.nextRun!=0 AND c.inProgress=0";
$jobs = $this->db->sql($sql);
// Iterate over all AutoCron Jobs
foreach ($jobs as $job) {
// Start progress counter
$this->db->sql("UPDATE Cron SET inProgress=NOW() WHERE id=? LIMIT 1", ROW_REGULAR, [$job[COLUMN_ID]]);
switch ($job[AUTOCRON_TYPE]) {
case AUTOCRON_TYPE_WEBSITE:
$job = $this->doJobWebsite($job);
break;
case AUTOCRON_TYPE_MAIL:
$job = $this->doJobMail($job);
break;
default:
throw new ShellException('Unknown cron.type value: ' . $job[AUTOCRON_TYPE], ERROR_UNKNOWN_MODE);
}
// Finish Job
$job = $this->calcNextRun($job);
$sql = "UPDATE Cron SET lastRun=NOW(), lastStatus=?, nextRun=?, inProgress=0 WHERE id=? LIMIT 1";
$this->db->sql($sql, ROW_REGULAR, [$job[AUTOCRON_LAST_STATUS], $job[AUTOCRON_NEXT_RUN], $job[COLUMN_ID]]);
}
$this->checkForOldJobs(AUTOCRON_MAX_AGE_MINUTES);
}
}
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 8/17/17
* Time: 10:29 PM
*/
require_once(__DIR__ . '/AutoCron.php');
try {
$autocron = new \qfq\AutoCron();
$autocron->process();
} catch (qfq\UserFormException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\CodeException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (qfq\DbException $e) {
$answer[API_MESSAGE] = $e->formatMessage();
} catch (\Exception $e) {
$answer[API_MESSAGE] = "Generic Exception: " . $e->getMessage();
}
......@@ -262,6 +262,7 @@ const ERROR_DIRTY_MISSING_LOCK = 2203;
const ERROR_DIRTY_ALREADY_LOCKED = 2204;
const ERROR_DIRTY_RECORD_MODIFIED = 2205;
//
// Store Names: Identifier
//
......@@ -1131,4 +1132,22 @@ const DIRTY_API_ACTION_RELEASE = 'release';
const DIRTY_API_ACTION_EXTEND = 'extend';
const LOCK_NOT_FOUND = 0;
const LOCK_FOUND_OWNER = 1;
const LOCK_FOUND_CONFLICT = 2;
\ No newline at end of file
const LOCK_FOUND_CONFLICT = 2;
// AutoCron
const AUTOCRON_MAX_AGE_MINUTES = 10;
const AUTOCRON_TYPE = 'type';
const AUTOCRON_TYPE_WEBSITE = 'website';
const AUTOCRON_TYPE_MAIL = 'mail';
const AUTOCRON_LAST_RUN = 'lastRun';
const AUTOCRON_LAST_STATUS = 'lastStatus';
const AUTOCRON_NEXT_RUN = 'nextRun';
const AUTOCRON_FREQUENCY = 'frequency';
const AUTOCRON_IN_PROGRESS = 'inProgress';
const AUTOCRON_STATUS = 'status';
const AUTOCRON_CONTENT = 'content';
const AUTOCRON_UNIT = 'unit';
const AUTOCRON_COUNT = 'count';
<?php
/**
* Created by PhpStorm.
* User: crose
* Date: 8/17/17
* Time: 11:21 PM
*/
namespace qfq;
require_once(__DIR__ . '/AbstractException.php');
/**
* Class DownloadException
*
* Thrown by AutoCron...
*
* @package qfq\exceptions
*/
class ShellException extends AbstractException {
/*
* @return string HTML formatted error string
*/
public function formatMessage() {
$this->messageArray['Type'] = 'Shell Exception';
return parent::formatException();
}
}
\ No newline at end of file
......@@ -405,3 +405,26 @@ VALUES
# Default record for table Period
INSERT INTO Period (start, name, created) VALUES (NOW(), 'dummy', NOW());
# AutoCRON
CREATE TABLE IF NOT EXISTS `Cron` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`grId` INT(11) NOT NULL,
`type` ENUM('mail', 'website') NOT NULL DEFAULT 'website',
`lastRun` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`lastStatus` TEXT NOT NULL,
`nextRun` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`frequency` VARCHAR(32) NOT NULL,
`inProgress` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
`status` ENUM('enable', 'disable') NOT NULL DEFAULT 'enable',
`content` TEXT NOT NULL,
`comment` TEXT NOT NULL,
`modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
AUTO_INCREMENT = 0
DEFAULT CHARSET = utf8;
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment