-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #991 from CatoTH/v4-background-jobs
Background Jobs
- Loading branch information
Showing
18 changed files
with
608 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace app\commands; | ||
|
||
use app\components\BackgroundJobProcessor; | ||
use app\components\BackgroundJobScheduler; | ||
use Yii; | ||
use yii\console\Controller; | ||
|
||
/** | ||
* Run background commends | ||
*/ | ||
class BackgroundJobController extends Controller | ||
{ | ||
private const DEFAULT_MAX_EVENTS = 1000; | ||
private const DEFAULT_MAX_RUNTIME_SECONDS = 600; | ||
private const DEFAULT_MAX_MEMORY_USAGE = 64_000_000; | ||
|
||
private const MAX_RETENTION_PERIOD_HOURS = 24 * 3; | ||
|
||
protected int $maxEvents = self::DEFAULT_MAX_EVENTS; | ||
protected int $maxRuntimeSeconds = self::DEFAULT_MAX_RUNTIME_SECONDS; | ||
protected int $maxMemoryUsage = self::DEFAULT_MAX_MEMORY_USAGE; | ||
|
||
public function options($actionID): array | ||
{ | ||
return ['maxEvents', 'maxRuntimeSeconds', 'maxMemoryUsage']; | ||
} | ||
|
||
/** | ||
* Runs the background job processor | ||
* Options: | ||
* --max-runtime-seconds 600 | ||
* --max-events 1000 | ||
* --max-memory-usage 64000000 | ||
*/ | ||
public function actionRun(): void | ||
{ | ||
echo "Starting background job processor at: " . (new \DateTimeImmutable())->format("Y-m-d H:i:s.u") . "\n"; | ||
|
||
$connection = \Yii::$app->getDb(); | ||
$connection->enableLogging = false; | ||
|
||
$processor = new BackgroundJobProcessor($connection); | ||
while (!$this->needsRestart($processor)) { | ||
$row = $processor->getJobAndSetStarted(); | ||
if ($row) { | ||
$processor->processRow($row); | ||
} else { | ||
usleep(100_000); | ||
} | ||
} | ||
|
||
echo "Stopping background job processor at: " . (new \DateTimeImmutable())->format("Y-m-d H:i:s.u") . "\n"; | ||
} | ||
|
||
private function needsRestart(BackgroundJobProcessor $processor): bool | ||
{ | ||
if ($processor->getProcessedEvents() >= $this->maxEvents) { | ||
echo "Stopping because maximum number of processed events has been reached.\n"; | ||
return true; | ||
} | ||
|
||
if ($processor->getRuntimeInSeconds() >= $this->maxRuntimeSeconds) { | ||
echo "Stopping because maximum runtime has been reached.\n"; | ||
return true; | ||
} | ||
|
||
if (memory_get_peak_usage() >= $this->maxMemoryUsage) { | ||
echo "Stopping because maximum memory usage has been reached.\n"; | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Cleans up old tasks from database | ||
*/ | ||
public function actionCleanup(): void | ||
{ | ||
$deletedJobs = BackgroundJobScheduler::cleanup(self::MAX_RETENTION_PERIOD_HOURS); | ||
|
||
echo "Deleted $deletedJobs jobs.\n"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace app\components; | ||
|
||
use app\models\backgroundJobs\IBackgroundJob; | ||
use yii\db\Connection; | ||
|
||
class BackgroundJobProcessor | ||
{ | ||
private Connection $connection; | ||
|
||
private int $processedEvents = 0; | ||
private \DateTimeImmutable $startedAt; | ||
|
||
public function __construct(Connection $connection) { | ||
$this->connection = $connection; | ||
$this->startedAt = new \DateTimeImmutable(); | ||
} | ||
|
||
public function getJobAndSetStarted(): ?IBackgroundJob { | ||
$foundJob = null; | ||
|
||
$this->connection->transaction(function () use (&$foundJob) { | ||
$command = $this->connection->createCommand('SELECT * FROM backgroundJob WHERE dateStarted IS NULL ORDER BY id ASC LIMIT 0,1 FOR UPDATE'); | ||
$foundRows = $command->queryAll(); | ||
if (empty($foundRows)) { | ||
return; | ||
} | ||
|
||
$foundRow = $foundRows[0]; | ||
$this->connection->createCommand('UPDATE backgroundJob SET dateStarted = NOW() WHERE id = :id', ['id' => $foundRow['id']])->execute(); | ||
|
||
$foundJob = IBackgroundJob::fromJson( | ||
intval($foundRow['id']), | ||
$foundRow['type'], | ||
($foundRow['siteId'] > 0 ? $foundRow['siteId'] : null), | ||
($foundRow['consultationId'] > 0 ? $foundRow['consultationId'] : null), | ||
$foundRow['payload'] | ||
); | ||
}); | ||
|
||
return $foundJob; | ||
} | ||
|
||
public function processRow(IBackgroundJob $job): void | ||
{ | ||
$this->connection->createCommand( | ||
'UPDATE backgroundJob SET dateUpdated = NOW() WHERE id = :id', | ||
['id' => $job->getId()] | ||
)->execute(); | ||
|
||
try { | ||
$job->execute(); | ||
|
||
$this->connection->createCommand( | ||
'UPDATE backgroundJob SET dateFinished = NOW() WHERE id = :id', | ||
['id' => $job->getId()] | ||
)->execute(); | ||
} catch (\Throwable $exception) { | ||
$this->connection->createCommand( | ||
'UPDATE backgroundJob SET error = :error WHERE id = :id', | ||
[':error' => $exception->getMessage() . PHP_EOL . $exception->getTraceAsString(), ':id' => $job->getId()] | ||
)->execute(); | ||
} | ||
} | ||
|
||
public function getProcessedEvents(): int { | ||
return $this->processedEvents; | ||
} | ||
|
||
public function getRuntimeInSeconds(): int { | ||
return (new \DateTimeImmutable())->getTimestamp() - $this->startedAt->getTimestamp(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace app\components; | ||
|
||
use app\models\backgroundJobs\IBackgroundJob; | ||
use app\models\settings\AntragsgruenApp; | ||
|
||
class BackgroundJobScheduler | ||
{ | ||
public const HEALTH_MAX_AGE_SECONDS = 120; | ||
|
||
public static function executeOrScheduleJob(IBackgroundJob $job): void | ||
{ | ||
if (isset(AntragsgruenApp::getInstance()->backgroundJobs['notifications']) && AntragsgruenApp::getInstance()->backgroundJobs['notifications']) { | ||
\Yii::$app->getDb()->createCommand( | ||
'INSERT INTO `backgroundJob` (`siteId`, `consultationId`, `type`, `dateCreation`, `payload`) VALUES (:siteId, :consultationId, :type, NOW(), :payload)', | ||
[ | ||
':siteId' => $job->getSite()?->id, | ||
':consultationId' => $job->getConsultation()?->id, | ||
':type' => $job->getTypeId(), | ||
':payload' => $job->toJson(), | ||
] | ||
)->execute(); | ||
} else { | ||
$job->execute(); | ||
} | ||
} | ||
|
||
/** | ||
* @return array{healthy: bool|null, data: array<string, mixed>} | ||
*/ | ||
public static function getDiagnostics(): array | ||
{ | ||
if (!isset(AntragsgruenApp::getInstance()->backgroundJobs['notifications']) || !AntragsgruenApp::getInstance()->backgroundJobs['notifications']) { | ||
return [ | ||
'healthy' => null, | ||
'data' => [], | ||
]; | ||
} | ||
|
||
$command = \Yii::$app->getDb()->createCommand('SELECT MIN(dateCreation) minAge, COUNT(*) num FROM backgroundJob WHERE dateStarted IS NULL'); | ||
$result = $command->queryAll()[0]; | ||
$unstarted = [ | ||
'num' => intval($result['num']), | ||
'age' => ($result['minAge'] ? (time() - Tools::dateSql2timestamp($result['minAge'])) : 0), | ||
]; | ||
|
||
$command = \Yii::$app->getDb()->createCommand('SELECT MIN(dateCreation) minAge, COUNT(*) num FROM backgroundJob WHERE dateFinished IS NULL'); | ||
$result = $command->queryAll()[0]; | ||
$unfinished = [ | ||
'num' => intval($result['num']), | ||
'age' => ($result['minAge'] ? (time() - Tools::dateSql2timestamp($result['minAge'])) : 0), | ||
]; | ||
|
||
return [ | ||
'healthy' => ($unstarted['age'] <= self::HEALTH_MAX_AGE_SECONDS && $unfinished['age'] <= self::HEALTH_MAX_AGE_SECONDS), | ||
'data' => [ | ||
'unstarted' => $unstarted, | ||
'unfinished' => $unfinished, | ||
], | ||
]; | ||
} | ||
|
||
public static function cleanup(int $maxHageHours): int | ||
{ | ||
$command = \Yii::$app->getDb()->createCommand( | ||
'DELETE FROM backgroundJob WHERE dateFinished < NOW() - INTERVAL :hours HOUR', | ||
[':hours' => $maxHageHours] | ||
); | ||
|
||
return $command->execute(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.