optimize tag and rebuilding

This commit is contained in:
team2
2026-02-23 08:51:21 +01:00
parent 8c58d6777d
commit dceeaeee52
8 changed files with 678 additions and 167 deletions

View File

@@ -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();
}
}