--- /dev/null
+<?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'] ]);
+ }
+ }
+}
--- /dev/null
+<?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();
+}
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,