From 58e119f6a8619cc53254b4cf117291114be79b6f Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 2 Aug 2025 11:07:42 +0300 Subject: [PATCH 01/15] Leave only *Handler API --- README.md | 4 +- src/Postgres/PostgresAdvisoryLocker.php | 45 +------------- .../Postgres/PostgresAdvisoryLockerTest.php | 59 ++++++++++--------- 3 files changed, 34 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 4ef4186..254c59a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ $locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); $lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); $dbConnection->beginTransaction(); -$lock = $locker->acquireSessionLevelLockHandler( +$lock = $locker->acquireSessionLevelLock( $dbConnection, $lockId, \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, @@ -61,7 +61,7 @@ $locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); $lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); try { - $lock = $locker->acquireSessionLevelLockHandler( + $lock = $locker->acquireSessionLevelLock( $dbConnection, $lockId, \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 6ab3dc0..69a30d1 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -28,7 +28,7 @@ final class PostgresAdvisoryLocker * * TODO: Cover with tests */ - public function acquireTransactionLevelLockHandler( + public function acquireTransactionLevelLock( PDO $dbConnection, PostgresLockKey $postgresLockId, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, @@ -45,33 +45,13 @@ public function acquireTransactionLevelLockHandler( ); } - /** - * Acquire a transaction-level advisory lock with configurable wait and access modes. - * - * TODO: Do we need low-level API? - */ - public function acquireTransactionLevelLock( - PDO $dbConnection, - PostgresLockKey $postgresLockId, - PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, - PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): bool { - return $this->acquireLock( - $dbConnection, - $postgresLockId, - PostgresLockLevelEnum::Transaction, - $waitMode, - $accessMode, - ); - } - /** * Acquire a session-level advisory lock with configurable wait and access modes. * * TODO: Write that transaction-level is recommended. * TODO: Cover with tests */ - public function acquireSessionLevelLockHandler( + public function acquireSessionLevelLock( PDO $dbConnection, PostgresLockKey $postgresLockId, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, @@ -92,27 +72,6 @@ public function acquireSessionLevelLockHandler( ); } - /** - * Acquire a session-level advisory lock with configurable wait and access modes. - * - * TODO: Write that transaction-level is recommended. - * TODO: Do we need low-level API? - */ - public function acquireSessionLevelLock( - PDO $dbConnection, - PostgresLockKey $postgresLockId, - PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, - PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): bool { - return $this->acquireLock( - $dbConnection, - $postgresLockId, - PostgresLockLevelEnum::Session, - $waitMode, - $accessMode, - ); - } - /** * Release session level advisory lock. */ diff --git a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php index c7ec210..7beeef0 100644 --- a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php @@ -39,7 +39,7 @@ public function testItCanTryAcquireLockWithinSession( accessMode: $accessMode, ); - $this->assertTrue($isLockAcquired); + $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId, $accessMode); } @@ -71,7 +71,7 @@ public function testItCanTryAcquireLockWithinTransaction( accessMode: $accessMode, ); - $this->assertTrue($isLockAcquired); + $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId, $accessMode); } @@ -100,7 +100,7 @@ public function testItCanTryAcquireLockFromIntKeysCornerCases(): void $postgresLockId, ); - $this->assertTrue($isLockAcquired); + $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } @@ -133,17 +133,17 @@ public function testItCanTryAcquireLockInSameConnectionOnlyOnce(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $isLockAcquired1 = $locker->acquireSessionLevelLock( + $isLock1Acquired = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, ); - $isLockAcquired2 = $locker->acquireSessionLevelLock( + $isLock2Acquired = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, ); - $this->assertTrue($isLockAcquired1); - $this->assertTrue($isLockAcquired2); + $this->assertTrue($isLock1Acquired->wasAcquired); + $this->assertTrue($isLock2Acquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } @@ -164,8 +164,8 @@ public function testItCanTryAcquireMultipleLocksInOneConnection(): void $postgresLockId2, ); - $this->assertTrue($isLock1Acquired); - $this->assertTrue($isLock2Acquired); + $this->assertTrue($isLock1Acquired->wasAcquired); + $this->assertTrue($isLock2Acquired->wasAcquired); $this->assertPgAdvisoryLocksCount(2); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); @@ -177,17 +177,18 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + // TODO: Fix corner-case: without variable its self-destructing instantly + $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId, ); - $isLockAcquired = $locker->acquireSessionLevelLock( + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, $postgresLockId, ); - $this->assertFalse($isLockAcquired); + $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); } @@ -199,7 +200,7 @@ public function testItCanReleaseLock( $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + $connectionLock = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, accessMode: $accessMode, @@ -235,7 +236,7 @@ public function testItCanNotReleaseLockOfDifferentModes( $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + $connectionLock = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, accessMode: $acquireMode, @@ -271,11 +272,11 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + $connectionLock1 = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, ); - $locker->acquireSessionLevelLock( + $connectionLock2 = $locker->acquireSessionLevelLock( $dbConnection, $postgresLockId, ); @@ -308,7 +309,7 @@ public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void $postgresLockId, ); - $this->assertTrue($isLockAcquired); + $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId); } @@ -319,23 +320,23 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + $connection1Lock1 = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId, ); - $locker->acquireSessionLevelLock( + $connection1Lock2 = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId, ); $isLockReleased = $locker->releaseSessionLevelLock($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->acquireSessionLevelLock( + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, $postgresLockId, ); $this->assertTrue($isLockReleased); - $this->assertFalse($isLockAcquired); + $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); @@ -359,7 +360,7 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); - $locker->acquireSessionLevelLock( + $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId, ); @@ -412,19 +413,19 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherConnectionLocks $postgresLockId2 = PostgresLockKey::create('test2'); $postgresLockId3 = PostgresLockKey::create('test3'); $postgresLockId4 = PostgresLockKey::create('test4'); - $locker->acquireSessionLevelLock( + $connect1Lock1 = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId1, ); - $locker->acquireSessionLevelLock( + $connect1Lock2 = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId2, ); - $locker->acquireSessionLevelLock( + $connect2Lock3 = $locker->acquireSessionLevelLock( $dbConnection2, $postgresLockId3, ); - $locker->acquireSessionLevelLock( + $connect2Lock4 = $locker->acquireSessionLevelLock( $dbConnection2, $postgresLockId4, ); @@ -460,17 +461,17 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockKey::create('test'); $dbConnection1->beginTransaction(); - $locker->acquireSessionLevelLock( + $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, $postgresLockId, ); - $isLockAcquired = $locker->acquireSessionLevelLock( + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, $postgresLockId, ); - $this->assertFalse($isLockAcquired); + $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); } From b72f27b765ef17bf12ff06810980e8c7652ca314 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 3 Aug 2025 08:15:52 +0300 Subject: [PATCH 02/15] Leave only *Handler API --- src/Postgres/PostgresAdvisoryLocker.php | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 69a30d1..9d1a0d1 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -25,8 +25,6 @@ final class PostgresAdvisoryLocker { /** * Acquire a transaction-level advisory lock with configurable wait and access modes. - * - * TODO: Cover with tests */ public function acquireTransactionLevelLock( PDO $dbConnection, @@ -48,8 +46,24 @@ public function acquireTransactionLevelLock( /** * Acquire a session-level advisory lock with configurable wait and access modes. * - * TODO: Write that transaction-level is recommended. - * TODO: Cover with tests + * ⚠️ You MUST retain the returned handle in a variable. + * If the handle is not stored and is immediately garbage collected, + * the lock will be released in the lock handle __destruct method. + * + * @example + * $handle = $locker->acquireSessionLevelLock(...); // ✅ Lock held + * + * $locker->acquireSessionLevelLock(...); // ❌ Lock immediately released + * + * ⚠️ Transaction-level advisory locks are strongly preferred over session-level locks. + * Session-level locks persist beyond transactions and may lead to deadlocks + * or require manual cleanup (e.g. `pg_advisory_unlock_all()`). + * + * Use session-level locks only when transactional locks are not suitable + * (transactions are not possible or redundant). + * + * @see SessionLevelLockHandle::__destruct + * @see acquireTransactionLevelLock() for preferred locking strategy. */ public function acquireSessionLevelLock( PDO $dbConnection, From a9e0c84363c493a4e3d3532d7b4881fe1c506be8 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 3 Aug 2025 08:34:43 +0300 Subject: [PATCH 03/15] Leave only *Handler API --- src/Postgres/PostgresAdvisoryLocker.php | 87 ++++++++++++++++++------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 9d1a0d1..dd201f3 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -28,14 +28,14 @@ final class PostgresAdvisoryLocker */ public function acquireTransactionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): TransactionLevelLockHandle { return new TransactionLevelLockHandle( wasAcquired: $this->acquireLock( $dbConnection, - $postgresLockId, + $postgresLockKey, PostgresLockLevelEnum::Transaction, $waitMode, $accessMode, @@ -49,36 +49,32 @@ public function acquireTransactionLevelLock( * ⚠️ You MUST retain the returned handle in a variable. * If the handle is not stored and is immediately garbage collected, * the lock will be released in the lock handle __destruct method. + * @see SessionLevelLockHandle::__destruct * * @example * $handle = $locker->acquireSessionLevelLock(...); // ✅ Lock held * * $locker->acquireSessionLevelLock(...); // ❌ Lock immediately released * - * ⚠️ Transaction-level advisory locks are strongly preferred over session-level locks. - * Session-level locks persist beyond transactions and may lead to deadlocks - * or require manual cleanup (e.g. `pg_advisory_unlock_all()`). - * - * Use session-level locks only when transactional locks are not suitable - * (transactions are not possible or redundant). - * - * @see SessionLevelLockHandle::__destruct + * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, + * as they are automatically released at the end of a transaction and are less error-prone. + * Use session-level locks only when transactional context is not available. * @see acquireTransactionLevelLock() for preferred locking strategy. */ public function acquireSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): SessionLevelLockHandle { return new SessionLevelLockHandle( $dbConnection, $this, - $postgresLockId, + $postgresLockKey, $accessMode, wasAcquired: $this->acquireLock( $dbConnection, - $postgresLockId, + $postgresLockKey, PostgresLockLevelEnum::Session, $waitMode, $accessMode, @@ -86,25 +82,72 @@ public function acquireSessionLevelLock( ); } + /** + * Acquires a session-level advisory lock and ensures its release after executing the callback. + * + * This method guarantees that the lock is released even if an exception is thrown during execution. + * Useful for safely wrapping critical sections that require locking. + * + * If the lock was not acquired (i.e., `wasAcquired` is `false`), it is up to the callback + * to decide how to handle the situation (e.g., retry, throw, log, or silently skip). + * + * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, + * as they are automatically released at the end of a transaction and are less error-prone. + * Use session-level locks only when transactional context is not available. + * @see acquireTransactionLevelLock() for preferred locking strategy. + * + * @param PDO $dbConnection Active database connection. + * @param PostgresLockKey $postgresLockKey Lock key to be acquired. + * @param callable(SessionLevelLockHandle): TReturn $callback A callback that receives the lock handle. + * @param PostgresLockWaitModeEnum $waitMode Whether to wait for the lock or fail immediately. Default is non-blocking. + * @param PostgresLockAccessModeEnum $accessMode Whether to acquire a shared or exclusive lock. Default is exclusive. + * @return TReturn The return value of the callback. + * + * @template TReturn + * + * TODO: Cover with tests + */ + public function withSessionLevelLock( + PDO $dbConnection, + PostgresLockKey $postgresLockKey, + callable $callback, + PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, + PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, + ): mixed { + $lockHandle = $this->acquireSessionLevelLock( + $dbConnection, + $postgresLockKey, + $waitMode, + $accessMode, + ); + + try { + return $callback($lockHandle); + } + finally { + $lockHandle->release(); + } + } + /** * Release session level advisory lock. */ public function releaseSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): bool { $sql = match ($accessMode) { PostgresLockAccessModeEnum::Exclusive => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id);', PostgresLockAccessModeEnum::Share => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id);', }; - $sql .= " -- $postgresLockId->humanReadableValue"; + $sql .= " -- $postgresLockKey->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockId->classId, - 'object_id' => $postgresLockId->objectId, + 'class_id' => $postgresLockKey->classId, + 'object_id' => $postgresLockKey->objectId, ], ); @@ -127,14 +170,14 @@ public function releaseAllSessionLevelLocks( private function acquireLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockLevelEnum $level, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): bool { if ($level === PostgresLockLevelEnum::Transaction && $dbConnection->inTransaction() === false) { throw new LogicException( - "Transaction-level advisory lock `$postgresLockId->humanReadableValue` cannot be acquired outside of transaction", + "Transaction-level advisory lock `$postgresLockKey->humanReadableValue` cannot be acquired outside of transaction", ); } @@ -180,13 +223,13 @@ private function acquireLock( PostgresLockAccessModeEnum::Share, ] => 'SELECT PG_ADVISORY_LOCK_SHARED(:class_id, :object_id);', }; - $sql .= " -- $postgresLockId->humanReadableValue"; + $sql .= " -- $postgresLockKey->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockId->classId, - 'object_id' => $postgresLockId->objectId, + 'class_id' => $postgresLockKey->classId, + 'object_id' => $postgresLockKey->objectId, ], ); From e37960f6bb99d6d5067054686d09ca881d861e1a Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 3 Aug 2025 08:39:14 +0300 Subject: [PATCH 04/15] Leave only *Handler API --- src/Postgres/PostgresAdvisoryLocker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index dd201f3..4f9c4b4 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -107,7 +107,7 @@ public function acquireSessionLevelLock( * * TODO: Cover with tests */ - public function withSessionLevelLock( + public function withinSessionLevelLock( PDO $dbConnection, PostgresLockKey $postgresLockKey, callable $callback, From 0205b4b83f8364af0c70bcb9984565d07feed58b Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 3 Aug 2025 08:46:43 +0300 Subject: [PATCH 05/15] Leave only *Handler API --- .../LockHandle/SessionLevelLockHandle.php | 4 +- .../AbstractIntegrationTestCase.php | 22 +-- .../Postgres/PostgresAdvisoryLockerTest.php | 174 +++++++++--------- test/Unit/Postgres/PostgresLockKeyTest.php | 36 ++-- 4 files changed, 118 insertions(+), 118 deletions(-) diff --git a/src/Postgres/LockHandle/SessionLevelLockHandle.php b/src/Postgres/LockHandle/SessionLevelLockHandle.php index e3298c3..23b0fdc 100644 --- a/src/Postgres/LockHandle/SessionLevelLockHandle.php +++ b/src/Postgres/LockHandle/SessionLevelLockHandle.php @@ -28,7 +28,7 @@ final class SessionLevelLockHandle public function __construct( private readonly PDO $dbConnection, private readonly PostgresAdvisoryLocker $locker, - public readonly PostgresLockKey $lockId, + public readonly PostgresLockKey $lockKey, public readonly PostgresLockAccessModeEnum $accessMode, public readonly bool $wasAcquired, ) {} @@ -49,7 +49,7 @@ public function release(): bool $wasReleased = $this->locker->releaseSessionLevelLock( $this->dbConnection, - $this->lockId, + $this->lockKey, ); if ($wasReleased) { diff --git a/test/Integration/AbstractIntegrationTestCase.php b/test/Integration/AbstractIntegrationTestCase.php index cbc1e73..914b7a2 100644 --- a/test/Integration/AbstractIntegrationTestCase.php +++ b/test/Integration/AbstractIntegrationTestCase.php @@ -44,39 +44,39 @@ protected function initPostgresPdoConnection(): PDO protected function assertPgAdvisoryLockExistsInConnection( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockAccessModeEnum $mode = PostgresLockAccessModeEnum::Exclusive, ): void { $row = $this->findPostgresAdvisoryLockInConnection( $dbConnection, - $postgresLockId, + $postgresLockKey, $mode, ); - $lockIdString = $postgresLockId->humanReadableValue; + $lockKeyString = $postgresLockKey->humanReadableValue; $this->assertTrue( $row !== null, - "Lock id `$lockIdString` does not exists", + "Lock id `$lockKeyString` does not exists", ); } protected function assertPgAdvisoryLockMissingInConnection( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockAccessModeEnum $mode = PostgresLockAccessModeEnum::Exclusive, ): void { $row = $this->findPostgresAdvisoryLockInConnection( $dbConnection, - $postgresLockId, + $postgresLockKey, $mode, ); - $lockIdString = $postgresLockId->humanReadableValue; + $lockKeyString = $postgresLockKey->humanReadableValue; $this->assertTrue( $row === null, - "Lock id `$lockIdString` is present", + "Lock id `$lockKeyString` is present", ); } @@ -95,7 +95,7 @@ protected function assertPgAdvisoryLocksCount( private function findPostgresAdvisoryLockInConnection( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $postgresLockKey, PostgresLockAccessModeEnum $mode, ): object | null { $statement = $dbConnection->prepare( @@ -112,8 +112,8 @@ private function findPostgresAdvisoryLockInConnection( ); $statement->execute( [ - 'lock_class_id' => $postgresLockId->classId, - 'lock_object_id' => $postgresLockId->objectId, + 'lock_class_id' => $postgresLockKey->classId, + 'lock_object_id' => $postgresLockKey->objectId, 'lock_object_subid' => 2, // Using two keyed locks 'connection_pid' => $dbConnection->pgsqlGetPid(), 'mode' => $mode->value, diff --git a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php index 7beeef0..c67dc31 100644 --- a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php @@ -31,17 +31,17 @@ public function testItCanTryAcquireLockWithinSession( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $isLockAcquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId, $accessMode); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey, $accessMode); } public static function provideItCanTryAcquireLockWithinSessionData(): array @@ -62,18 +62,18 @@ public function testItCanTryAcquireLockWithinTransaction( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection->beginTransaction(); $isLockAcquired = $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId, $accessMode); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey, $accessMode); } public static function provideItCanTryAcquireLockWithinTransactionData(): array @@ -93,16 +93,16 @@ public function testItCanTryAcquireLockFromIntKeysCornerCases(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::createFromInternalIds(self::DB_INT32_VALUE_MIN, 0); + $lockKey = PostgresLockKey::createFromInternalIds(self::DB_INT32_VALUE_MIN, 0); $isLockAcquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey); } public static function provideItCanTryAcquireLockFromIntKeysCornerCasesData(): array @@ -131,44 +131,44 @@ public function testItCanTryAcquireLockInSameConnectionOnlyOnce(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $isLock1Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $isLock2Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $this->assertTrue($isLock1Acquired->wasAcquired); $this->assertTrue($isLock2Acquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey); } public function testItCanTryAcquireMultipleLocksInOneConnection(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = PostgresLockKey::create('test1'); - $postgresLockId2 = PostgresLockKey::create('test2'); + $lockKey1 = PostgresLockKey::create('test1'); + $lockKey2 = PostgresLockKey::create('test2'); $isLock1Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId1, + $lockKey1, ); $isLock2Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId2, + $lockKey2, ); $this->assertTrue($isLock1Acquired->wasAcquired); $this->assertTrue($isLock2Acquired->wasAcquired); $this->assertPgAdvisoryLocksCount(2); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey2); } public function testItCannotAcquireSameLockInTwoConnections(): void @@ -176,21 +176,21 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); // TODO: Fix corner-case: without variable its self-destructing instantly $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $lockKey); } #[DataProvider('provideItCanReleaseLockData')] @@ -199,16 +199,16 @@ public function testItCanReleaseLock( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $connectionLock = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); $isLockReleased = $locker->releaseSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); @@ -235,22 +235,22 @@ public function testItCanNotReleaseLockOfDifferentModes( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $connectionLock = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $acquireMode, ); $isLockReleased = $locker->releaseSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $releaseMode, ); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId, $acquireMode); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey, $acquireMode); } public static function provideItCanNotReleaseLockOfDifferentModesData(): array @@ -271,18 +271,18 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $connectionLock1 = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $connectionLock2 = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); - $isLockReleased1 = $locker->releaseSessionLevelLock($dbConnection, $postgresLockId); - $isLockReleased2 = $locker->releaseSessionLevelLock($dbConnection, $postgresLockId); + $isLockReleased1 = $locker->releaseSessionLevelLock($dbConnection, $lockKey); + $isLockReleased2 = $locker->releaseSessionLevelLock($dbConnection, $lockKey); $this->assertTrue($isLockReleased1); $this->assertTrue($isLockReleased2); @@ -294,24 +294,24 @@ public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $locker->releaseSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $isLockAcquired = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $lockKey); } public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLocked(): void @@ -319,36 +319,36 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $connection1Lock1 = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $connection1Lock2 = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); - $isLockReleased = $locker->releaseSessionLevelLock($dbConnection1, $postgresLockId); + $isLockReleased = $locker->releaseSessionLevelLock($dbConnection1, $lockKey); $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); $this->assertTrue($isLockReleased); $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $lockKey); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $lockKey); } public function testItCannotReleaseLockIfNotAcquired(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); - $isLockReleased = $locker->releaseSessionLevelLock($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseSessionLevelLock($dbConnection, $lockKey); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(0); @@ -359,17 +359,17 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); - $isLockReleased = $locker->releaseSessionLevelLock($dbConnection2, $postgresLockId); + $isLockReleased = $locker->releaseSessionLevelLock($dbConnection2, $lockKey); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $lockKey); } public function testItCanReleaseAllLocksInConnection(): void @@ -409,32 +409,32 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherConnectionLocks $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId1 = PostgresLockKey::create('test'); - $postgresLockId2 = PostgresLockKey::create('test2'); - $postgresLockId3 = PostgresLockKey::create('test3'); - $postgresLockId4 = PostgresLockKey::create('test4'); + $lockKey1 = PostgresLockKey::create('test'); + $lockKey2 = PostgresLockKey::create('test2'); + $lockKey3 = PostgresLockKey::create('test3'); + $lockKey4 = PostgresLockKey::create('test4'); $connect1Lock1 = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId1, + $lockKey1, ); $connect1Lock2 = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId2, + $lockKey2, ); $connect2Lock3 = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId3, + $lockKey3, ); $connect2Lock4 = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId4, + $lockKey4, ); $locker->releaseAllSessionLevelLocks($dbConnection1); $this->assertPgAdvisoryLocksCount(2); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId3); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId4); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $lockKey3); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $lockKey4); } public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void @@ -446,11 +446,11 @@ public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); } @@ -459,66 +459,66 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection1->beginTransaction(); $connection1Lock = $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $lockKey); } public function testItCanAutoReleaseLockAcquiredWithinTransactionOnCommit(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection->beginTransaction(); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $dbConnection->commit(); $this->assertPgAdvisoryLocksCount(0); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $lockKey); } public function testItCanAutoReleaseLockAcquiredWithinTransactionOnRollback(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection->beginTransaction(); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $dbConnection->rollBack(); $this->assertPgAdvisoryLocksCount(0); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $lockKey); } public function testItCanAutoReleaseLockAcquiredWithinTransactionOnConnectionKill(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection->beginTransaction(); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $dbConnection = null; @@ -530,41 +530,41 @@ public function testItCannotReleaseLockAcquiredWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection->beginTransaction(); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); - $isLockReleased = $locker->releaseSessionLevelLock($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseSessionLevelLock($dbConnection, $lockKey); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey); } public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = PostgresLockKey::create('test'); - $postgresLockId2 = PostgresLockKey::create('test2'); + $lockKey1 = PostgresLockKey::create('test'); + $lockKey2 = PostgresLockKey::create('test2'); $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId1, + $lockKey1, ); $dbConnection->beginTransaction(); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId2, + $lockKey2, ); $locker->releaseAllSessionLevelLocks($dbConnection); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $lockKey1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey2); } private function initLocker(): PostgresAdvisoryLocker diff --git a/test/Unit/Postgres/PostgresLockKeyTest.php b/test/Unit/Postgres/PostgresLockKeyTest.php index 4adb626..93d3d85 100644 --- a/test/Unit/Postgres/PostgresLockKeyTest.php +++ b/test/Unit/Postgres/PostgresLockKeyTest.php @@ -22,20 +22,20 @@ final class PostgresLockKeyTest extends AbstractUnitTestCase private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - #[DataProvider('provideItCanCreatePostgresLockIdFromKeyValueData')] - public function testItCanCreatePostgresLockIdFromKeyValue( + #[DataProvider('provideItCanCreatePostgresLockKeyFromNamespaceValueData')] + public function testItCanCreatePostgresLockKeyFromNamespaceValue( string $key, string $value, int $expectedClassId, int $expectedObjectId, ): void { - $postgresLockId = PostgresLockKey::create($key, $value); + $lockKey = PostgresLockKey::create($key, $value); - $this->assertSame($expectedClassId, $postgresLockId->classId); - $this->assertSame($expectedObjectId, $postgresLockId->objectId); + $this->assertSame($expectedClassId, $lockKey->classId); + $this->assertSame($expectedObjectId, $lockKey->objectId); } - public static function provideItCanCreatePostgresLockIdFromKeyValueData(): array + public static function provideItCanCreatePostgresLockKeyFromNamespaceValueData(): array { return [ 'key + empty value' => [ @@ -53,18 +53,18 @@ public static function provideItCanCreatePostgresLockIdFromKeyValueData(): array ]; } - #[DataProvider('provideItCanCreatePostgresLockIdFromIntKeysData')] - public function testItCanCreatePostgresLockIdFromIntKeys( + #[DataProvider('provideItCanCreatePostgresLockKeyFromIntKeysData')] + public function testItCanCreatePostgresLockKeyFromIntKeys( int $classId, int $objectId, ): void { - $lockId = PostgresLockKey::createFromInternalIds($classId, $objectId); + $lockKey = PostgresLockKey::createFromInternalIds($classId, $objectId); - $this->assertSame($classId, $lockId->classId); - $this->assertSame($objectId, $lockId->objectId); + $this->assertSame($classId, $lockKey->classId); + $this->assertSame($objectId, $lockKey->objectId); } - public static function provideItCanCreatePostgresLockIdFromIntKeysData(): array + public static function provideItCanCreatePostgresLockKeyFromIntKeysData(): array { return [ 'min class_id' => [ @@ -86,8 +86,8 @@ public static function provideItCanCreatePostgresLockIdFromIntKeysData(): array ]; } - #[DataProvider('provideItCanCreatePostgresLockIdFromOutOfRangeIntKeysData')] - public function testItCanNotCreatePostgresLockIdFromOutOfRangeIntKeys( + #[DataProvider('provideItCanCreatePostgresLockKeyFromOutOfRangeIntKeysData')] + public function testItCanNotCreatePostgresLockKeyFromOutOfRangeIntKeys( int $classId, int $objectId, string $expectedExceptionMessage, @@ -95,13 +95,13 @@ public function testItCanNotCreatePostgresLockIdFromOutOfRangeIntKeys( $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $lockId = PostgresLockKey::createFromInternalIds($classId, $objectId); + $lockKey = PostgresLockKey::createFromInternalIds($classId, $objectId); - $this->assertSame($classId, $lockId->classId); - $this->assertSame($objectId, $lockId->objectId); + $this->assertSame($classId, $lockKey->classId); + $this->assertSame($objectId, $lockKey->objectId); } - public static function provideItCanCreatePostgresLockIdFromOutOfRangeIntKeysData(): array + public static function provideItCanCreatePostgresLockKeyFromOutOfRangeIntKeysData(): array { return [ 'min class_id' => [ From 8cf80142aff7cd63ec844d6d78fb2a09412c5b21 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 4 Aug 2025 09:23:11 +0300 Subject: [PATCH 06/15] Leave only *Handler API --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 254c59a..6c48ef5 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,30 @@ $dbConnection->commit(); #### Session-level advisory lock +Callback API + +```php +$dbConnection = new PDO($dsn, $username, $password); + +$locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); +$lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); + +$lock = $locker->withinSessionLevelLock( + $dbConnection, + $lockId, + function (\Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle $lock): Payment { + if ($lock->wasAcquired) { + // Execute logic if lock was successful + } else { + // Execute logic if lock acquisition has been failed + } + } + \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, + \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, +); +``` + +Low-level API ```php $dbConnection = new PDO($dsn, $username, $password); From 0a477c33a34c43944f8e0229ebf992171120e249 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 4 Aug 2025 09:26:30 +0300 Subject: [PATCH 07/15] Leave only *Handler API --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c48ef5..64bd582 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,18 @@ $dbConnection = new PDO($dsn, $username, $password); $locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); $lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); -$lock = $locker->withinSessionLevelLock( +$payment = $locker->withinSessionLevelLock( $dbConnection, $lockId, - function (\Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle $lock): Payment { + function ( + \Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle $lock, + ): Payment { // Define a type of $payment variable, and it will be resolved by analyzers if ($lock->wasAcquired) { // Execute logic if lock was successful } else { // Execute logic if lock acquisition has been failed } - } + }, \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, ); From 67e8dff7b190e177803feb78880e939b08dc98f6 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Mon, 4 Aug 2025 09:28:37 +0300 Subject: [PATCH 08/15] Leave only *Handler API --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64bd582..71a6793 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ $payment = $locker->withinSessionLevelLock( $lockId, function ( \Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle $lock, - ): Payment { // Define a type of $payment variable, and it will be resolved by analyzers + ): Payment { // Define a type of $payment variable, so it will be resolved by analyzers if ($lock->wasAcquired) { // Execute logic if lock was successful } else { From 256adaad694de7ac936c341b29d8582fc41be16e Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Tue, 5 Aug 2025 12:17:09 +0300 Subject: [PATCH 09/15] Leave only *Handler API --- README.md | 23 ++++++------ .../Enum/PostgresLockAccessModeEnum.php | 8 ++-- src/Postgres/Enum/PostgresLockLevelEnum.php | 12 +++--- .../Enum/PostgresLockWaitModeEnum.php | 10 ++--- src/Postgres/PostgresAdvisoryLocker.php | 37 ++++++++++--------- .../AbstractIntegrationTestCase.php | 6 ++- 6 files changed, 51 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 71a6793..f7431a3 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,12 @@ Callback API $dbConnection = new PDO($dsn, $username, $password); $locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); -$lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); +$lockKey = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); $payment = $locker->withinSessionLevelLock( - $dbConnection, - $lockId, - function ( + dbConnection: $dbConnection, + key: $lockKey, + callback: function ( \Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle $lock, ): Payment { // Define a type of $payment variable, so it will be resolved by analyzers if ($lock->wasAcquired) { @@ -74,24 +74,25 @@ $payment = $locker->withinSessionLevelLock( // Execute logic if lock acquisition has been failed } }, - \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, - \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, + waitMode: \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, + accessMode: \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, ); ``` Low-level API + ```php $dbConnection = new PDO($dsn, $username, $password); $locker = new \Cog\DbLocker\Postgres\PostgresAdvisoryLocker(); -$lockId = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); +$lockKey = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); try { $lock = $locker->acquireSessionLevelLock( - $dbConnection, - $lockId, - \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, - \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, + dbConnection: $dbConnection, + key: $lockKey, + waitMode: \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, + accessMode: \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, ); if ($lock->wasAcquired) { // Execute logic if lock was successful diff --git a/src/Postgres/Enum/PostgresLockAccessModeEnum.php b/src/Postgres/Enum/PostgresLockAccessModeEnum.php index 8319ead..f6bbe4e 100644 --- a/src/Postgres/Enum/PostgresLockAccessModeEnum.php +++ b/src/Postgres/Enum/PostgresLockAccessModeEnum.php @@ -7,10 +7,10 @@ /** * PostgresLockAccessModeEnum defines the access mode of advisory lock acquisition. * - * TODO: Need string values only for tests, should add match to tests instead. + * TODO: Write details about access mode. */ -enum PostgresLockAccessModeEnum: string +enum PostgresLockAccessModeEnum { - case Exclusive = 'ExclusiveLock'; - case Share = 'ShareLock'; + case Exclusive; + case Share; } diff --git a/src/Postgres/Enum/PostgresLockLevelEnum.php b/src/Postgres/Enum/PostgresLockLevelEnum.php index 3b983c2..1e99efc 100644 --- a/src/Postgres/Enum/PostgresLockLevelEnum.php +++ b/src/Postgres/Enum/PostgresLockLevelEnum.php @@ -7,19 +7,19 @@ /** * PostgresLockLevelEnum defines the level of advisory lock acquisition. * + * - Transaction. Transaction-level (recommended) advisory lock (with _XACT_): + * - PG_ADVISORY_XACT_LOCK + * - PG_ADVISORY_XACT_LOCK_SHARED + * - PG_TRY_ADVISORY_XACT_LOCK + * - PG_TRY_ADVISORY_XACT_LOCK_SHARED * - Session. Session-level advisory lock (without _XACT_): * - PG_ADVISORY_LOCK * - PG_ADVISORY_LOCK_SHARED * - PG_TRY_ADVISORY_LOCK * - PG_TRY_ADVISORY_LOCK_SHARED - * - Transaction. Transaction-level advisory lock (with _XACT_): - * - PG_ADVISORY_XACT_LOCK - * - PG_ADVISORY_XACT_LOCK_SHARED - * - PG_TRY_ADVISORY_XACT_LOCK - * - PG_TRY_ADVISORY_XACT_LOCK_SHARED */ enum PostgresLockLevelEnum { - case Session; case Transaction; + case Session; } diff --git a/src/Postgres/Enum/PostgresLockWaitModeEnum.php b/src/Postgres/Enum/PostgresLockWaitModeEnum.php index 401ac89..41e3488 100644 --- a/src/Postgres/Enum/PostgresLockWaitModeEnum.php +++ b/src/Postgres/Enum/PostgresLockWaitModeEnum.php @@ -7,16 +7,16 @@ /** * PostgresLockWaitModeEnum defines the type of advisory lock acquisition. * - * - NonBlocking. Attempt to acquire the lock without blocking (with _TRY_): - * - PG_TRY_ADVISORY_LOCK - * - PG_TRY_ADVISORY_LOCK_SHARED - * - PG_TRY_ADVISORY_XACT_LOCK - * - PG_TRY_ADVISORY_XACT_LOCK_SHARED * - Blocking. Acquire the lock, blocking until it becomes available (without _TRY_): * - PG_ADVISORY_LOCK * - PG_ADVISORY_LOCK_SHARED * - PG_ADVISORY_XACT_LOCK * - PG_ADVISORY_XACT_LOCK_SHARED + * - NonBlocking. Attempt to acquire the lock without blocking (with _TRY_): + * - PG_TRY_ADVISORY_LOCK + * - PG_TRY_ADVISORY_LOCK_SHARED + * - PG_TRY_ADVISORY_XACT_LOCK + * - PG_TRY_ADVISORY_XACT_LOCK_SHARED */ enum PostgresLockWaitModeEnum { diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 4f9c4b4..ca1d753 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -28,14 +28,14 @@ final class PostgresAdvisoryLocker */ public function acquireTransactionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockKey, + PostgresLockKey $key, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): TransactionLevelLockHandle { return new TransactionLevelLockHandle( wasAcquired: $this->acquireLock( $dbConnection, - $postgresLockKey, + $key, PostgresLockLevelEnum::Transaction, $waitMode, $accessMode, @@ -63,18 +63,18 @@ public function acquireTransactionLevelLock( */ public function acquireSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockKey, + PostgresLockKey $key, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): SessionLevelLockHandle { return new SessionLevelLockHandle( $dbConnection, $this, - $postgresLockKey, + $key, $accessMode, wasAcquired: $this->acquireLock( $dbConnection, - $postgresLockKey, + $key, PostgresLockLevelEnum::Session, $waitMode, $accessMode, @@ -94,10 +94,9 @@ public function acquireSessionLevelLock( * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, * as they are automatically released at the end of a transaction and are less error-prone. * Use session-level locks only when transactional context is not available. - * @see acquireTransactionLevelLock() for preferred locking strategy. * * @param PDO $dbConnection Active database connection. - * @param PostgresLockKey $postgresLockKey Lock key to be acquired. + * @param PostgresLockKey $key Lock key to be acquired. * @param callable(SessionLevelLockHandle): TReturn $callback A callback that receives the lock handle. * @param PostgresLockWaitModeEnum $waitMode Whether to wait for the lock or fail immediately. Default is non-blocking. * @param PostgresLockAccessModeEnum $accessMode Whether to acquire a shared or exclusive lock. Default is exclusive. @@ -106,17 +105,19 @@ public function acquireSessionLevelLock( * @template TReturn * * TODO: Cover with tests + *@see acquireTransactionLevelLock() for preferred locking strategy. + * */ public function withinSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockKey, + PostgresLockKey $key, callable $callback, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): mixed { $lockHandle = $this->acquireSessionLevelLock( $dbConnection, - $postgresLockKey, + $key, $waitMode, $accessMode, ); @@ -134,20 +135,20 @@ public function withinSessionLevelLock( */ public function releaseSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockKey, + PostgresLockKey $key, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): bool { $sql = match ($accessMode) { PostgresLockAccessModeEnum::Exclusive => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id);', PostgresLockAccessModeEnum::Share => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id);', }; - $sql .= " -- $postgresLockKey->humanReadableValue"; + $sql .= " -- $key->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockKey->classId, - 'object_id' => $postgresLockKey->objectId, + 'class_id' => $key->classId, + 'object_id' => $key->objectId, ], ); @@ -170,14 +171,14 @@ public function releaseAllSessionLevelLocks( private function acquireLock( PDO $dbConnection, - PostgresLockKey $postgresLockKey, + PostgresLockKey $key, PostgresLockLevelEnum $level, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): bool { if ($level === PostgresLockLevelEnum::Transaction && $dbConnection->inTransaction() === false) { throw new LogicException( - "Transaction-level advisory lock `$postgresLockKey->humanReadableValue` cannot be acquired outside of transaction", + "Transaction-level advisory lock `$key->humanReadableValue` cannot be acquired outside of transaction", ); } @@ -223,13 +224,13 @@ private function acquireLock( PostgresLockAccessModeEnum::Share, ] => 'SELECT PG_ADVISORY_LOCK_SHARED(:class_id, :object_id);', }; - $sql .= " -- $postgresLockKey->humanReadableValue"; + $sql .= " -- $key->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockKey->classId, - 'object_id' => $postgresLockKey->objectId, + 'class_id' => $key->classId, + 'object_id' => $key->objectId, ], ); diff --git a/test/Integration/AbstractIntegrationTestCase.php b/test/Integration/AbstractIntegrationTestCase.php index 914b7a2..d948e64 100644 --- a/test/Integration/AbstractIntegrationTestCase.php +++ b/test/Integration/AbstractIntegrationTestCase.php @@ -116,7 +116,11 @@ private function findPostgresAdvisoryLockInConnection( 'lock_object_id' => $postgresLockKey->objectId, 'lock_object_subid' => 2, // Using two keyed locks 'connection_pid' => $dbConnection->pgsqlGetPid(), - 'mode' => $mode->value, + 'mode' => match ($mode) { + PostgresLockAccessModeEnum::Exclusive => 'ExclusiveLock', + PostgresLockAccessModeEnum::Share => 'ShareLock', + default => throw new \LogicException("Unknown mode $mode->name"), + }, ], ); From 3a489f184c7adfa4a7ea2c207231e240ef03fdd7 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Tue, 5 Aug 2025 12:20:55 +0300 Subject: [PATCH 10/15] Leave only *Handler API --- README.md | 6 ++++++ src/Postgres/PostgresAdvisoryLocker.php | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f7431a3..377b448 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ License

