142 lines
5.0 KiB
PHP
142 lines
5.0 KiB
PHP
<?php
|
||
|
||
namespace App\Jobs;
|
||
|
||
use App\Models\EvaluationLock;
|
||
use App\Models\EvaluationRun;
|
||
use App\Services\Evaluation\EvaluationCoordinator;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Foundation\Queue\Queueable;
|
||
use Throwable;
|
||
|
||
/**
|
||
* Job: FinalizeRunJob
|
||
*
|
||
* Odpovědnost:
|
||
* - Finální krok vyhodnocovací pipeline pro jeden EvaluationRun.
|
||
* - Přepíná vyhodnocovací běh z technického stavu "rozpracováno" do
|
||
* konzistentního, uzavřeného stavu, který je připraven k prezentaci
|
||
* nebo publikaci výsledků.
|
||
*
|
||
* Kontext:
|
||
* - Spouští se po úspěšném dokončení agregace výsledků
|
||
* (AggregateLogResultsJob / DispatchAggregateResultsJobsJob).
|
||
* - Pracuje výhradně s agregovanými daty svázanými s evaluation_run_id.
|
||
*
|
||
* Co job dělá (typicky):
|
||
* - Ověří, že všechny předchozí kroky pipeline byly úspěšně dokončeny.
|
||
* - Provede finální validace výsledků (např. konzistence součtů).
|
||
* - Označí agregované výsledky jako finální pro daný EvaluationRun:
|
||
* - nastaví příznaky typu is_final / is_official
|
||
* - případně provede přesun z dočasných (staging) struktur
|
||
* do finálních tabulek
|
||
* - Aktualizuje stav EvaluationRun:
|
||
* - status -> SUCCEEDED (nebo FAILED při chybě)
|
||
* - nastaví finished_at
|
||
* - Uvolní locky držené pro scope vyhodnocení.
|
||
* - Zapíše závěrečné auditní události (EvaluationRunEvent).
|
||
*
|
||
* Co job NEDĚLÁ:
|
||
* - nepočítá body ani skóre
|
||
* - nemění výsledky jednotlivých QSO
|
||
* - neřeší export ani prezentaci v UI
|
||
*
|
||
* Zásady návrhu:
|
||
* - Tento krok musí být atomický z pohledu stavu vyhodnocení.
|
||
* - Při selhání musí být EvaluationRun jednoznačně označen jako FAILED
|
||
* a nesmí zůstat v nekonzistentním stavu.
|
||
* - Veškerá logika patří do service layer (např. EvaluationFinalizerService).
|
||
*
|
||
* Queue:
|
||
* - Spouští se ve frontě "evaluation".
|
||
* - Nemá běžet paralelně nad stejným EvaluationRun.
|
||
*/
|
||
class FinalizeRunJob implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
public int $tries = 2;
|
||
public array $backoff = [60];
|
||
|
||
/**
|
||
* Create a new job instance.
|
||
*/
|
||
public function __construct(
|
||
protected int $evaluationRunId,
|
||
protected ?string $lockKey = null
|
||
)
|
||
{
|
||
//
|
||
}
|
||
|
||
/**
|
||
* Finalizuje vyhodnocovací běh.
|
||
*
|
||
* Metoda handle():
|
||
* - provede závěrečné kontroly konzistence dat
|
||
* - označí výsledky jako finální / oficiální
|
||
* - přepne EvaluationRun do koncového stavu (SUCCEEDED / FAILED)
|
||
* - uvolní zdroje a locky držené během vyhodnocení
|
||
*
|
||
* Poznámky:
|
||
* - Tento job je posledním krokem pipeline.
|
||
* - Po jeho úspěšném dokončení musí být možné výsledky bezpečně
|
||
* zobrazit nebo exportovat.
|
||
*/
|
||
public function handle(): void
|
||
{
|
||
$run = EvaluationRun::find($this->evaluationRunId);
|
||
if (! $run || $run->isCanceled()) {
|
||
return;
|
||
}
|
||
$coordinator = new EvaluationCoordinator();
|
||
|
||
try {
|
||
$coordinator->eventInfo($run, 'Finalize: krok spuštěn.', [
|
||
'step' => 'finalize',
|
||
'round_id' => $run->round_id,
|
||
]);
|
||
// Po ručních zásazích může být potřeba znovu aplikovat override a přepočítat pořadí.
|
||
(new ApplyLogOverridesJob($run->id))->handle();
|
||
(new RecalculateOfficialRanksJob($run->id))->handle();
|
||
$coordinator->eventInfo($run, 'Před finalizací byl znovu aplikován override a přepočítáno pořadí.', [
|
||
'step' => 'finalize',
|
||
'round_id' => $run->round_id,
|
||
]);
|
||
|
||
$run->update([
|
||
'status' => 'SUCCEEDED',
|
||
'current_step' => 'finalize',
|
||
'progress_total' => 1,
|
||
'progress_done' => 1,
|
||
'finished_at' => now(),
|
||
]);
|
||
$coordinator->eventInfo($run, 'Vyhodnocení dokončeno.', [
|
||
'step' => 'finalize',
|
||
'round_id' => $run->round_id,
|
||
'step_progress_done' => 1,
|
||
'step_progress_total' => 1,
|
||
]);
|
||
$coordinator->eventInfo($run, 'Finalize: krok dokončen.', [
|
||
'step' => 'finalize',
|
||
'round_id' => $run->round_id,
|
||
]);
|
||
} catch (Throwable $e) {
|
||
$run->update([
|
||
'status' => 'FAILED',
|
||
'error' => $e->getMessage(),
|
||
'finished_at' => now(),
|
||
]);
|
||
$coordinator->eventError($run, "FinalizeRunJob selhal: {$e->getMessage()}", [
|
||
'step' => 'finalize',
|
||
'round_id' => $run->round_id,
|
||
]);
|
||
throw $e;
|
||
} finally {
|
||
// Lock musí být uvolněn vždy, i při chybě – jinak zablokuje další běhy.
|
||
$lockKey = $this->lockKey ?? "evaluation:round:{$run->round_id}";
|
||
EvaluationLock::release($lockKey, $run);
|
||
}
|
||
}
|
||
}
|