optimize tag and rebuilding
This commit is contained in:
@@ -10,6 +10,12 @@ use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class TagRebuildJobService
|
||||
{
|
||||
/**
|
||||
* Wenn ein QUEUED-Job länger nicht startet, gilt er als "stale" und wird auf FAILED gesetzt,
|
||||
* damit das System nicht dauerhaft blockiert.
|
||||
*/
|
||||
private const STALE_QUEUED_AFTER_SECONDS = 300; // 5 Minuten
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $agentLogger,
|
||||
@@ -28,24 +34,153 @@ final readonly class TagRebuildJobService
|
||||
return $job;
|
||||
}
|
||||
|
||||
public function enqueueIfIdle(): ?TagRebuildJob
|
||||
{
|
||||
// Coalescing: Wenn ein Job läuft oder queued ist -> nichts tun
|
||||
if ($this->hasActiveJob()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->enqueueAndStartAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Letzter Job (egal welcher Status).
|
||||
*/
|
||||
public function getLatestJob(): ?TagRebuildJob
|
||||
{
|
||||
return $this->em->createQueryBuilder()
|
||||
->select('j')
|
||||
->from(TagRebuildJob::class, 'j')
|
||||
->orderBy('j.createdAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Letzter Job mit Status COMPLETED.
|
||||
*/
|
||||
public function getLatestCompletedJob(): ?TagRebuildJob
|
||||
{
|
||||
return $this->em->createQueryBuilder()
|
||||
->select('j')
|
||||
->from(TagRebuildJob::class, 'j')
|
||||
->where('j.status = :status')
|
||||
->setParameter('status', TagRebuildJob::STATUS_COMPLETED)
|
||||
->orderBy('j.createdAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ob gerade ein Job aktiv ist:
|
||||
* - RUNNING ist immer aktiv
|
||||
* - QUEUED ist nur aktiv, wenn er nicht stale ist
|
||||
*
|
||||
* Zusätzlich: stale QUEUED Jobs werden auf FAILED gesetzt (Recovery).
|
||||
*/
|
||||
public function hasActiveJob(): bool
|
||||
{
|
||||
$this->markStaleQueuedJobsFailed();
|
||||
|
||||
$cutoff = new \DateTimeImmutable('-' . self::STALE_QUEUED_AFTER_SECONDS . ' seconds');
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('COUNT(j.id)')
|
||||
->from(TagRebuildJob::class, 'j')
|
||||
->where(
|
||||
$qb->expr()->orX(
|
||||
'j.status = :running',
|
||||
$qb->expr()->andX(
|
||||
'j.status = :queued',
|
||||
'j.createdAt >= :cutoff'
|
||||
)
|
||||
)
|
||||
)
|
||||
->setParameter('running', TagRebuildJob::STATUS_RUNNING)
|
||||
->setParameter('queued', TagRebuildJob::STATUS_QUEUED)
|
||||
->setParameter('cutoff', $cutoff);
|
||||
|
||||
return (int) $qb->getQuery()->getSingleScalarResult() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Job async über bin/console.
|
||||
* Wichtige Fixes:
|
||||
* - php explizit verwenden
|
||||
* - --no-interaction
|
||||
* - Logfile statt /dev/null
|
||||
*/
|
||||
private function startAsync(TagRebuildJob $job): void
|
||||
{
|
||||
$php = PHP_BINARY; // safest in runtime
|
||||
$console = rtrim($this->projectDir, '/') . '/bin/console';
|
||||
$projectDir = rtrim($this->projectDir, '/');
|
||||
$console = $projectDir . '/bin/console';
|
||||
|
||||
$jobId = (string) $job->getId();
|
||||
|
||||
$logDir = $projectDir . '/var/log/tags';
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0777, true);
|
||||
}
|
||||
$logFile = $logDir . '/job_' . $jobId . '.log';
|
||||
|
||||
// Robust: cd ins Projekt, dann nohup php bin/console ...
|
||||
$cmd = sprintf(
|
||||
'%s %s %s %s > /dev/null 2>&1 &',
|
||||
escapeshellarg($php),
|
||||
'cd %s && nohup %s %s %s %s --no-interaction >> %s 2>&1 &',
|
||||
escapeshellarg($projectDir),
|
||||
escapeshellcmd('php'),
|
||||
escapeshellarg($console),
|
||||
'mto:agent:tags:job:run',
|
||||
escapeshellarg((string)$job->getId())
|
||||
escapeshellarg('mto:agent:tags:job:run'),
|
||||
escapeshellarg($jobId),
|
||||
escapeshellarg($logFile)
|
||||
);
|
||||
|
||||
$this->agentLogger->info('[tags] enqueue job async', [
|
||||
'job' => (string)$job->getId(),
|
||||
'job' => $jobId,
|
||||
'cmd' => $cmd,
|
||||
'log' => $logFile,
|
||||
]);
|
||||
|
||||
@exec($cmd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recovery gegen "ewig QUEUED":
|
||||
* Setzt alte QUEUED Jobs auf FAILED, damit enqueueIfIdle() nicht dauerhaft blockiert.
|
||||
*/
|
||||
private function markStaleQueuedJobsFailed(): void
|
||||
{
|
||||
$cutoff = new \DateTimeImmutable('-' . self::STALE_QUEUED_AFTER_SECONDS . ' seconds');
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb->select('j')
|
||||
->from(TagRebuildJob::class, 'j')
|
||||
->where('j.status = :queued')
|
||||
->andWhere('j.createdAt < :cutoff')
|
||||
->setParameter('queued', TagRebuildJob::STATUS_QUEUED)
|
||||
->setParameter('cutoff', $cutoff)
|
||||
->setMaxResults(25);
|
||||
|
||||
/** @var TagRebuildJob[] $stale */
|
||||
$stale = $qb->getQuery()->getResult();
|
||||
|
||||
if (!$stale) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($stale as $job) {
|
||||
$jobId = (string) $job->getId();
|
||||
|
||||
$job->markFailed('Stale QUEUED job detected (async start likely failed).');
|
||||
|
||||
$this->agentLogger->warning('[tags] stale QUEUED job marked FAILED', [
|
||||
'job' => $jobId,
|
||||
'cutoff' => $cutoff->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user