Add BackgroundQueueHandler and AbstractBackgroundJob
authorTim Düsterhus <duesterhus@woltlab.com>
Mon, 8 Jun 2015 00:23:39 +0000 (02:23 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Sun, 21 Jun 2015 12:59:21 +0000 (14:59 +0200)
wcfsetup/install/files/lib/system/background/BackgroundQueueHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/background/job/AbstractBackgroundJob.class.php [new file with mode: 0644]
wcfsetup/setup/db/install.sql

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 (file)
index 0000000..b8db0b3
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+namespace wcf\system\background;
+use wcf\system\background\job\AbstractBackgroundJob;
+use wcf\system\exception\LoggedException;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Manages the background queue.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 (file)
index 0000000..075ea04
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+namespace wcf\system\background\job;
+
+/**
+ * An AbstractBackgroundJob can be performed asynchronously by
+ * the background queue.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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();
+}
index 3c298dc5ac1994a1ae5b602d609c405ff2d4f2bd..ac53487415ca4b3e7c146359ced055b94fadaaf2 100644 (file)
@@ -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,