clock = $clock ?? static fn(): string => current_time( 'mysql', true ); } public function set_clock( callable $clock ): void { $this->clock = $clock; } /** * The token this instance holds, or null when it does not hold the lock. * Callers embed it in guarded row updates so they apply only while the claim is theirs. */ public function token(): ?string { return $this->token; } private function now(): string { return ( $this->clock )(); } private function fresh_token(): string { return bin2hex( random_bytes( 16 ) ); } /** * Claim the lock for a migration. Returns true only when this call won. */ public function claim( int $migration_id ): bool { $wpdb = $this->wpdb; $now = $this->now(); $expires = gmdate( 'Y-m-d H:i:s', strtotime( $now ) + self::DEFAULT_TTL ); $token = $this->fresh_token(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $affected = (int) $wpdb->query( $wpdb->prepare( 'UPDATE %i SET worker_lock_owner = %s, worker_lock_expires = %s, updated_at = %s WHERE id = %d AND (worker_lock_owner IS NULL OR worker_lock_expires IS NULL OR worker_lock_expires < %s)', $wpdb->prefix . 'shiftora_migrations', $token, $expires, $now, $migration_id, $now ) ); if ( $affected > 0 ) { $this->token = $token; } return $affected > 0; } /** * Release the lock this instance holds. Scoped to the token so a stale * worker cannot release a newer claim. */ public function release( int $migration_id ): void { $wpdb = $this->wpdb; if ( null === $this->token ) { return; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom table, fresh read required $wpdb->query( $wpdb->prepare( 'UPDATE %i SET worker_lock_owner = NULL, worker_lock_expires = NULL, updated_at = %s WHERE id = %d AND worker_lock_owner = %s', $wpdb->prefix . 'shiftora_migrations', $this->now(), $migration_id, $this->token ) ); $this->token = null; } }