diff --git a/README.md b/README.md index 4ef4186..cb3cf07 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,11 @@ License

+## Things to decide + +- [ ] 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)? + ## Introduction > WARNING! This library is currently under development and may not be stable. Use in your services at your own risk. @@ -38,7 +43,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, @@ -54,18 +59,45 @@ $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'); +$lockKey = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); + +$payment = $locker->withinSessionLevelLock( + 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) { + // Execute logic if lock was successful + } else { + // Execute logic if lock acquisition has been failed + } + }, + 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(); +$lockKey = \Cog\DbLocker\Postgres\PostgresLockKey::create('user', '4'); try { - $lock = $locker->acquireSessionLevelLockHandler( - $dbConnection, - $lockId, - \Cog\DbLocker\Postgres\Enum\PostgresLockWaitModeEnum::NonBlocking, - \Cog\DbLocker\Postgres\Enum\PostgresLockAccessModeEnum::Exclusive, + $lock = $locker->acquireSessionLevelLock( + 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/LockHandle/SessionLevelLockHandle.php b/src/Postgres/LockHandle/SessionLevelLockHandle.php index e3298c3..ff13f8e 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) { @@ -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 6ab3dc0..90d7633 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; @@ -25,66 +24,93 @@ final class PostgresAdvisoryLocker { /** * Acquire a transaction-level advisory lock with configurable wait and access modes. - * - * TODO: Cover with tests */ - public function acquireTransactionLevelLockHandler( + public function acquireTransactionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $key, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): TransactionLevelLockHandle { - return new TransactionLevelLockHandle( - wasAcquired: $this->acquireLock( - $dbConnection, - $postgresLockId, - PostgresLockLevelEnum::Transaction, - $waitMode, - $accessMode, - ), + ): bool { + return $this->acquireLock( + $dbConnection, + $key, + PostgresLockLevelEnum::Transaction, + $waitMode, + $accessMode, ); } /** - * Acquire a transaction-level advisory lock with configurable wait and access modes. + * 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. * - * TODO: Do we need low-level API? + * @template TReturn + * + * @param PDO $dbConnection Active database connection. + * @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. + * @return TReturn The return value of the callback. */ - public function acquireTransactionLevelLock( + public function withinSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $key, + callable $callback, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, - ): bool { - return $this->acquireLock( + ): mixed { + $lockHandle = $this->acquireSessionLevelLock( $dbConnection, - $postgresLockId, - PostgresLockLevelEnum::Transaction, + $key, $waitMode, $accessMode, ); + + try { + return $callback($lockHandle); + } + finally { + $this->releaseSessionLevelLock( + $dbConnection, + $key, + $accessMode, + ); + } } /** * Acquire a session-level advisory lock with configurable wait and access modes. * - * TODO: Write that transaction-level is recommended. - * TODO: Cover with tests + * ⚠️ 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 acquireSessionLevelLockHandler( + public function acquireSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + PostgresLockKey $key, PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking, PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive, ): SessionLevelLockHandle { return new SessionLevelLockHandle( $dbConnection, $this, - $postgresLockId, + $key, $accessMode, wasAcquired: $this->acquireLock( $dbConnection, - $postgresLockId, + $key, PostgresLockLevelEnum::Session, $waitMode, $accessMode, @@ -92,46 +118,25 @@ 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. */ public function releaseSessionLevelLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + 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 .= " -- $postgresLockId->humanReadableValue"; + $sql .= " -- $key->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockId->classId, - 'object_id' => $postgresLockId->objectId, + 'class_id' => $key->classId, + 'object_id' => $key->objectId, ], ); @@ -154,14 +159,14 @@ public function releaseAllSessionLevelLocks( private function acquireLock( PDO $dbConnection, - PostgresLockKey $postgresLockId, + 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 `$postgresLockId->humanReadableValue` cannot be acquired outside of transaction", + "Transaction-level advisory lock `$key->humanReadableValue` cannot be acquired outside of transaction", ); } @@ -207,13 +212,13 @@ private function acquireLock( PostgresLockAccessModeEnum::Share, ] => 'SELECT PG_ADVISORY_LOCK_SHARED(:class_id, :object_id);', }; - $sql .= " -- $postgresLockId->humanReadableValue"; + $sql .= " -- $key->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( [ - 'class_id' => $postgresLockId->classId, - 'object_id' => $postgresLockId->objectId, + 'class_id' => $key->classId, + 'object_id' => $key->objectId, ], ); diff --git a/test/Integration/AbstractIntegrationTestCase.php b/test/Integration/AbstractIntegrationTestCase.php index cbc1e73..d948e64 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,11 +112,15 @@ 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, + 'mode' => match ($mode) { + PostgresLockAccessModeEnum::Exclusive => 'ExclusiveLock', + PostgresLockAccessModeEnum::Share => 'ShareLock', + default => throw new \LogicException("Unknown mode $mode->name"), + }, ], ); diff --git a/test/Integration/Postgres/PostgresAdvisoryLockerTest.php b/test/Integration/Postgres/PostgresAdvisoryLockerTest.php index c7ec210..e355fa1 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); + $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); $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); + $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'); - $isLockAcquired1 = $locker->acquireSessionLevelLock( + $isLock1Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); - $isLockAcquired2 = $locker->acquireSessionLevelLock( + $isLock2Acquired = $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); - $this->assertTrue($isLockAcquired1); - $this->assertTrue($isLockAcquired2); + $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); - $this->assertTrue($isLock2Acquired); + $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,20 +176,20 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); - $isLockAcquired = $locker->acquireSessionLevelLock( + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); - $this->assertFalse($isLockAcquired); + $this->assertFalse($connection2Lock->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); + $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $lockKey); } #[DataProvider('provideItCanReleaseLockData')] @@ -198,16 +198,16 @@ public function testItCanReleaseLock( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); $isLockReleased = $locker->releaseSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, accessMode: $accessMode, ); @@ -234,22 +234,22 @@ public function testItCanNotReleaseLockOfDifferentModes( ): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $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 @@ -270,18 +270,18 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireSessionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); $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); @@ -293,24 +293,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); + $this->assertTrue($isLockAcquired->wasAcquired); $this->assertPgAdvisoryLocksCount(1); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $lockKey); } public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLocked(): void @@ -318,36 +318,36 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); - $isLockReleased = $locker->releaseSessionLevelLock($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->acquireSessionLevelLock( + $isLockReleased = $locker->releaseSessionLevelLock($dbConnection1, $lockKey); + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); $this->assertTrue($isLockReleased); - $this->assertFalse($isLockAcquired); + $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); @@ -358,17 +358,17 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $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 @@ -408,32 +408,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'); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId1, + $lockKey1, ); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId2, + $lockKey2, ); $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId3, + $lockKey3, ); $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 @@ -445,11 +445,11 @@ public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $locker->acquireTransactionLevelLock( $dbConnection, - $postgresLockId, + $lockKey, ); } @@ -458,66 +458,66 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockKey::create('test'); + $lockKey = PostgresLockKey::create('test'); $dbConnection1->beginTransaction(); $locker->acquireSessionLevelLock( $dbConnection1, - $postgresLockId, + $lockKey, ); - $isLockAcquired = $locker->acquireSessionLevelLock( + $connection2Lock = $locker->acquireSessionLevelLock( $dbConnection2, - $postgresLockId, + $lockKey, ); - $this->assertFalse($isLockAcquired); + $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; @@ -529,41 +529,65 @@ 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); + } + + 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 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' => [