::", claimed with an * atomic INSERT IGNORE (Options::claim()). While it is held, nothing starts a run. A lock whose heartbeat (the latest * of `since`, the run's and every lane's heartbeat_at) is older than 10 minutes belongs to a run that died; the next * trigger deletes it with compare-and-delete and claims it. Finishing a generation moves the value from g to g+1 with * compare-and-swap, so of several finish actions exactly one goes on. A lock value that is not a run's (a garbled or * foreign row) is replaced the same way at once, so it can never block every run. */ final class SyncLock { const STALE_AFTER = 600; const PATTERN = '/^([0-9a-f-]{36}):(\d+):(\d+)$/'; const RUN_ID_PATTERN = '/^[0-9a-f-]{36}$/'; /** * Claims the lock for run $run_id (a lowercase UUID). Any other id is refused without a write: stored, it would * read as a malformed lock, which the next trigger replaces at once even though the run is alive. */ public static function acquire( string $run_id ): bool { if ( 1 !== preg_match( self::RUN_ID_PATTERN, $run_id ) ) { return false; } $value = self::value( $run_id, 0 ); if ( Options::claim( Options::SYNC_LOCK, $value ) ) { return true; } $held = Options::raw( Options::SYNC_LOCK ); if ( null === $held ) { return Options::claim( Options::SYNC_LOCK, $value ); // Released in between. } $malformed = null === self::parse( $held ); if ( ! $malformed && ! self::is_stale() ) { return false; } if ( ! Options::compare_and_delete( Options::SYNC_LOCK, $held ) ) { return false; // Another trigger took it over first. } Log::warning( $malformed ? 'Approved-product sync: the lock held a value that is not a run; a new run replaces it.' : 'Approved-product sync: the previous run made no progress for 10 minutes; a new run replaces it.' ); return Options::claim( Options::SYNC_LOCK, $value ); } /** * @return array{run_id: string, gen: int, since: int}|null */ public static function held(): ?array { $held = Options::raw( Options::SYNC_LOCK ); return null === $held ? null : self::parse( $held ); } /** * The lock value's parts, or null when it is not a run's "::". * * @return array{run_id: string, gen: int, since: int}|null */ private static function parse( string $held ): ?array { if ( 1 !== preg_match( self::PATTERN, $held, $match ) ) { return null; } return array( 'run_id' => $match[1], 'gen' => (int) $match[2], 'since' => (int) $match[3], ); } /** * Whether the lock is generation $gen of run $run_id right now (read from the table on every call). * * @phpstan-impure */ public static function is_held_by( string $run_id, int $gen ): bool { $held = Options::raw( Options::SYNC_LOCK ); return null !== $held && self::is_value_of( $held, $run_id, $gen ); } /** * Whether lock value $held is generation $gen of run $run_id. */ private static function is_value_of( string $held, string $run_id, int $gen ): bool { $parts = self::parse( $held ); return null !== $parts && $parts['run_id'] === $run_id && $parts['gen'] === $gen; } public static function advance( string $run_id, int $gen ): bool { $held = Options::raw( Options::SYNC_LOCK ); if ( null === $held || ! self::is_value_of( $held, $run_id, $gen ) ) { return false; } return Options::compare_and_swap( Options::SYNC_LOCK, $held, self::value( $run_id, $gen + 1 ) ); } public static function release( string $run_id, int $gen ): void { $held = Options::raw( Options::SYNC_LOCK ); if ( null !== $held && self::is_value_of( $held, $run_id, $gen ) ) { Options::compare_and_delete( Options::SYNC_LOCK, $held ); } } public static function is_stale(): bool { $held = self::held(); return null !== $held && time() - max( $held['since'], SyncState::heartbeat_at() ) > self::STALE_AFTER; } public static function clear(): void { delete_option( Options::SYNC_LOCK ); } private static function value( string $run_id, int $gen ): string { return $run_id . ':' . $gen . ':' . time(); } }