'integer', 'locked_at' => 'datetime', 'expires_at' => 'datetime', ]; public function evaluationRun(): BelongsTo { return $this->belongsTo(EvaluationRun::class); } /** * Pokusí se získat lock pro daný klíč. * * - Pokud existuje expirovaný lock, před pokusem ho smaže. * - Pokud už aktivní lock existuje, vrátí null. * * @param string $key Jedinečný identifikátor scope (např. "round:5"). * @param \App\Models\EvaluationRun|null $run Volitelný běh, ke kterému se lock váže. * @param \DateInterval|int|\Carbon\Carbon|null $ttl Doba platnosti (sekundy nebo DateInterval nebo konkrétní expirace). */ public static function acquire(string $key, ?EvaluationRun $run = null, \DateInterval|int|Carbon|null $ttl = null): ?self { $now = Carbon::now(); // uklidíme expirovaný lock se stejným klíčem, aby neblokoval unikátní index self::where('key', $key) ->whereNotNull('expires_at') ->where('expires_at', '<', $now) ->delete(); $expiresAt = null; if ($ttl instanceof Carbon) { $expiresAt = $ttl; } elseif ($ttl instanceof \DateInterval) { $expiresAt = (clone $now)->add($ttl); } elseif (is_int($ttl)) { $expiresAt = (clone $now)->addSeconds($ttl); } try { return self::create([ 'key' => $key, 'evaluation_run_id' => $run?->id, 'locked_at' => $now, 'expires_at' => $expiresAt, ]); } catch (QueryException $e) { // unikátní klíč porušen -> lock už drží někdo jiný if (str_contains(strtolower($e->getMessage()), 'duplicate') || str_contains(strtolower($e->getMessage()), 'unique')) { return null; } throw $e; } } /** * Uvolní lock podle klíče (a volitelně evaluation_run_id). */ public static function release(string $key, ?EvaluationRun $run = null): int { $query = self::where('key', $key); if ($run) { $query->where('evaluation_run_id', $run->id); } return $query->delete(); } /** * Zjistí, zda lock existuje a neexpiroval. */ public static function isLocked(string $key): bool { $now = Carbon::now(); return self::where('key', $key) ->where(function ($q) use ($now) { $q->whereNull('expires_at') ->orWhere('expires_at', '>', $now); }) ->exists(); } }