generateOwnerId(); $now = $this->clock->time(); $expiresAt = Timestamp::fromFloat($now->toFloat() + $ttl->toSeconds()); $lockData = json_encode([ 'owner_id' => $ownerId, 'acquired_at' => $now->toFloat(), 'expires_at' => $expiresAt->toFloat(), 'server_id' => $this->serverId, ]); // Check if lock already exists (NX semantics) if ($this->redis->exists($lockKey) > 0) { return null; // Lock bereits von anderem Process gehalten } // SET with Expiration $success = $this->redis->set( $lockKey, $lockData, (int) $ttl->toSeconds() ); if (!$success) { return null; // Fehler beim Setzen } $lockId = uniqid('lock_', true); return new DistributedLock( lockId: $lockId, jobId: $jobId, ownerId: $ownerId, acquiredAt: $now, expiresAt: $expiresAt, ttl: $ttl ); } /** * Gibt einen Lock frei */ public function releaseLock(DistributedLock $lock): bool { $lockKey = self::LOCK_PREFIX . $lock->jobId; // Lösche Lock nur wenn wir der Owner sind $lockData = $this->redis->get($lockKey); if ($lockData === false) { return false; // Lock existiert nicht mehr } $data = json_decode($lockData, true); if ($data['owner_id'] !== $lock->ownerId) { return false; // Lock gehört jemand anderem } $this->redis->delete($lockKey); $this->redis->delete(self::HEARTBEAT_PREFIX . $lock->jobId); return true; } /** * Erneuert einen Lock (Heartbeat) * * Verhindert dass Lock während langer Job-Ausführung expired */ public function heartbeat(DistributedLock $lock): bool { $lockKey = self::LOCK_PREFIX . $lock->jobId; $heartbeatKey = self::HEARTBEAT_PREFIX . $lock->jobId; // Prüfe ob wir noch Lock-Owner sind $lockData = $this->redis->get($lockKey); if ($lockData === false) { return false; // Lock expired } $data = json_decode($lockData, true); if ($data['owner_id'] !== $lock->ownerId) { return false; // Lock von jemand anderem übernommen } // Erneuere TTL $ttlSeconds = $lock->ttl?->toSeconds() ?? 300; $this->redis->expire($lockKey, (int) $ttlSeconds); // Setze Heartbeat Timestamp $this->redis->set( $heartbeatKey, (string) $this->clock->time()->toFloat(), (int) $ttlSeconds ); return true; } /** * Prüft ob ein Job aktuell gelockt ist */ public function isLocked(string $jobId): bool { $lockKey = self::LOCK_PREFIX . $jobId; return $this->redis->exists($lockKey) > 0; } /** * Holt Lock-Informationen für einen Job */ public function getLockInfo(string $jobId): ?array { $lockKey = self::LOCK_PREFIX . $jobId; $lockData = $this->redis->get($lockKey); if ($lockData === false) { return null; } $data = json_decode($lockData, true); $ttl = $this->redis->ttl($lockKey); return [ 'job_id' => $jobId, 'owner_id' => $data['owner_id'] ?? 'unknown', 'server_id' => $data['server_id'] ?? 'unknown', 'acquired_at' => $data['acquired_at'] ?? null, 'expires_at' => $data['expires_at'] ?? null, 'ttl_remaining' => $ttl > 0 ? $ttl : 0, ]; } /** * Holt letzten Heartbeat-Timestamp für einen Job */ public function getLastHeartbeat(string $jobId): ?Timestamp { $heartbeatKey = self::HEARTBEAT_PREFIX . $jobId; $timestamp = $this->redis->get($heartbeatKey); if ($timestamp === false) { return null; } return Timestamp::fromFloat((float) $timestamp); } /** * Forciert Lock-Release (z.B. bei Dead Lock Detection) */ public function forceRelease(string $jobId): bool { $lockKey = self::LOCK_PREFIX . $jobId; $heartbeatKey = self::HEARTBEAT_PREFIX . $jobId; $deleted = $this->redis->delete($lockKey, $heartbeatKey); return $deleted > 0; } /** * Gibt alle aktiven Locks zurück */ public function getActiveLocks(): array { $pattern = self::LOCK_PREFIX . '*'; $keys = $this->redis->command('KEYS', $pattern); $locks = []; foreach ($keys as $key) { $jobId = str_replace(self::LOCK_PREFIX, '', $key); $lockInfo = $this->getLockInfo($jobId); if ($lockInfo !== null) { $locks[] = $lockInfo; } } return $locks; } private function generateOwnerId(): string { return sprintf( '%s:%s:%d', $this->serverId, gethostname(), getmypid() ); } }