+## Things to decide + +- [ ] Do we need handle methods? Or leave simple bool for transaction and callback only for session lock +- [ ] Should wait mode be blocking or non-blocking by default? +- [ ] Should callback for session lock be at the end of the params? + ## Introduction > WARNING! This library is currently under development and may not be stable. Use in your services at your own risk. diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index ca1d753..cdd4986 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -126,7 +126,11 @@ public function withinSessionLevelLock( return $callback($lockHandle); } finally { - $lockHandle->release(); + $this->releaseSessionLevelLock( + $dbConnection, + $key, + $accessMode, + ); } } From a2a211820a45b6990894c0b69245b427c5c51c17 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Tue, 5 Aug 2025 12:40:06 +0300 Subject: [PATCH 11/15] Leave only *Handler API --- README.md | 2 +- src/Postgres/PostgresAdvisoryLocker.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 377b448..61290cd 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [ ] Do we need handle methods? Or leave simple bool for transaction and callback only for session lock - [ ] Should wait mode be blocking or non-blocking by default? -- [ ] Should callback for session lock be at the end of the params? +- [ ] Should callback for session lock be at the end of the params (after optional ones)? ## Introduction diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index cdd4986..91de6dd 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -94,6 +94,7 @@ public function acquireSessionLevelLock( * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, * as they are automatically released at the end of a transaction and are less error-prone. * Use session-level locks only when transactional context is not available. + * @see acquireTransactionLevelLock() for preferred locking strategy. * * @param PDO $dbConnection Active database connection. * @param PostgresLockKey $key Lock key to be acquired. @@ -105,8 +106,6 @@ public function acquireSessionLevelLock( * @template TReturn * * TODO: Cover with tests - *@see acquireTransactionLevelLock() for preferred locking strategy. - * */ public function withinSessionLevelLock( PDO $dbConnection, From 9c08b96379f06f40f1ab2fba10484471238c6dee Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 9 Aug 2025 09:29:55 +0300 Subject: [PATCH 12/15] Leave only *Handler API --- .../LockHandle/SessionLevelLockHandle.php | 9 ------ .../LockHandle/TransactionLevelLockHandle.php | 24 --------------- src/Postgres/PostgresAdvisoryLocker.php | 30 ++++++------------- .../Postgres/PostgresAdvisoryLockerTest.php | 29 +++++++++--------- 4 files changed, 23 insertions(+), 69 deletions(-) delete mode 100644 src/Postgres/LockHandle/TransactionLevelLockHandle.php diff --git a/src/Postgres/LockHandle/SessionLevelLockHandle.php b/src/Postgres/LockHandle/SessionLevelLockHandle.php index 23b0fdc..ff13f8e 100644 --- a/src/Postgres/LockHandle/SessionLevelLockHandle.php +++ b/src/Postgres/LockHandle/SessionLevelLockHandle.php @@ -58,13 +58,4 @@ public function release(): bool return $wasReleased; } - - /** - * Automatically release the lock when the handle is destroyed. - */ - public function __destruct() - { - // TODO: Do we need to - $this->release(); - } } \ No newline at end of file diff --git a/src/Postgres/LockHandle/TransactionLevelLockHandle.php b/src/Postgres/LockHandle/TransactionLevelLockHandle.php deleted file mode 100644 index 999b12f..0000000 --- a/src/Postgres/LockHandle/TransactionLevelLockHandle.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Cog\DbLocker\Postgres\LockHandle; - -/** - * @internal - */ -final class TransactionLevelLockHandle -{ - public function __construct( - public readonly bool $wasAcquired, - ) {} -} \ No newline at end of file diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 91de6dd..0acdde6 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -17,7 +17,6 @@ use Cog\DbLocker\Postgres\Enum\PostgresLockLevelEnum; use Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum; use Cog\DbLocker\Postgres\LockHandle\SessionLevelLockHandle; -use Cog\DbLocker\Postgres\LockHandle\TransactionLevelLockHandle; use LogicException; use PDO; @@ -31,31 +30,19 @@ public function acquireTransactionLevelLock( PostgresLockKey $key, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): TransactionLevelLockHandle { - return new TransactionLevelLockHandle( - wasAcquired: $this->acquireLock( - $dbConnection, - $key, - PostgresLockLevelEnum::Transaction, - $waitMode, - $accessMode, - ), + ): bool { + return $this->acquireLock( + $dbConnection, + $key, + PostgresLockLevelEnum::Transaction, + $waitMode, + $accessMode, ); } /** * Acquire a session-level advisory lock with configurable wait and access modes. * - * ⚠️ You MUST retain the returned handle in a variable. - * If the handle is not stored and is immediately garbage collected, - * the lock will be released in the lock handle __destruct method. - * @see SessionLevelLockHandle::__destruct - * - * @example - * $handle = $locker->acquireSessionLevelLock(...); // ✅ Lock held - * - * $locker->acquireSessionLevelLock(...); // ❌ Lock immediately released - * * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, * as they are automatically released at the end of a transaction and are less error-prone. * Use session-level locks only when transactional context is not available. @@ -94,7 +81,6 @@ public function acquireSessionLevelLock( * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, * as they are automatically released at the end of a transaction and are less error-prone. * Use session-level locks only when transactional context is not available. - * @see acquireTransactionLevelLock() for preferred locking strategy. * * @param PDO $dbConnection Active database connection. * @param PostgresLockKey $key Lock key to be acquired. @@ -106,6 +92,8 @@ public function acquireSessionLevelLock( * @template TReturn * * TODO: Cover with tests + * @see acquireTransactionLevelLock() for preferred locking strategy. + * */ public function withinSessionLevelLock( PDO $dbConnection, diff --git a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php index c67dc31..659def6 100644 --- a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php @@ -71,7 +71,7 @@ public function testItCanTryAcquireLockWithinTransaction( accessMode: $accessMode, ); - $this->assertTrue($isLockAcquired->wasAcquired); + $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey, $accessMode); } @@ -177,8 +177,7 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - // TODO: Fix corner-case: without variable its self-destructing instantly - $connection1Lock = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey, ); @@ -200,7 +199,7 @@ public function testItCanReleaseLock( $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - $connectionLock = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection, $lockKey, accessMode: $accessMode, @@ -236,7 +235,7 @@ public function testItCanNotReleaseLockOfDifferentModes( $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - $connectionLock = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection, $lockKey, accessMode: $acquireMode, @@ -272,11 +271,11 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - $connectionLock1 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection, $lockKey, ); - $connectionLock2 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection, $lockKey, ); @@ -320,11 +319,11 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - $connection1Lock1 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey, ); - $connection1Lock2 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey, ); @@ -360,7 +359,7 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); - $connection1Lock = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey, ); @@ -413,19 +412,19 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherConnectionLocks $lockKey2 = PostgresLockKey::create('test2'); $lockKey3 = PostgresLockKey::create('test3'); $lockKey4 = PostgresLockKey::create('test4'); - $connect1Lock1 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey1, ); - $connect1Lock2 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey2, ); - $connect2Lock3 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection2, $lockKey3, ); - $connect2Lock4 = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection2, $lockKey4, ); @@ -461,7 +460,7 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $dbConnection2 = $this->initPostgresPdoConnection(); $lockKey = PostgresLockKey::create('test'); $dbConnection1->beginTransaction(); - $connection1Lock = $locker->acquireSessionLevelLock( + $locker->acquireSessionLevelLock( $dbConnection1, $lockKey, ); From b7fe0a0113419b02f6a3a37b8ca0b1eaa3ead246 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 9 Aug 2025 09:52:21 +0300 Subject: [PATCH 13/15] Leave only *Handler API --- src/Postgres/PostgresAdvisoryLocker.php | 9 +++---- .../Postgres/PostgresAdvisoryLockerTest.php | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 0acdde6..484e8ca 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -81,6 +81,9 @@ public function acquireSessionLevelLock( * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, * as they are automatically released at the end of a transaction and are less error-prone. * Use session-level locks only when transactional context is not available. + * @see acquireTransactionLevelLock() for preferred locking strategy. + * + * @template TReturn * * @param PDO $dbConnection Active database connection. * @param PostgresLockKey $key Lock key to be acquired. @@ -88,12 +91,6 @@ public function acquireSessionLevelLock( * @param PostgresLockWaitModeEnum $waitMode Whether to wait for the lock or fail immediately. Default is non-blocking. * @param PostgresLockAccessModeEnum $accessMode Whether to acquire a shared or exclusive lock. Default is exclusive. * @return TReturn The return value of the callback. - * - * @template TReturn - * - * TODO: Cover with tests - * @see acquireTransactionLevelLock() for preferred locking strategy. - * */ public function withinSessionLevelLock( PDO $dbConnection, diff --git a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php index 659def6..e355fa1 100644 --- a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php @@ -566,6 +566,30 @@ public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey2); } + public function testItCanExecuteCodeWithinSessionLock(): void + { + $locker = $this->initLocker(); + $dbConnection = $this->initPostgresPdoConnection(); + $lockKey = PostgresLockKey::create('test'); + $x = 2; + $y = 3; + + $result = $locker->withinSessionLevelLock( + $dbConnection, + $lockKey, + function () use ($dbConnection, $lockKey, $x, $y): int { + $this->assertPgAdvisoryLocksCount(1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $lockKey); + + return $x + $y; + }, + ); + + $this->assertSame(5, $result); + $this->assertPgAdvisoryLocksCount(0); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $lockKey); + } + private function initLocker(): PostgresAdvisoryLocker { return new PostgresAdvisoryLocker(); From fe66f4fea58a5694af9382888b0a34198f449b9b Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 9 Aug 2025 10:05:13 +0300 Subject: [PATCH 14/15] Leave only *Handler API --- src/Postgres/PostgresAdvisoryLocker.php | 58 ++++++++++++------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Postgres/PostgresAdvisoryLocker.php b/src/Postgres/PostgresAdvisoryLocker.php index 484e8ca..90d7633 100644 --- a/src/Postgres/PostgresAdvisoryLocker.php +++ b/src/Postgres/PostgresAdvisoryLocker.php @@ -40,35 +40,6 @@ public function acquireTransactionLevelLock( ); } - /** - * Acquire a session-level advisory lock with configurable wait and access modes. - * - * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, - * as they are automatically released at the end of a transaction and are less error-prone. - * Use session-level locks only when transactional context is not available. - * @see acquireTransactionLevelLock() for preferred locking strategy. - */ - public function acquireSessionLevelLock( - PDO $dbConnection, - PostgresLockKey $key, - PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, - PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): SessionLevelLockHandle { - return new SessionLevelLockHandle( - $dbConnection, - $this, - $key, - $accessMode, - wasAcquired: $this->acquireLock( - $dbConnection, - $key, - PostgresLockLevelEnum::Session, - $waitMode, - $accessMode, - ), - ); - } - /** * Acquires a session-level advisory lock and ensures its release after executing the callback. * @@ -118,6 +89,35 @@ public function withinSessionLevelLock( } } + /** + * Acquire a session-level advisory lock with configurable wait and access modes. + * + * ⚠️ Transaction-level advisory locks are strongly preferred whenever possible, + * as they are automatically released at the end of a transaction and are less error-prone. + * Use session-level locks only when transactional context is not available. + * @see acquireTransactionLevelLock() for preferred locking strategy. + */ + public function acquireSessionLevelLock( + PDO $dbConnection, + PostgresLockKey $key, + PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, + PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, + ): SessionLevelLockHandle { + return new SessionLevelLockHandle( + $dbConnection, + $this, + $key, + $accessMode, + wasAcquired: $this->acquireLock( + $dbConnection, + $key, + PostgresLockLevelEnum::Session, + $waitMode, + $accessMode, + ), + ); + } + /** * Release session level advisory lock. */ From 9990b1b6592810a0f2f349179221c38873f0ce06 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 9 Aug 2025 10:32:12 +0300 Subject: [PATCH 15/15] Leave only *Handler API --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 61290cd..cb3cf07 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ ## Things to decide -- [ ] Do we need handle methods? Or leave simple bool for transaction and callback only for session lock - [ ] Should wait mode be blocking or non-blocking by default? - [ ] Should callback for session lock be at the end of the params (after optional ones)?