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