From db6698ad50986f95d4b140a0ee9fe071e9c2819e Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 8 Jun 2015 02:23:39 +0200 Subject: [PATCH] Add BackgroundQueueHandler and AbstractBackgroundJob --- .../BackgroundQueueHandler.class.php | 123 ++++++++++++++++++ .../job/AbstractBackgroundJob.class.php | 61 +++++++++ wcfsetup/setup/db/install.sql | 9 ++ 3 files changed, 193 insertions(+) create mode 100644 wcfsetup/install/files/lib/system/background/BackgroundQueueHandler.class.php create mode 100644 wcfsetup/install/files/lib/system/background/job/AbstractBackgroundJob.class.php diff --git a/wcfsetup/install/files/lib/system/background/BackgroundQueueHandler.class.php b/wcfsetup/install/files/lib/system/background/BackgroundQueueHandler.class.php new file mode 100644 index 0000000000..b8db0b3956 --- /dev/null +++ b/wcfsetup/install/files/lib/system/background/BackgroundQueueHandler.class.php @@ -0,0 +1,123 @@ + + * @package com.woltlab.wcf + * @subpackage system.background.job + * @category Community Framework + */ +class BackgroundQueueHandler extends SingletonFactory { + /** + * Enqueues the given job for execution at the given time. + * Note: The time is a minimum time. Depending on the size of + * the queue the job can be performed later as well! + * + * @param \wcf\system\background\job\AbstractBackgroundJob $job The job to enqueue. + * @param int $time Earliest time to consider the job for execution. + */ + public function enqueue(AbstractBackgroundJob $job, $time = 0) { + $sql = "INSERT INTO wcf".WCF_N."_background_job + (job, time) + VALUES (?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + serialize($job), + $time + ]); + } + + /** + * Performs the (single) job that is due next. + * This method automatically handles requeuing in case of failure. + */ + public function performJob() { + WCF::getDB()->beginTransaction(); + $commited = false; + try { + $sql = "SELECT jobID, job + FROM wcf".WCF_N."_background_job + WHERE status = ? + AND time <= ? + ORDER BY time ASC, jobID ASC + FOR UPDATE"; + $statement = WCF::getDB()->prepareStatement($sql, 1); + $statement->execute([ + 'ready', + TIME_NOW + ]); + $row = $statement->fetchSingleRow(); + if (!$row) { + // nothing to do here + return; + } + + // lock job + $sql = "UPDATE wcf".WCF_N."_background_job + SET status = ?, + time = ? + WHERE jobID = ? + AND status = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + 'processing', + TIME_NOW, + $row['jobID'], + 'ready' + ]); + if ($statement->getAffectedRows() != 1) { + // somebody stole the job + // this cannot happen unless MySQL violates it's contract to lock the row + // -> silently ignore, there will be plenty of other oppurtunities to perform a job + return; + } + WCF::getDB()->commitTransaction(); + $commited = true; + } + finally { + if (!$commited) WCF::getDB()->rollbackTransaction(); + } + + $job = null; + try { + // no shut up operator, exception will be caught + $job = unserialize($row['job']); + if ($job) { + $job->perform(); + } + } + catch (\Exception $e) { + // gotta catch 'em all + if ($job) { + $job->fail(); + + if ($job->getFailures() <= $job::MAX_FAILURES) { + $this->enqueue($job, TIME_NOW + $job->retryAfter()); + } + else { + // job failed too often: log + if ($e instanceof LoggedException) $e->getExceptionID(); + } + } + else { + // job is completely broken: log + if ($e instanceof LoggedException) $e->getExceptionID(); + } + } + finally { + // remove entry of processed job + $sql = "DELETE FROM wcf".WCF_N."_background_job + WHERE jobID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $row['jobID'] ]); + } + } +} diff --git a/wcfsetup/install/files/lib/system/background/job/AbstractBackgroundJob.class.php b/wcfsetup/install/files/lib/system/background/job/AbstractBackgroundJob.class.php new file mode 100644 index 0000000000..075ea04bdc --- /dev/null +++ b/wcfsetup/install/files/lib/system/background/job/AbstractBackgroundJob.class.php @@ -0,0 +1,61 @@ + + * @package com.woltlab.wcf + * @subpackage system.background.job + * @category Community Framework + */ +abstract class AbstractBackgroundJob { + /** + * The number of times this job can fail, before completely + * dequeuing it. The default is 3. + * + * @var int + */ + const MAX_FAILURES = 3; + + /** + * The number of times this job already failed. + * @var int + */ + private $failures = 0; + + /** + * Returns the number of times this job already failed. + * + * @return int + */ + public final function getFailures() { + return $this->failures; + } + + /** + * Increments the fail counter. + */ + public final function fail() { + $this->failures++; + } + + /** + * Returns the number of seconds to wait before requeuing a failed job. + * + * @return int 30 minutes by default + */ + public function retryAfter() { + return 30 * 60; + } + + /** + * Performs the job. It will automatically be requeued up to MAX_FAILURES times + * if it fails (either throws an Exception or does not finish until the clean up + * cronjob comes along). + */ + abstract public function perform(); +} diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 3c298dc5ac..ac53487415 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -166,6 +166,15 @@ CREATE TABLE wcf1_attachment ( KEY (objectID, uploadTime) ); +DROP TABLE IF EXISTS wcf1_background_job; +CREATE TABLE wcf1_background_job ( + jobID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, + job MEDIUMTEXT NOT NULL, + status ENUM('ready', 'processing') NOT NULL DEFAULT 'ready', + time INT(10) NOT NULL, + KEY (status, time) +); + DROP TABLE IF EXISTS wcf1_bbcode; CREATE TABLE wcf1_bbcode ( bbcodeID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, -- 2.20.1