diff --git a/src/Cache/Lock/PdoLockProvider.php b/src/Cache/Lock/PdoLockProvider.php index f219532..6524cb7 100644 --- a/src/Cache/Lock/PdoLockProvider.php +++ b/src/Cache/Lock/PdoLockProvider.php @@ -91,6 +91,11 @@ private static function advisoryKeys(string $value): array ]; } + private static function isDatabaseTrue(mixed $value): bool + { + return $value === true || $value === 1 || $value === '1' || $value === 't'; + } + private static function signedHex32(string $value): int { $unsigned = (int) hexdec($value); @@ -150,7 +155,7 @@ private function acquirePgsql(string $key, float $waitSeconds, float $leaseSecon $stmt = $this->pdo->prepare('SELECT pg_try_advisory_lock(:k1, :k2)'); $stmt->execute([':k1' => $advisoryKeys[0], ':k2' => $advisoryKeys[1]]); $result = $stmt->fetchColumn(); - if ($result === 1 || $result === 't' || $result === '1') { + if (self::isDatabaseTrue($result)) { $this->activeTokens[$lockKey] = $token; return new LockHandle($lockKey, $token, $advisoryKeys, $leaseSeconds); @@ -207,7 +212,7 @@ private function releasePgsql(LockHandle $handle): bool $stmt->execute([':k1' => $advisoryKeys[0], ':k2' => $advisoryKeys[1]]); $result = $stmt->fetchColumn(); - return $result === 1 || $result === 't' || $result === '1'; + return self::isDatabaseTrue($result); } catch (Throwable) { return false; } diff --git a/tests/Cache/LockProviderTest.php b/tests/Cache/LockProviderTest.php index bd428c6..1ec0f2d 100644 --- a/tests/Cache/LockProviderTest.php +++ b/tests/Cache/LockProviderTest.php @@ -76,3 +76,64 @@ } } }); + +test('PostgreSQL PDO locks accept native boolean results', function (): void { + $pdo = new class () extends PDO { + public function __construct() + { + } + + public function getAttribute(int $attribute): mixed + { + unset($attribute); + + return 'pgsql'; + } + + public function prepare(string $query, array $options = []): PDOStatement|false + { + unset($query, $options); + + return $this->successfulStatement(); + } + + public function query(string $query, ?int $fetchMode = null, mixed ...$fetchModeArgs): PDOStatement|false + { + unset($query, $fetchMode, $fetchModeArgs); + + return $this->successfulStatement(); + } + + private function successfulStatement(): PDOStatement + { + return new class () extends PDOStatement { + public function __construct() + { + } + + public function execute(?array $params = null): bool + { + unset($params); + + return true; + } + + public function fetchColumn(int $column = 0): mixed + { + unset($column); + + return true; + } + }; + } + }; + $provider = new PdoLockProvider($pdo); + $handle = $provider->acquire('worker:postgres', 0.0, 10.0); + + expect($handle)->not->toBeNull() + ->and($provider->refresh($handle, 10.0))->toBeTrue(); + + $provider->release($handle); + + expect($provider->refresh($handle, 10.0))->toBeFalse(); +}); diff --git a/tests/Cache/PdoPgsqlCachePoolTest.php b/tests/Cache/PdoPgsqlCachePoolTest.php index 2f010f2..6a53d61 100644 --- a/tests/Cache/PdoPgsqlCachePoolTest.php +++ b/tests/Cache/PdoPgsqlCachePoolTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Infocyph\CacheLayer\Cache\Cache; +use Infocyph\CacheLayer\Cache\Lock\PdoLockProvider; if (! in_array('pgsql', PDO::getAvailableDrivers(), true)) { test('PostgreSQL PDO driver not present')->skip(); @@ -56,3 +57,24 @@ ->and($this->cache->get('a'))->toBeNull() ->and($this->cache->get('b'))->toBe('B'); }); + +test('PostgreSQL advisory locks accept native PDO booleans and release ownership', function () use ($dsn, $user, $pass) { + $connect = static function () use ($dsn, $user, $pass): PDO { + return new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); + }; + $holder = new PdoLockProvider($connect(), 'cachelayer:test:'); + $contender = new PdoLockProvider($connect(), 'cachelayer:test:'); + $key = 'postgres-lock-' . bin2hex(random_bytes(8)); + $handle = $holder->acquire($key, 0.0, 5.0); + + expect($handle)->not->toBeNull() + ->and($holder->refresh($handle, 5.0))->toBeTrue() + ->and($contender->acquire($key, 0.0, 5.0))->toBeNull(); + + $holder->release($handle); + + $replacement = $contender->acquire($key, 0.0, 5.0); + expect($holder->refresh($handle, 5.0))->toBeFalse() + ->and($replacement)->not->toBeNull(); + $contender->release($replacement); +});