From 8798e5e917a70e1a68d43932fd2ff44d2475cceb Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 26 Jul 2025 17:27:13 +0300 Subject: [PATCH 01/13] Rewrite logic to two keyed lock --- src/LockId/PostgresLockId.php | 7 +++---- .../Locker/PostgresAdvisoryLockerTest.php | 8 ++++---- test/Unit/LockId/PostgresLockIdTest.php | 12 ++++++------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/LockId/PostgresLockId.php b/src/LockId/PostgresLockId.php index 650640c..2e0e8c8 100644 --- a/src/LockId/PostgresLockId.php +++ b/src/LockId/PostgresLockId.php @@ -17,18 +17,17 @@ final class PostgresLockId { - private const DB_INT64_VALUE_MIN = -9_223_372_036_854_775_808; - private const DB_INT64_VALUE_MAX = 9_223_372_036_854_775_807; + private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; public function __construct( public readonly int $id, public readonly string $humanReadableValue = '', ) { - if ($id < self::DB_INT64_VALUE_MIN) { + if ($id < self::DB_INT32_VALUE_MIN) { throw new InvalidArgumentException('Out of bound exception (id is too small)'); } - if ($id > self::DB_INT64_VALUE_MAX) { + if ($id > self::DB_INT32_VALUE_MAX) { throw new InvalidArgumentException('Out of bound exception (id is too big)'); } } diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index f7628c1..31bc4f1 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -21,8 +21,8 @@ final class PostgresAdvisoryLockerTest extends AbstractIntegrationTestCase { - private const DB_INT64_VALUE_MIN = 0; - private const DB_INT64_VALUE_MAX = 9223372036854775807; + private const DB_INT32_VALUE_MIN = -2_147_483_648; + private const DB_INT32_VALUE_MAX = 2_147_483_647; public function test_it_can_acquire_lock(): void { @@ -41,7 +41,7 @@ public function test_it_can_acquire_lock_with_smallest_lock_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT64_VALUE_MIN); + $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MIN); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -54,7 +54,7 @@ public function test_it_can_acquire_lock_with_biggest_lock_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT64_VALUE_MAX); + $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MAX); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); diff --git a/test/Unit/LockId/PostgresLockIdTest.php b/test/Unit/LockId/PostgresLockIdTest.php index 5a73607..8def7f0 100644 --- a/test/Unit/LockId/PostgresLockIdTest.php +++ b/test/Unit/LockId/PostgresLockIdTest.php @@ -19,21 +19,21 @@ final class PostgresLockIdTest extends AbstractUnitTestCase { - private const DB_INT64_VALUE_MIN = 0; - private const DB_INT64_VALUE_MAX = 9223372036854775807; + private const DB_INT32_VALUE_MIN = -2_147_483_648; + private const DB_INT32_VALUE_MAX = 2_147_483_647; public function test_it_can_create_postgres_lock_id_with_min_id(): void { - $lockId = new PostgresLockId(self::DB_INT64_VALUE_MIN); + $lockId = new PostgresLockId(self::DB_INT32_VALUE_MIN); - $this->assertSame(self::DB_INT64_VALUE_MIN, $lockId->id); + $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->id); } public function test_it_can_create_postgres_lock_id_with_max_id(): void { - $lockId = new PostgresLockId(self::DB_INT64_VALUE_MAX); + $lockId = new PostgresLockId(self::DB_INT32_VALUE_MAX); - $this->assertSame(self::DB_INT64_VALUE_MAX, $lockId->id); + $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->id); } public function test_it_can_create_postgres_lock_id_from_lock_id(): void From 788962f246da58afd47f9954e420efe59c040f68 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sat, 26 Jul 2025 17:43:22 +0300 Subject: [PATCH 02/13] Rewrite logic to two keyed lock --- src/LockId/PostgresLockId.php | 24 ++++++++----- src/Locker/PostgresAdvisoryLocker.php | 15 ++++---- .../AbstractIntegrationTestCase.php | 16 ++------- .../Locker/PostgresAdvisoryLockerTest.php | 34 ++++++++++++++++--- test/Unit/LockId/PostgresLockIdTest.php | 30 +++++++++++----- 5 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/LockId/PostgresLockId.php b/src/LockId/PostgresLockId.php index 2e0e8c8..c29c775 100644 --- a/src/LockId/PostgresLockId.php +++ b/src/LockId/PostgresLockId.php @@ -21,25 +21,31 @@ final class PostgresLockId private const DB_INT32_VALUE_MAX = 2_147_483_647; public function __construct( - public readonly int $id, + public readonly int $classId, + public readonly int $objectId, public readonly string $humanReadableValue = '', ) { - if ($id < self::DB_INT32_VALUE_MIN) { - throw new InvalidArgumentException('Out of bound exception (id is too small)'); + if ($classId < self::DB_INT32_VALUE_MIN) { + throw new InvalidArgumentException('Out of bound exception (classId is too small)'); } - if ($id > self::DB_INT32_VALUE_MAX) { - throw new InvalidArgumentException('Out of bound exception (id is too big)'); + if ($classId > self::DB_INT32_VALUE_MAX) { + throw new InvalidArgumentException('Out of bound exception (classId is too big)'); + } + if ($objectId < self::DB_INT32_VALUE_MIN) { + throw new InvalidArgumentException('Out of bound exception (objectId is too small)'); + } + if ($objectId > self::DB_INT32_VALUE_MAX) { + throw new InvalidArgumentException('Out of bound exception (objectId is too big)'); } } public static function fromLockId( LockId $lockId, ): self { - $lockStringId = (string)$lockId; - return new self( - id: self::convertStringToSignedInt32($lockStringId), - humanReadableValue: $lockStringId, + classId: self::convertStringToSignedInt32($lockId->key), + objectId: self::convertStringToSignedInt32($lockId->value), + humanReadableValue: (string)$lockId, ); } diff --git a/src/Locker/PostgresAdvisoryLocker.php b/src/Locker/PostgresAdvisoryLocker.php index 1062109..d1fa828 100644 --- a/src/Locker/PostgresAdvisoryLocker.php +++ b/src/Locker/PostgresAdvisoryLocker.php @@ -26,12 +26,13 @@ public function tryAcquireLock( // TODO: Need to sanitize humanReadableValue? $statement = $dbConnection->prepare( <<humanReadableValue + SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- $postgresLockId->humanReadableValue SQL, ); $statement->execute( [ - 'lock_id' => $postgresLockId->id, + 'class_id' => $postgresLockId->classId, + 'object_id' => $postgresLockId->objectId, ], ); @@ -53,12 +54,13 @@ public function tryAcquireLockWithinTransaction( // TODO: Need to sanitize humanReadableValue? $statement = $dbConnection->prepare( <<humanReadableValue + SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- $postgresLockId->humanReadableValue SQL, ); $statement->execute( [ - 'lock_id' => $postgresLockId->id, + 'class_id' => $postgresLockId->classId, + 'object_id' => $postgresLockId->objectId, ], ); @@ -71,12 +73,13 @@ public function releaseLock( ): bool { $statement = $dbConnection->prepare( <<humanReadableValue + SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id); -- $postgresLockId->humanReadableValue SQL, ); $statement->execute( [ - 'lock_id' => $postgresLockId->id, + 'class_id' => $postgresLockId->classId, + 'object_id' => $postgresLockId->objectId, ], ); diff --git a/test/Integration/AbstractIntegrationTestCase.php b/test/Integration/AbstractIntegrationTestCase.php index 33c0ba0..c45d47e 100644 --- a/test/Integration/AbstractIntegrationTestCase.php +++ b/test/Integration/AbstractIntegrationTestCase.php @@ -91,16 +91,6 @@ private function findPostgresAdvisoryLockInConnection( ): object | null { // For one-argument advisory locks, Postgres stores the signed 64-bit key as two 32-bit integers: // classid = high 32 bits, objid = low 32 bits. - $lockClassId = ($postgresLockId->id >> 32) & 0xFFFFFFFF; - $lockObjectId = $postgresLockId->id & 0xFFFFFFFF; - - // Convert to signed 32-bit if necessary (Postgres stores as signed) - if ($lockClassId > 0x7FFFFFFF) { - $lockClassId -= 0x100000000; - } - if ($lockObjectId > 0x7FFFFFFF) { - $lockObjectId -= 0x100000000; - } $statement = $dbConnection->prepare( <<<'SQL' @@ -116,9 +106,9 @@ private function findPostgresAdvisoryLockInConnection( ); $statement->execute( [ - 'lock_class_id' => $lockClassId, - 'lock_object_id' => $lockObjectId, - 'lock_object_subid' => 1, // For one keyed value + 'lock_class_id' => $postgresLockId->classId, + 'lock_object_id' => $postgresLockId->objectId, + 'lock_object_subid' => 2, // Using two keyed locks 'connection_pid' => $dbConnection->pgsqlGetPid(), 'mode' => self::MODE_EXCLUSIVE, ], diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index 31bc4f1..336d9bf 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -37,11 +37,11 @@ public function test_it_can_acquire_lock(): void $this->assertPgAdvisoryLocksCount(1); } - public function test_it_can_acquire_lock_with_smallest_lock_id(): void + public function test_it_can_acquire_lock_with_min_class_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MIN); + $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MIN, 0); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -50,11 +50,37 @@ public function test_it_can_acquire_lock_with_smallest_lock_id(): void $this->assertPgAdvisoryLocksCount(1); } - public function test_it_can_acquire_lock_with_biggest_lock_id(): void + public function test_it_can_acquire_lock_with_max_class_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MAX); + $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MAX, 0); + + $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); + + $this->assertTrue($isLockAcquired); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLocksCount(1); + } + + public function test_it_can_acquire_lock_with_min_object_id(): void + { + $locker = $this->initLocker(); + $dbConnection = $this->initPostgresPdoConnection(); + $postgresLockId = new PostgresLockId(0, self::DB_INT32_VALUE_MIN); + + $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); + + $this->assertTrue($isLockAcquired); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); + $this->assertPgAdvisoryLocksCount(1); + } + + public function test_it_can_acquire_lock_with_max_object_id(): void + { + $locker = $this->initLocker(); + $dbConnection = $this->initPostgresPdoConnection(); + $postgresLockId = new PostgresLockId(0, self::DB_INT32_VALUE_MAX); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); diff --git a/test/Unit/LockId/PostgresLockIdTest.php b/test/Unit/LockId/PostgresLockIdTest.php index 8def7f0..d1614c4 100644 --- a/test/Unit/LockId/PostgresLockIdTest.php +++ b/test/Unit/LockId/PostgresLockIdTest.php @@ -22,18 +22,32 @@ final class PostgresLockIdTest extends AbstractUnitTestCase private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - public function test_it_can_create_postgres_lock_id_with_min_id(): void + public function test_it_can_create_postgres_lock_id_with_min_class_id(): void { - $lockId = new PostgresLockId(self::DB_INT32_VALUE_MIN); + $lockId = new PostgresLockId(self::DB_INT32_VALUE_MIN, 0); - $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->id); + $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->classId); } - public function test_it_can_create_postgres_lock_id_with_max_id(): void + public function test_it_can_create_postgres_lock_id_with_max_class_id(): void { - $lockId = new PostgresLockId(self::DB_INT32_VALUE_MAX); + $lockId = new PostgresLockId(self::DB_INT32_VALUE_MAX, 0); - $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->id); + $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->classId); + } + + public function test_it_can_create_postgres_lock_id_with_min_object_id(): void + { + $lockId = new PostgresLockId(0, self::DB_INT32_VALUE_MIN); + + $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->objectId); + } + + public function test_it_can_create_postgres_lock_id_with_max_object_id(): void + { + $lockId = new PostgresLockId(0, self::DB_INT32_VALUE_MAX); + + $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->objectId); } public function test_it_can_create_postgres_lock_id_from_lock_id(): void @@ -42,7 +56,7 @@ public function test_it_can_create_postgres_lock_id_from_lock_id(): void $postgresLockId = PostgresLockId::fromLockId($lockId); - $this->assertSame(-662733300, $postgresLockId->id); + $this->assertSame(-662733300, $postgresLockId->classId); } public function test_it_can_create_postgres_lock_id_from_lock_id_with_value(): void @@ -51,6 +65,6 @@ public function test_it_can_create_postgres_lock_id_from_lock_id_with_value(): v $postgresLockId = PostgresLockId::fromLockId($lockId); - $this->assertSame(782632948, $postgresLockId->id); + $this->assertSame(-662733300, $postgresLockId->classId); } } From 77e15157267378b2e82e700394a8e48a43bf4756 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 00:29:54 +0300 Subject: [PATCH 03/13] Rewrite logic to two keyed lock --- README.md | 20 +++-- src/LockId/PostgresLockId.php | 12 +++ test/Unit/LockId/PostgresLockIdTest.php | 105 +++++++++++++++++------- 3 files changed, 99 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index f1fbf6d..7e92d98 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ PHP application-level database locking mechanisms to implement concurrency contr Supported drivers: -- Postgres +- Postgres — [PostgreSQL Advisory Locks Documentation](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS) ## Installation @@ -33,12 +33,13 @@ composer require cybercog/php-db-locker $dbConnection = new PDO($dsn, $username, $password); $postgresLocker = new \Cog\DbLocker\Locker\PostgresAdvisoryLocker(); -$postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromLockId( - new LockId('user', '4'), -); +$postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromKeyValue('user', '4'); $dbConnection->beginTransaction(); -$isLockAcquired = $postgresLocker->acquireLockWithinTransaction($dbConnection, $postgresLockId); +$isLockAcquired = $postgresLocker->acquireLockWithinTransaction( + $dbConnection, + $postgresLockId, +); if ($isLockAcquired) { // Execute logic if lock was successful } else { @@ -53,11 +54,12 @@ $dbConnection->commit(); $dbConnection = new PDO($dsn, $username, $password); $postgresLocker = new \Cog\DbLocker\Locker\PostgresAdvisoryLocker(); -$postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromLockId( - new LockId('user', '4'), -); +$postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromKeyValue('user', '4'); -$isLockAcquired = $postgresLocker->acquireLock($dbConnection, $postgresLockId); +$isLockAcquired = $postgresLocker->acquireLock( + $dbConnection, + $postgresLockId, +); if ($isLockAcquired) { // Execute logic if lock was successful } else { diff --git a/src/LockId/PostgresLockId.php b/src/LockId/PostgresLockId.php index c29c775..a6cbbfd 100644 --- a/src/LockId/PostgresLockId.php +++ b/src/LockId/PostgresLockId.php @@ -49,6 +49,18 @@ classId: self::convertStringToSignedInt32($lockId->key), ); } + public static function fromKeyValue( + string $key, + string $value = '', + ): self { + return self::fromLockId( + new LockId( + $key, + $value, + ), + ); + } + /** * Generates a deterministic signed 32-bit integer ID * from a string for use as a Postgres advisory lock key. diff --git a/test/Unit/LockId/PostgresLockIdTest.php b/test/Unit/LockId/PostgresLockIdTest.php index d1614c4..8db752b 100644 --- a/test/Unit/LockId/PostgresLockIdTest.php +++ b/test/Unit/LockId/PostgresLockIdTest.php @@ -16,55 +16,102 @@ use Cog\DbLocker\LockId\LockId; use Cog\DbLocker\LockId\PostgresLockId; use Cog\Test\DbLocker\Unit\AbstractUnitTestCase; +use PHPUnit\Framework\Attributes\DataProvider; final class PostgresLockIdTest extends AbstractUnitTestCase { private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - public function test_it_can_create_postgres_lock_id_with_min_class_id(): void - { - $lockId = new PostgresLockId(self::DB_INT32_VALUE_MIN, 0); + #[DataProvider('provideItCanCreatePostgresLockIdData')] + public function testItCanCreatePostgresLockId( + int $classId, + int $objectId, + ): void { + $lockId = new PostgresLockId($classId, $objectId); - $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->classId); + $this->assertSame($classId, $lockId->classId); + $this->assertSame($objectId, $lockId->objectId); } - public function test_it_can_create_postgres_lock_id_with_max_class_id(): void + public static function provideItCanCreatePostgresLockIdData(): array { - $lockId = new PostgresLockId(self::DB_INT32_VALUE_MAX, 0); - - $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->classId); + return [ + 'min class_id' => [ + self::DB_INT32_VALUE_MIN, + 0, + ], + 'max class_id' => [ + self::DB_INT32_VALUE_MAX, + 0, + ], + 'min object_id' => [ + 0, + self::DB_INT32_VALUE_MIN, + ], + 'max object_id' => [ + 0, + self::DB_INT32_VALUE_MAX, + ], + ]; } - public function test_it_can_create_postgres_lock_id_with_min_object_id(): void - { - $lockId = new PostgresLockId(0, self::DB_INT32_VALUE_MIN); + #[DataProvider('provideItCanCreatePostgresLockIdFromLockIdData')] + public function testItCanCreatePostgresLockIdFromLockId( + LockId $lockId, + int $expectedClassId, + int $expectedObjectId, + ): void { + $postgresLockId = PostgresLockId::fromLockId($lockId); - $this->assertSame(self::DB_INT32_VALUE_MIN, $lockId->objectId); + $this->assertSame($expectedClassId, $postgresLockId->classId); + $this->assertSame($expectedObjectId, $postgresLockId->objectId); } - public function test_it_can_create_postgres_lock_id_with_max_object_id(): void + public static function provideItCanCreatePostgresLockIdFromLockIdData(): array { - $lockId = new PostgresLockId(0, self::DB_INT32_VALUE_MAX); - - $this->assertSame(self::DB_INT32_VALUE_MAX, $lockId->objectId); + return [ + 'key only' => [ + new LockId('test'), + -662733300, + 0, + ], + 'key + value' => [ + new LockId('test', '1'), + -662733300, + -2082672713, + ], + ]; } - public function test_it_can_create_postgres_lock_id_from_lock_id(): void - { - $lockId = new LockId('test'); - - $postgresLockId = PostgresLockId::fromLockId($lockId); - - $this->assertSame(-662733300, $postgresLockId->classId); + #[DataProvider('provideItCanCreatePostgresLockIdFromKeyValueData')] + public function testItCanCreatePostgresLockIdFromKeyValue( + string $key, + string $value, + int $expectedClassId, + int $expectedObjectId, + ): void { + $postgresLockId = PostgresLockId::fromKeyValue($key, $value); + + $this->assertSame($expectedClassId, $postgresLockId->classId); + $this->assertSame($expectedObjectId, $postgresLockId->objectId); } - public function test_it_can_create_postgres_lock_id_from_lock_id_with_value(): void + public static function provideItCanCreatePostgresLockIdFromKeyValueData(): array { - $lockId = new LockId('test', '1'); - - $postgresLockId = PostgresLockId::fromLockId($lockId); - - $this->assertSame(-662733300, $postgresLockId->classId); + return [ + 'key + empty value' => [ + 'test', + '', + -662733300, + 0, + ], + 'key + value' => [ + 'test', + '1', + -662733300, + -2082672713, + ], + ]; } } From e4148458ed18d8650bc6b88b02bdd6b2b2fe60c4 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 00:44:24 +0300 Subject: [PATCH 04/13] Rewrite logic to two keyed lock --- src/LockId/PostgresLockId.php | 36 ++++-- .../Locker/PostgresAdvisoryLockerTest.php | 8 +- test/Unit/LockId/PostgresLockIdTest.php | 121 ++++++++++++------ 3 files changed, 108 insertions(+), 57 deletions(-) diff --git a/src/LockId/PostgresLockId.php b/src/LockId/PostgresLockId.php index a6cbbfd..b418746 100644 --- a/src/LockId/PostgresLockId.php +++ b/src/LockId/PostgresLockId.php @@ -20,25 +20,37 @@ final class PostgresLockId private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - public function __construct( + private function __construct( public readonly int $classId, public readonly int $objectId, public readonly string $humanReadableValue = '', ) { if ($classId < self::DB_INT32_VALUE_MIN) { - throw new InvalidArgumentException('Out of bound exception (classId is too small)'); + throw new InvalidArgumentException("Out of bound exception (classId=$classId is too small)"); } if ($classId > self::DB_INT32_VALUE_MAX) { - throw new InvalidArgumentException('Out of bound exception (classId is too big)'); + throw new InvalidArgumentException("Out of bound exception (classId=$classId is too big)"); } if ($objectId < self::DB_INT32_VALUE_MIN) { - throw new InvalidArgumentException('Out of bound exception (objectId is too small)'); + throw new InvalidArgumentException("Out of bound exception (objectId=$objectId is too small)"); } if ($objectId > self::DB_INT32_VALUE_MAX) { - throw new InvalidArgumentException('Out of bound exception (objectId is too big)'); + throw new InvalidArgumentException("Out of bound exception (objectId=$objectId is too big)"); } } + public static function fromKeyValue( + string $key, + string $value = '', + ): self { + return self::fromLockId( + new LockId( + $key, + $value, + ), + ); + } + public static function fromLockId( LockId $lockId, ): self { @@ -49,15 +61,13 @@ classId: self::convertStringToSignedInt32($lockId->key), ); } - public static function fromKeyValue( - string $key, - string $value = '', + public static function fromIntKeys( + int $classId, + int $objectId, ): self { - return self::fromLockId( - new LockId( - $key, - $value, - ), + return new self( + $classId, + $objectId, ); } diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index 336d9bf..1b1db0f 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -41,7 +41,7 @@ public function test_it_can_acquire_lock_with_min_class_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MIN, 0); + $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -54,7 +54,7 @@ public function test_it_can_acquire_lock_with_max_class_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(self::DB_INT32_VALUE_MAX, 0); + $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MAX, 0); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -67,7 +67,7 @@ public function test_it_can_acquire_lock_with_min_object_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(0, self::DB_INT32_VALUE_MIN); + $postgresLockId = PostgresLockId::fromIntKeys(0, self::DB_INT32_VALUE_MIN); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -80,7 +80,7 @@ public function test_it_can_acquire_lock_with_max_object_id(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = new PostgresLockId(0, self::DB_INT32_VALUE_MAX); + $postgresLockId = PostgresLockId::fromIntKeys(0, self::DB_INT32_VALUE_MAX); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); diff --git a/test/Unit/LockId/PostgresLockIdTest.php b/test/Unit/LockId/PostgresLockIdTest.php index 8db752b..b1911ae 100644 --- a/test/Unit/LockId/PostgresLockIdTest.php +++ b/test/Unit/LockId/PostgresLockIdTest.php @@ -23,35 +23,33 @@ final class PostgresLockIdTest extends AbstractUnitTestCase private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - #[DataProvider('provideItCanCreatePostgresLockIdData')] - public function testItCanCreatePostgresLockId( - int $classId, - int $objectId, + #[DataProvider('provideItCanCreatePostgresLockIdFromKeyValueData')] + public function testItCanCreatePostgresLockIdFromKeyValue( + string $key, + string $value, + int $expectedClassId, + int $expectedObjectId, ): void { - $lockId = new PostgresLockId($classId, $objectId); + $postgresLockId = PostgresLockId::fromKeyValue($key, $value); - $this->assertSame($classId, $lockId->classId); - $this->assertSame($objectId, $lockId->objectId); + $this->assertSame($expectedClassId, $postgresLockId->classId); + $this->assertSame($expectedObjectId, $postgresLockId->objectId); } - public static function provideItCanCreatePostgresLockIdData(): array + public static function provideItCanCreatePostgresLockIdFromKeyValueData(): array { return [ - 'min class_id' => [ - self::DB_INT32_VALUE_MIN, - 0, - ], - 'max class_id' => [ - self::DB_INT32_VALUE_MAX, - 0, - ], - 'min object_id' => [ + 'key + empty value' => [ + 'test', + '', + -662733300, 0, - self::DB_INT32_VALUE_MIN, ], - 'max object_id' => [ - 0, - self::DB_INT32_VALUE_MAX, + 'key + value' => [ + 'test', + '1', + -662733300, + -2082672713, ], ]; } @@ -84,33 +82,76 @@ public static function provideItCanCreatePostgresLockIdFromLockIdData(): array ]; } - #[DataProvider('provideItCanCreatePostgresLockIdFromKeyValueData')] - public function testItCanCreatePostgresLockIdFromKeyValue( - string $key, - string $value, - int $expectedClassId, - int $expectedObjectId, + #[DataProvider('provideItCanCreatePostgresLockIdFromIntKeysData')] + public function testItCanCreatePostgresLockIdFromIntKeys( + int $classId, + int $objectId, ): void { - $postgresLockId = PostgresLockId::fromKeyValue($key, $value); + $lockId = PostgresLockId::fromIntKeys($classId, $objectId); - $this->assertSame($expectedClassId, $postgresLockId->classId); - $this->assertSame($expectedObjectId, $postgresLockId->objectId); + $this->assertSame($classId, $lockId->classId); + $this->assertSame($objectId, $lockId->objectId); } - public static function provideItCanCreatePostgresLockIdFromKeyValueData(): array + public static function provideItCanCreatePostgresLockIdFromIntKeysData(): array { return [ - 'key + empty value' => [ - 'test', - '', - -662733300, + 'min class_id' => [ + self::DB_INT32_VALUE_MIN, 0, ], - 'key + value' => [ - 'test', - '1', - -662733300, - -2082672713, + 'max class_id' => [ + self::DB_INT32_VALUE_MAX, + 0, + ], + 'min object_id' => [ + 0, + self::DB_INT32_VALUE_MIN, + ], + 'max object_id' => [ + 0, + self::DB_INT32_VALUE_MAX, + ], + ]; + } + + #[DataProvider('provideItCanCreatePostgresLockIdFromOutOfRangeIntKeysData')] + public function testItCanNotCreatePostgresLockIdFromOutOfRangeIntKeys( + int $classId, + int $objectId, + string $expectedExceptionMessage, + ): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedExceptionMessage); + + $lockId = PostgresLockId::fromIntKeys($classId, $objectId); + + $this->assertSame($classId, $lockId->classId); + $this->assertSame($objectId, $lockId->objectId); + } + + public static function provideItCanCreatePostgresLockIdFromOutOfRangeIntKeysData(): array + { + return [ + 'min class_id' => [ + self::DB_INT32_VALUE_MIN - 1, + 0, + "Out of bound exception (classId=-2147483649 is too small)" + ], + 'max class_id' => [ + self::DB_INT32_VALUE_MAX + 1, + 0, + "Out of bound exception (classId=2147483648 is too big)" + ], + 'min object_id' => [ + 0, + self::DB_INT32_VALUE_MIN - 1, + "Out of bound exception (objectId=-2147483649 is too small)" + ], + 'max object_id' => [ + 0, + self::DB_INT32_VALUE_MAX + 1, + "Out of bound exception (objectId=2147483648 is too big)" ], ]; } From 8d68c3d5f103293cea97ad7de934fcfbac09bf40 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 01:06:20 +0300 Subject: [PATCH 05/13] Rewrite logic to two keyed lock --- .../Locker/PostgresAdvisoryLockerTest.php | 172 ++++++++---------- test/Unit/LockId/LockIdTest.php | 67 +++---- 2 files changed, 111 insertions(+), 128 deletions(-) diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index 1b1db0f..368e4ff 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -14,119 +14,103 @@ namespace Cog\Test\DbLocker\Integration\Locker; use Cog\DbLocker\Locker\PostgresAdvisoryLocker; -use Cog\DbLocker\LockId\LockId; use Cog\DbLocker\LockId\PostgresLockId; use Cog\Test\DbLocker\Integration\AbstractIntegrationTestCase; use LogicException; +use PHPUnit\Framework\Attributes\DataProvider; final class PostgresAdvisoryLockerTest extends AbstractIntegrationTestCase { private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - public function test_it_can_acquire_lock(): void + public function testItCanAcquireLock(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); $this->assertPgAdvisoryLocksCount(1); - } - - public function test_it_can_acquire_lock_with_min_class_id(): void - { - $locker = $this->initLocker(); - $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); - - $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); - - $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); - $this->assertPgAdvisoryLocksCount(1); } - public function test_it_can_acquire_lock_with_max_class_id(): void + #[DataProvider('provideItCanAcquireLockFromIntKeysCornerCasesData')] + public function testItCanAcquireLockFromIntKeysCornerCases(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MAX, 0); + $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); $this->assertPgAdvisoryLocksCount(1); - } - - public function test_it_can_acquire_lock_with_min_object_id(): void - { - $locker = $this->initLocker(); - $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockId::fromIntKeys(0, self::DB_INT32_VALUE_MIN); - - $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); - - $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); - $this->assertPgAdvisoryLocksCount(1); } - public function test_it_can_acquire_lock_with_max_object_id(): void + public static function provideItCanAcquireLockFromIntKeysCornerCasesData(): array { - $locker = $this->initLocker(); - $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = PostgresLockId::fromIntKeys(0, self::DB_INT32_VALUE_MAX); - - $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); - - $this->assertTrue($isLockAcquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); - $this->assertPgAdvisoryLocksCount(1); + return [ + 'min class_id' => [ + self::DB_INT32_VALUE_MIN, + 0, + ], + 'max class_id' => [ + self::DB_INT32_VALUE_MAX, + 0, + ], + 'min object_id' => [ + 0, + self::DB_INT32_VALUE_MIN, + ], + 'max object_id' => [ + 0, + self::DB_INT32_VALUE_MAX, + ], + ]; } - public function test_it_can_acquire_lock_in_same_connection_only_once(): void + public function testItCanAcquireLockInSameConnectionOnlyOnce(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $isLockAcquired1 = $locker->tryAcquireLock($dbConnection, $postgresLockId); $isLockAcquired2 = $locker->tryAcquireLock($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired1); $this->assertTrue($isLockAcquired2); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); $this->assertPgAdvisoryLocksCount(1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - public function test_it_can_acquire_multiple_locks_in_one_connection(): void + public function testItCanAcquireMultipleLocksInOneConnection(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = $this->initPostgresLockId('test1'); - $postgresLockId2 = $this->initPostgresLockId('test2'); + $postgresLockId1 = PostgresLockId::fromKeyValue('test1'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $isLock1Acquired = $locker->tryAcquireLock($dbConnection, $postgresLockId1); $isLock2Acquired = $locker->tryAcquireLock($dbConnection, $postgresLockId2); $this->assertTrue($isLock1Acquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId1); $this->assertTrue($isLock2Acquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); $this->assertPgAdvisoryLocksCount(2); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); } - public function test_it_cannot_acquire_same_lock_in_two_connections(): void + public function testItCannotAcquireSameLockInTwoConnections(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection1, $postgresLockId); $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); @@ -136,11 +120,11 @@ public function test_it_cannot_acquire_same_lock_in_two_connections(): void $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); } - public function test_it_can_release_lock(): void + public function testItCanReleaseLock(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection, $postgresLockId); $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); @@ -149,11 +133,11 @@ public function test_it_can_release_lock(): void $this->assertPgAdvisoryLocksCount(0); } - public function test_it_can_release_lock_twice_if_acquired_twice(): void + public function testItCanReleaseLockTwiceIfAcquiredTwice(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection, $postgresLockId); $locker->tryAcquireLock($dbConnection, $postgresLockId); @@ -165,28 +149,28 @@ public function test_it_can_release_lock_twice_if_acquired_twice(): void $this->assertPgAdvisoryLocksCount(0); } - public function test_it_can_acquire_lock_in_second_connection_after_release(): void + public function testItCanAcquireLockInSecondConnectionAfterRelease(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection1, $postgresLockId); $locker->releaseLock($dbConnection1, $postgresLockId); $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); $this->assertTrue($isLockAcquired); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId); $this->assertPgAdvisoryLocksCount(1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId); } - public function test_it_cannot_acquire_lock_in_second_connection_after_one_release_twice_locked(): void + public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLocked(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection1, $postgresLockId); $locker->tryAcquireLock($dbConnection1, $postgresLockId); @@ -200,11 +184,11 @@ public function test_it_cannot_acquire_lock_in_second_connection_after_one_relea $this->assertPgAdvisoryLockMissingInConnection($dbConnection2, $postgresLockId); } - public function test_it_cannot_release_lock_if_not_acquired(): void + public function testItCannotReleaseLockIfNotAcquired(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); @@ -212,27 +196,27 @@ public function test_it_cannot_release_lock_if_not_acquired(): void $this->assertPgAdvisoryLocksCount(0); } - public function test_it_cannot_release_lock_if_acquired_in_other_connection(): void + public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLock($dbConnection1, $postgresLockId); $isLockReleased = $locker->releaseLock($dbConnection2, $postgresLockId); $this->assertFalse($isLockReleased); - $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); $this->assertPgAdvisoryLocksCount(1); + $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); } - public function test_it_can_release_all_locks_in_connection(): void + public function testItCanReleaseAllLocksInConnection(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = $this->initPostgresLockId('test'); - $postgresLockId2 = $this->initPostgresLockId('test2'); + $postgresLockId1 = PostgresLockId::fromKeyValue('test'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $locker->tryAcquireLock($dbConnection, $postgresLockId1); $locker->tryAcquireLock($dbConnection, $postgresLockId2); @@ -241,7 +225,7 @@ public function test_it_can_release_all_locks_in_connection(): void $this->assertPgAdvisoryLocksCount(0); } - public function test_it_can_release_all_locks_in_connection_if_no_locks_were_acquired(): void + public function testItCanReleaseAllLocksInConnectionIfNoLocksWereAcquired(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); @@ -251,15 +235,15 @@ public function test_it_can_release_all_locks_in_connection_if_no_locks_were_acq $this->assertPgAdvisoryLocksCount(0); } - public function test_it_can_release_all_locks_in_connection_but_keeps_other_locks(): void + public function testItCanReleaseAllLocksInConnectionButKeepsOtherLocks(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId1 = $this->initPostgresLockId('test'); - $postgresLockId2 = $this->initPostgresLockId('test2'); - $postgresLockId3 = $this->initPostgresLockId('test3'); - $postgresLockId4 = $this->initPostgresLockId('test4'); + $postgresLockId1 = PostgresLockId::fromKeyValue('test'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); + $postgresLockId3 = PostgresLockId::fromKeyValue('test3'); + $postgresLockId4 = PostgresLockId::fromKeyValue('test4'); $locker->tryAcquireLock($dbConnection1, $postgresLockId1); $locker->tryAcquireLock($dbConnection1, $postgresLockId2); $locker->tryAcquireLock($dbConnection2, $postgresLockId3); @@ -272,11 +256,11 @@ public function test_it_can_release_all_locks_in_connection_but_keeps_other_lock $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId4); } - public function test_it_can_acquire_lock_within_transaction(): void + public function testItCanAcquireLockWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); $isLockAcquired = $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); @@ -286,7 +270,7 @@ public function test_it_can_acquire_lock_within_transaction(): void $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - public function test_it_cannot_acquire_lock_within_transaction_not_in_transaction(): void + public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void { $this->expectException(LogicException::class); $this->expectExceptionMessage( @@ -295,17 +279,17 @@ public function test_it_cannot_acquire_lock_within_transaction_not_in_transactio $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); } - public function test_it_cannot_acquire_lock_in_second_connection_if_taken_within_transaction(): void + public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection1->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection1, $postgresLockId); @@ -316,11 +300,11 @@ public function test_it_cannot_acquire_lock_in_second_connection_if_taken_within $this->assertPgAdvisoryLockExistsInConnection($dbConnection1, $postgresLockId); } - public function test_it_can_auto_release_lock_acquired_within_transaction_on_commit(): void + public function testItCanAutoReleaseLockAcquiredWithinTransactionOnCommit(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); @@ -330,11 +314,11 @@ public function test_it_can_auto_release_lock_acquired_within_transaction_on_com $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId); } - public function test_it_can_auto_release_lock_acquired_within_transaction_on_rollback(): void + public function testItCanAutoReleaseLockAcquiredWithinTransactionOnRollback(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); @@ -344,11 +328,11 @@ public function test_it_can_auto_release_lock_acquired_within_transaction_on_rol $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId); } - public function test_it_can_auto_release_lock_acquired_within_transaction_on_connection_kill(): void + public function testItCanAutoReleaseLockAcquiredWithinTransactionOnConnectionKill(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); @@ -357,11 +341,11 @@ public function test_it_can_auto_release_lock_acquired_within_transaction_on_con $this->assertPgAdvisoryLocksCount(0); } - public function test_it_cannot_release_lock_acquired_within_transaction(): void + public function testItCannotReleaseLockAcquiredWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId = $this->initPostgresLockId('test'); + $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); @@ -372,12 +356,12 @@ public function test_it_cannot_release_lock_acquired_within_transaction(): void $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - public function test_it_cannot_release_all_locks_acquired_within_transaction(): void + public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = $this->initPostgresLockId('test'); - $postgresLockId2 = $this->initPostgresLockId('test2'); + $postgresLockId1 = PostgresLockId::fromKeyValue('test'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $locker->tryAcquireLock($dbConnection, $postgresLockId1); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId2); @@ -393,10 +377,4 @@ private function initLocker(): PostgresAdvisoryLocker { return new PostgresAdvisoryLocker(); } - - private function initPostgresLockId( - string $lockKey, - ): PostgresLockId { - return PostgresLockId::fromLockId(new LockId($lockKey)); - } } diff --git a/test/Unit/LockId/LockIdTest.php b/test/Unit/LockId/LockIdTest.php index dfc028b..a22f95c 100644 --- a/test/Unit/LockId/LockIdTest.php +++ b/test/Unit/LockId/LockIdTest.php @@ -16,45 +16,50 @@ use Cog\DbLocker\LockId\LockId; use Cog\Test\DbLocker\Unit\AbstractUnitTestCase; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; final class LockIdTest extends AbstractUnitTestCase { - public function test_it_can_create_lock_id(): void - { - $lockId = new LockId('test'); - - $this->assertSame('test', (string)$lockId); - } - - public function test_it_can_create_lock_id_with_space_key(): void - { - $lockId = new LockId(' '); - - $this->assertSame(' ', (string)$lockId); + #[DataProvider('provideItCanCreateLockIdData')] + public function testItCanCreateLockId( + string $key, + string $value, + string $expectedCompiledId, + ): void { + $lockId = new LockId($key, $value); + + $this->assertSame($key, $lockId->key); + $this->assertSame($value, $lockId->value); + $this->assertSame($expectedCompiledId, (string)$lockId); } - public function test_it_can_create_lock_id_with_spaced_key(): void + public static function provideItCanCreateLockIdData(): array { - $lockId = new LockId(' test '); - - $this->assertSame(' test ', (string)$lockId); - } - - public function test_it_can_create_lock_id_with_value(): void - { - $lockId = new LockId('test', '1'); - - $this->assertSame('test:1', (string)$lockId); - } - - public function test_it_can_create_lock_id_with_value_and_spaced_key(): void - { - $lockId = new LockId(' test ', '1'); - - $this->assertSame(' test :1', (string)$lockId); + return [ + 'key only' => [ + 'test', + '', + 'test', + ], + 'key space' => [ + ' ', + '', + ' ', + ], + 'key space + value space' => [ + ' ', + ' ', + ' : ', + ], + 'key + value' => [ + ' test ', + ' 12 ', + ' test : 12 ', + ], + ]; } - public function test_it_cannot_create_lock_id_with_empty_key(): void + public function testItCannotCreateLockIdWithEmptyKey(): void { $this->expectException(InvalidArgumentException::class); From c2240b346315eccb9afe7ff6708e6755a7271922 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 01:24:21 +0300 Subject: [PATCH 06/13] Rewrite logic to two keyed lock --- README.md | 2 +- src/Locker/PostgresAdvisoryLocker.php | 40 ++++++--- .../Locker/PostgresAdvisoryLockerTest.php | 90 +++++++++---------- 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 7e92d98..e55e4bd 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ if ($isLockAcquired) { } else { // Execute logic if lock acquisition has been failed } -$postgresLocker->releaseLock($dbConnection, $postgresLockId); +$postgresLocker->releaseLockWithinSession($dbConnection, $postgresLockId); ``` ## Changelog diff --git a/src/Locker/PostgresAdvisoryLocker.php b/src/Locker/PostgresAdvisoryLocker.php index d1fa828..908f8bd 100644 --- a/src/Locker/PostgresAdvisoryLocker.php +++ b/src/Locker/PostgresAdvisoryLocker.php @@ -19,14 +19,25 @@ final class PostgresAdvisoryLocker { - public function tryAcquireLock( + /** + * Try to acquire transaction-level lock (recommended). + */ + public function tryAcquireLockWithinTransaction( PDO $dbConnection, PostgresLockId $postgresLockId, ): bool { + if ($dbConnection->inTransaction() === false) { + $lockId = $postgresLockId->humanReadableValue; + + throw new LogicException( + "Transaction-level advisory lock `$lockId` cannot be acquired outside of transaction", + ); + } + // TODO: Need to sanitize humanReadableValue? $statement = $dbConnection->prepare( <<humanReadableValue + SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- $postgresLockId->humanReadableValue SQL, ); $statement->execute( @@ -39,22 +50,17 @@ public function tryAcquireLock( return $statement->fetchColumn(0); } - public function tryAcquireLockWithinTransaction( + /** + * Try to acquire session-level lock (use only if transaction-level lock not applicable). + */ + public function tryAcquireLockWithinSession( PDO $dbConnection, PostgresLockId $postgresLockId, ): bool { - if ($dbConnection->inTransaction() === false) { - $lockId = $postgresLockId->humanReadableValue; - - throw new LogicException( - "Transaction-level advisory lock `$lockId` cannot be acquired outside of transaction", - ); - } - // TODO: Need to sanitize humanReadableValue? $statement = $dbConnection->prepare( <<humanReadableValue + SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- $postgresLockId->humanReadableValue SQL, ); $statement->execute( @@ -67,7 +73,10 @@ public function tryAcquireLockWithinTransaction( return $statement->fetchColumn(0); } - public function releaseLock( + /** + * Release session-level lock. + */ + public function releaseLockWithinSession( PDO $dbConnection, PostgresLockId $postgresLockId, ): bool { @@ -86,7 +95,10 @@ public function releaseLock( return $statement->fetchColumn(0); } - public function releaseAllLocks( + /** + * Release all session-level locks. + */ + public function releaseAllLocksWithinSession( PDO $dbConnection, ): void { $statement = $dbConnection->prepare( diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index 368e4ff..261517c 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -24,34 +24,34 @@ final class PostgresAdvisoryLockerTest extends AbstractIntegrationTestCase private const DB_INT32_VALUE_MIN = -2_147_483_648; private const DB_INT32_VALUE_MAX = 2_147_483_647; - public function testItCanAcquireLock(): void + public function testItCanTryAcquireLockWithinSession(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - #[DataProvider('provideItCanAcquireLockFromIntKeysCornerCasesData')] - public function testItCanAcquireLockFromIntKeysCornerCases(): void + #[DataProvider('provideItCanTryAcquireLockFromIntKeysCornerCasesData')] + public function testItCanTryAcquireLockFromIntKeysCornerCases(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); - $isLockAcquired = $locker->tryAcquireLock($dbConnection, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - public static function provideItCanAcquireLockFromIntKeysCornerCasesData(): array + public static function provideItCanTryAcquireLockFromIntKeysCornerCasesData(): array { return [ 'min class_id' => [ @@ -73,14 +73,14 @@ public static function provideItCanAcquireLockFromIntKeysCornerCasesData(): arra ]; } - public function testItCanAcquireLockInSameConnectionOnlyOnce(): void + public function testItCanTryAcquireLockInSameConnectionOnlyOnce(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired1 = $locker->tryAcquireLock($dbConnection, $postgresLockId); - $isLockAcquired2 = $locker->tryAcquireLock($dbConnection, $postgresLockId); + $isLockAcquired1 = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired2 = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired1); $this->assertTrue($isLockAcquired2); @@ -88,15 +88,15 @@ public function testItCanAcquireLockInSameConnectionOnlyOnce(): void $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId); } - public function testItCanAcquireMultipleLocksInOneConnection(): void + public function testItCanTryAcquireMultipleLocksInOneConnection(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test1'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $isLock1Acquired = $locker->tryAcquireLock($dbConnection, $postgresLockId1); - $isLock2Acquired = $locker->tryAcquireLock($dbConnection, $postgresLockId2); + $isLock1Acquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); + $isLock2Acquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId2); $this->assertTrue($isLock1Acquired); $this->assertTrue($isLock2Acquired); @@ -111,9 +111,9 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection1, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -125,9 +125,9 @@ public function testItCanReleaseLock(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); - $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockReleased); $this->assertPgAdvisoryLocksCount(0); @@ -138,27 +138,27 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection, $postgresLockId); - $locker->tryAcquireLock($dbConnection, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); - $isLockReleased1 = $locker->releaseLock($dbConnection, $postgresLockId); - $isLockReleased2 = $locker->releaseLock($dbConnection, $postgresLockId); + $isLockReleased1 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); + $isLockReleased2 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockReleased1); $this->assertTrue($isLockReleased2); $this->assertPgAdvisoryLocksCount(0); } - public function testItCanAcquireLockInSecondConnectionAfterRelease(): void + public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void { $locker = $this->initLocker(); $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection1, $postgresLockId); - $locker->releaseLock($dbConnection1, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -171,11 +171,11 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection1, $postgresLockId); - $locker->tryAcquireLock($dbConnection1, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); - $isLockReleased = $locker->releaseLock($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); + $isLockReleased = $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertTrue($isLockReleased); $this->assertFalse($isLockAcquired); @@ -190,7 +190,7 @@ public function testItCannotReleaseLockIfNotAcquired(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(0); @@ -202,9 +202,9 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLock($dbConnection1, $postgresLockId); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); - $isLockReleased = $locker->releaseLock($dbConnection2, $postgresLockId); + $isLockReleased = $locker->releaseLockWithinSession($dbConnection2, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); @@ -217,10 +217,10 @@ public function testItCanReleaseAllLocksInConnection(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->tryAcquireLock($dbConnection, $postgresLockId1); - $locker->tryAcquireLock($dbConnection, $postgresLockId2); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId2); - $locker->releaseAllLocks($dbConnection); + $locker->releaseAllLocksWithinSession($dbConnection); $this->assertPgAdvisoryLocksCount(0); } @@ -230,7 +230,7 @@ public function testItCanReleaseAllLocksInConnectionIfNoLocksWereAcquired(): voi $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $locker->releaseAllLocks($dbConnection); + $locker->releaseAllLocksWithinSession($dbConnection); $this->assertPgAdvisoryLocksCount(0); } @@ -244,19 +244,19 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherLocks(): void $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $postgresLockId3 = PostgresLockId::fromKeyValue('test3'); $postgresLockId4 = PostgresLockId::fromKeyValue('test4'); - $locker->tryAcquireLock($dbConnection1, $postgresLockId1); - $locker->tryAcquireLock($dbConnection1, $postgresLockId2); - $locker->tryAcquireLock($dbConnection2, $postgresLockId3); - $locker->tryAcquireLock($dbConnection2, $postgresLockId4); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId1); + $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId2); + $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId3); + $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId4); - $locker->releaseAllLocks($dbConnection1); + $locker->releaseAllLocksWithinSession($dbConnection1); $this->assertPgAdvisoryLocksCount(2); $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId3); $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId4); } - public function testItCanAcquireLockWithinTransaction(): void + public function testItCanTryAcquireLockWithinTransaction(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); @@ -293,7 +293,7 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $dbConnection1->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLock($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -349,7 +349,7 @@ public function testItCannotReleaseLockAcquiredWithinTransaction(): void $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); - $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); @@ -362,11 +362,11 @@ public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->tryAcquireLock($dbConnection, $postgresLockId1); + $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); $dbConnection->beginTransaction(); $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId2); - $locker->releaseAllLocks($dbConnection); + $locker->releaseAllLocksWithinSession($dbConnection); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId1); From bc1b55f43ca8a5284a75d5e97cc378bb4d21ff1b Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 01:33:25 +0300 Subject: [PATCH 07/13] Rewrite logic to two keyed lock --- src/Locker/PostgresAdvisoryLocker.php | 34 +++++----- src/Locker/PostgresLockModeEnum.php | 17 +++++ .../Locker/PostgresAdvisoryLockerTest.php | 66 +++++++++---------- 3 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 src/Locker/PostgresLockModeEnum.php diff --git a/src/Locker/PostgresAdvisoryLocker.php b/src/Locker/PostgresAdvisoryLocker.php index 908f8bd..e4c29f4 100644 --- a/src/Locker/PostgresAdvisoryLocker.php +++ b/src/Locker/PostgresAdvisoryLocker.php @@ -20,11 +20,12 @@ final class PostgresAdvisoryLocker { /** - * Try to acquire transaction-level lock (recommended). + * Acquire transaction-level lock (recommended). */ - public function tryAcquireLockWithinTransaction( + public function acquireLockWithinTransaction( PDO $dbConnection, PostgresLockId $postgresLockId, + PostgresLockModeEnum $lockMode = PostgresLockModeEnum::Try, ): bool { if ($dbConnection->inTransaction() === false) { $lockId = $postgresLockId->humanReadableValue; @@ -34,12 +35,12 @@ public function tryAcquireLockWithinTransaction( ); } - // TODO: Need to sanitize humanReadableValue? - $statement = $dbConnection->prepare( - <<humanReadableValue - SQL, - ); + $sql = match ($lockMode) { + PostgresLockModeEnum::Try => 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + PostgresLockModeEnum::Block => 'SELECT PG_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + }; + + $statement = $dbConnection->prepare($sql); $statement->execute( [ 'class_id' => $postgresLockId->classId, @@ -51,18 +52,19 @@ public function tryAcquireLockWithinTransaction( } /** - * Try to acquire session-level lock (use only if transaction-level lock not applicable). + * Acquire session-level lock (use only if transaction-level lock not applicable). */ - public function tryAcquireLockWithinSession( + public function acquireLockWithinSession( PDO $dbConnection, PostgresLockId $postgresLockId, + PostgresLockModeEnum $lockMode = PostgresLockModeEnum::Try, ): bool { - // TODO: Need to sanitize humanReadableValue? - $statement = $dbConnection->prepare( - <<humanReadableValue - SQL, - ); + $sql = match ($lockMode) { + PostgresLockModeEnum::Try => 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + PostgresLockModeEnum::Block => 'SELECT PG_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + }; + + $statement = $dbConnection->prepare($sql); $statement->execute( [ 'class_id' => $postgresLockId->classId, diff --git a/src/Locker/PostgresLockModeEnum.php b/src/Locker/PostgresLockModeEnum.php new file mode 100644 index 0000000..ba27df3 --- /dev/null +++ b/src/Locker/PostgresLockModeEnum.php @@ -0,0 +1,17 @@ +initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -44,7 +44,7 @@ public function testItCanTryAcquireLockFromIntKeysCornerCases(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -79,8 +79,8 @@ public function testItCanTryAcquireLockInSameConnectionOnlyOnce(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired1 = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); - $isLockAcquired2 = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired1 = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired2 = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired1); $this->assertTrue($isLockAcquired2); @@ -95,8 +95,8 @@ public function testItCanTryAcquireMultipleLocksInOneConnection(): void $postgresLockId1 = PostgresLockId::fromKeyValue('test1'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $isLock1Acquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); - $isLock2Acquired = $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId2); + $isLock1Acquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); + $isLock2Acquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId2); $this->assertTrue($isLock1Acquired); $this->assertTrue($isLock2Acquired); @@ -111,9 +111,9 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -125,7 +125,7 @@ public function testItCanReleaseLock(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId); $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -138,8 +138,8 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId); $isLockReleased1 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $isLockReleased2 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -155,10 +155,10 @@ public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -171,11 +171,11 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); $isLockReleased = $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertTrue($isLockReleased); $this->assertFalse($isLockAcquired); @@ -202,7 +202,7 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); $isLockReleased = $locker->releaseLockWithinSession($dbConnection2, $postgresLockId); @@ -217,8 +217,8 @@ public function testItCanReleaseAllLocksInConnection(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId2); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId2); $locker->releaseAllLocksWithinSession($dbConnection); @@ -244,10 +244,10 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherLocks(): void $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $postgresLockId3 = PostgresLockId::fromKeyValue('test3'); $postgresLockId4 = PostgresLockId::fromKeyValue('test4'); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId1); - $locker->tryAcquireLockWithinSession($dbConnection1, $postgresLockId2); - $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId3); - $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId4); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId1); + $locker->acquireLockWithinSession($dbConnection1, $postgresLockId2); + $locker->acquireLockWithinSession($dbConnection2, $postgresLockId3); + $locker->acquireLockWithinSession($dbConnection2, $postgresLockId4); $locker->releaseAllLocksWithinSession($dbConnection1); @@ -263,7 +263,7 @@ public function testItCanTryAcquireLockWithinTransaction(): void $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $isLockAcquired = $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -281,7 +281,7 @@ public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); } public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransaction(): void @@ -291,9 +291,9 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection1->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection1, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->tryAcquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -306,7 +306,7 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnCommit(): voi $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); $dbConnection->commit(); @@ -320,7 +320,7 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnRollback(): v $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); $dbConnection->rollBack(); @@ -334,7 +334,7 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnConnectionKil $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); $dbConnection = null; @@ -347,7 +347,7 @@ public function testItCannotReleaseLockAcquiredWithinTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -362,9 +362,9 @@ public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->tryAcquireLockWithinSession($dbConnection, $postgresLockId1); + $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); $dbConnection->beginTransaction(); - $locker->tryAcquireLockWithinTransaction($dbConnection, $postgresLockId2); + $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId2); $locker->releaseAllLocksWithinSession($dbConnection); From e74100d9064f8c144af11de54338349bc99f08fe Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 01:51:05 +0300 Subject: [PATCH 08/13] Rewrite logic to two keyed lock --- src/Locker/PostgresAdvisoryLockModeEnum.php | 17 ++ src/Locker/PostgresAdvisoryLockScopeEnum.php | 17 ++ src/Locker/PostgresAdvisoryLocker.php | 49 ++--- src/Locker/PostgresLockModeEnum.php | 17 -- .../Locker/PostgresAdvisoryLockerTest.php | 206 +++++++++++++++--- 5 files changed, 219 insertions(+), 87 deletions(-) create mode 100644 src/Locker/PostgresAdvisoryLockModeEnum.php create mode 100644 src/Locker/PostgresAdvisoryLockScopeEnum.php delete mode 100644 src/Locker/PostgresLockModeEnum.php diff --git a/src/Locker/PostgresAdvisoryLockModeEnum.php b/src/Locker/PostgresAdvisoryLockModeEnum.php new file mode 100644 index 0000000..d36cfbe --- /dev/null +++ b/src/Locker/PostgresAdvisoryLockModeEnum.php @@ -0,0 +1,17 @@ +inTransaction() === false) { - $lockId = $postgresLockId->humanReadableValue; - + if ($scope === PostgresAdvisoryLockScopeEnum::Transaction && $dbConnection->inTransaction() === false) { throw new LogicException( - "Transaction-level advisory lock `$lockId` cannot be acquired outside of transaction", + "Transaction-level advisory lock `$postgresLockId->humanReadableValue` cannot be acquired outside of transaction", ); } - $sql = match ($lockMode) { - PostgresLockModeEnum::Try => 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - PostgresLockModeEnum::Block => 'SELECT PG_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - }; - - $statement = $dbConnection->prepare($sql); - $statement->execute( - [ - 'class_id' => $postgresLockId->classId, - 'object_id' => $postgresLockId->objectId, - ], - ); - - return $statement->fetchColumn(0); - } - - /** - * Acquire session-level lock (use only if transaction-level lock not applicable). - */ - public function acquireLockWithinSession( - PDO $dbConnection, - PostgresLockId $postgresLockId, - PostgresLockModeEnum $lockMode = PostgresLockModeEnum::Try, - ): bool { - $sql = match ($lockMode) { - PostgresLockModeEnum::Try => 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - PostgresLockModeEnum::Block => 'SELECT PG_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + $sql = match ([$scope, $mode]) { + [PostgresAdvisoryLockScopeEnum::Transaction, PostgresAdvisoryLockModeEnum::Try] => + 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + [PostgresAdvisoryLockScopeEnum::Transaction, PostgresAdvisoryLockModeEnum::Block] => + 'SELECT PG_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + [PostgresAdvisoryLockScopeEnum::Session, PostgresAdvisoryLockModeEnum::Try] => + 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + [PostgresAdvisoryLockScopeEnum::Session, PostgresAdvisoryLockModeEnum::Block] => + 'SELECT PG_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, }; $statement = $dbConnection->prepare($sql); diff --git a/src/Locker/PostgresLockModeEnum.php b/src/Locker/PostgresLockModeEnum.php deleted file mode 100644 index ba27df3..0000000 --- a/src/Locker/PostgresLockModeEnum.php +++ /dev/null @@ -1,17 +0,0 @@ -initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -44,7 +49,11 @@ public function testItCanTryAcquireLockFromIntKeysCornerCases(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromIntKeys(self::DB_INT32_VALUE_MIN, 0); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -79,8 +88,16 @@ public function testItCanTryAcquireLockInSameConnectionOnlyOnce(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockAcquired1 = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); - $isLockAcquired2 = $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $isLockAcquired1 = $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); + $isLockAcquired2 = $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockAcquired1); $this->assertTrue($isLockAcquired2); @@ -95,8 +112,16 @@ public function testItCanTryAcquireMultipleLocksInOneConnection(): void $postgresLockId1 = PostgresLockId::fromKeyValue('test1'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $isLock1Acquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); - $isLock2Acquired = $locker->acquireLockWithinSession($dbConnection, $postgresLockId2); + $isLock1Acquired = $locker->acquireLock( + $dbConnection, + $postgresLockId1, + PostgresAdvisoryLockScopeEnum::Session, + ); + $isLock2Acquired = $locker->acquireLock( + $dbConnection, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLock1Acquired); $this->assertTrue($isLock2Acquired); @@ -111,9 +136,17 @@ public function testItCannotAcquireSameLockInTwoConnections(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection2, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -125,7 +158,11 @@ public function testItCanReleaseLock(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -138,8 +175,16 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $isLockReleased1 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); $isLockReleased2 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -155,10 +200,21 @@ public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); - $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->releaseLockWithinSession( + $dbConnection1, + $postgresLockId, + ); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection2, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -171,11 +227,23 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $isLockReleased = $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection2, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockReleased); $this->assertFalse($isLockAcquired); @@ -202,7 +270,11 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void $dbConnection1 = $this->initPostgresPdoConnection(); $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $isLockReleased = $locker->releaseLockWithinSession($dbConnection2, $postgresLockId); @@ -215,10 +287,16 @@ public function testItCanReleaseAllLocksInConnection(): void { $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $postgresLockId1 = PostgresLockId::fromKeyValue('test'); - $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId2); + $locker->acquireLock( + $dbConnection, + PostgresLockId::fromKeyValue('test'), + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection, + PostgresLockId::fromKeyValue('test2'), + PostgresAdvisoryLockScopeEnum::Session, + ); $locker->releaseAllLocksWithinSession($dbConnection); @@ -244,10 +322,26 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherLocks(): void $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); $postgresLockId3 = PostgresLockId::fromKeyValue('test3'); $postgresLockId4 = PostgresLockId::fromKeyValue('test4'); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId1); - $locker->acquireLockWithinSession($dbConnection1, $postgresLockId2); - $locker->acquireLockWithinSession($dbConnection2, $postgresLockId3); - $locker->acquireLockWithinSession($dbConnection2, $postgresLockId4); + $locker->acquireLock( + $dbConnection1, + $postgresLockId1, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection1, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection2, + $postgresLockId3, + PostgresAdvisoryLockScopeEnum::Session, + ); + $locker->acquireLock( + $dbConnection2, + $postgresLockId4, + PostgresAdvisoryLockScopeEnum::Session, + ); $locker->releaseAllLocksWithinSession($dbConnection1); @@ -263,7 +357,11 @@ public function testItCanTryAcquireLockWithinTransaction(): void $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $isLockAcquired = $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertTrue($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -281,7 +379,11 @@ public function testItCannotAcquireLockWithinTransactionNotInTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Transaction, + ); } public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransaction(): void @@ -291,9 +393,17 @@ public function testItCannotAcquireLockInSecondConnectionIfTakenWithinTransactio $dbConnection2 = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection1->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection1, $postgresLockId); + $locker->acquireLock( + $dbConnection1, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); - $isLockAcquired = $locker->acquireLockWithinSession($dbConnection2, $postgresLockId); + $isLockAcquired = $locker->acquireLock( + $dbConnection2, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Session, + ); $this->assertFalse($isLockAcquired); $this->assertPgAdvisoryLocksCount(1); @@ -306,7 +416,11 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnCommit(): voi $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Transaction, + ); $dbConnection->commit(); @@ -320,7 +434,11 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnRollback(): v $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Transaction, + ); $dbConnection->rollBack(); @@ -334,7 +452,11 @@ public function testItCanAutoReleaseLockAcquiredWithinTransactionOnConnectionKil $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Transaction, + ); $dbConnection = null; @@ -347,7 +469,11 @@ public function testItCannotReleaseLockAcquiredWithinTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); $dbConnection->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId); + $locker->acquireLock( + $dbConnection, + $postgresLockId, + PostgresAdvisoryLockScopeEnum::Transaction, + ); $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); @@ -362,9 +488,17 @@ public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId1 = PostgresLockId::fromKeyValue('test'); $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); - $locker->acquireLockWithinSession($dbConnection, $postgresLockId1); + $locker->acquireLock( + $dbConnection, + $postgresLockId1, + PostgresAdvisoryLockScopeEnum::Session, + ); $dbConnection->beginTransaction(); - $locker->acquireLockWithinTransaction($dbConnection, $postgresLockId2); + $locker->acquireLock( + $dbConnection, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Transaction, + ); $locker->releaseAllLocksWithinSession($dbConnection); From 788ef648b14742a1882ca8c70a7ecb82f3b52051 Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 16:25:35 +0300 Subject: [PATCH 09/13] Rewrite logic to two keyed lock --- README.md | 6 +- src/Locker/PostgresAdvisoryLocker.php | 14 ++- .../Locker/PostgresAdvisoryLockerTest.php | 87 ++++++++++++++++--- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e55e4bd..15c98d5 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,10 @@ $postgresLocker = new \Cog\DbLocker\Locker\PostgresAdvisoryLocker(); $postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromKeyValue('user', '4'); $dbConnection->beginTransaction(); -$isLockAcquired = $postgresLocker->acquireLockWithinTransaction( +$isLockAcquired = $postgresLocker->acquireLock( $dbConnection, $postgresLockId, + \Cog\DbLocker\Locker\PostgresAdvisoryLockScopeEnum::Transaction, ); if ($isLockAcquired) { // Execute logic if lock was successful @@ -59,13 +60,14 @@ $postgresLockId = \Cog\DbLocker\LockId\PostgresLockId::fromKeyValue('user', '4') $isLockAcquired = $postgresLocker->acquireLock( $dbConnection, $postgresLockId, + \Cog\DbLocker\Locker\PostgresAdvisoryLockScopeEnum::Session, ); if ($isLockAcquired) { // Execute logic if lock was successful } else { // Execute logic if lock acquisition has been failed } -$postgresLocker->releaseLockWithinSession($dbConnection, $postgresLockId); +$postgresLocker->releaseLock($dbConnection, $postgresLockId); ``` ## Changelog diff --git a/src/Locker/PostgresAdvisoryLocker.php b/src/Locker/PostgresAdvisoryLocker.php index d609186..faa380b 100644 --- a/src/Locker/PostgresAdvisoryLocker.php +++ b/src/Locker/PostgresAdvisoryLocker.php @@ -59,10 +59,15 @@ public function acquireLock( /** * Release session-level lock. */ - public function releaseLockWithinSession( + public function releaseLock( PDO $dbConnection, PostgresLockId $postgresLockId, + PostgresAdvisoryLockScopeEnum $scope = PostgresAdvisoryLockScopeEnum::Session, ): bool { + if ($scope === PostgresAdvisoryLockScopeEnum::Transaction) { + throw new \InvalidArgumentException('Transaction-level advisory lock cannot be released'); + } + $statement = $dbConnection->prepare( <<humanReadableValue @@ -81,9 +86,14 @@ public function releaseLockWithinSession( /** * Release all session-level locks. */ - public function releaseAllLocksWithinSession( + public function releaseAllLocks( PDO $dbConnection, + PostgresAdvisoryLockScopeEnum $scope = PostgresAdvisoryLockScopeEnum::Session, ): void { + if ($scope === PostgresAdvisoryLockScopeEnum::Transaction) { + throw new \InvalidArgumentException('Transaction-level advisory lock cannot be released'); + } + $statement = $dbConnection->prepare( <<<'SQL' SELECT PG_ADVISORY_UNLOCK_ALL(); diff --git a/test/Integration/Locker/PostgresAdvisoryLockerTest.php b/test/Integration/Locker/PostgresAdvisoryLockerTest.php index 536ea6f..ee3ea71 100644 --- a/test/Integration/Locker/PostgresAdvisoryLockerTest.php +++ b/test/Integration/Locker/PostgresAdvisoryLockerTest.php @@ -164,7 +164,7 @@ public function testItCanReleaseLock(): void PostgresAdvisoryLockScopeEnum::Session, ); - $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); $this->assertTrue($isLockReleased); $this->assertPgAdvisoryLocksCount(0); @@ -186,8 +186,8 @@ public function testItCanReleaseLockTwiceIfAcquiredTwice(): void PostgresAdvisoryLockScopeEnum::Session, ); - $isLockReleased1 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); - $isLockReleased2 = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); + $isLockReleased1 = $locker->releaseLock($dbConnection, $postgresLockId); + $isLockReleased2 = $locker->releaseLock($dbConnection, $postgresLockId); $this->assertTrue($isLockReleased1); $this->assertTrue($isLockReleased2); @@ -205,7 +205,7 @@ public function testItCanTryAcquireLockInSecondConnectionAfterRelease(): void $postgresLockId, PostgresAdvisoryLockScopeEnum::Session, ); - $locker->releaseLockWithinSession( + $locker->releaseLock( $dbConnection1, $postgresLockId, ); @@ -238,7 +238,7 @@ public function testItCannotAcquireLockInSecondConnectionAfterOneReleaseTwiceLoc PostgresAdvisoryLockScopeEnum::Session, ); - $isLockReleased = $locker->releaseLockWithinSession($dbConnection1, $postgresLockId); + $isLockReleased = $locker->releaseLock($dbConnection1, $postgresLockId); $isLockAcquired = $locker->acquireLock( $dbConnection2, $postgresLockId, @@ -258,7 +258,7 @@ public function testItCannotReleaseLockIfNotAcquired(): void $dbConnection = $this->initPostgresPdoConnection(); $postgresLockId = PostgresLockId::fromKeyValue('test'); - $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(0); @@ -276,7 +276,7 @@ public function testItCannotReleaseLockIfAcquiredInOtherConnection(): void PostgresAdvisoryLockScopeEnum::Session, ); - $isLockReleased = $locker->releaseLockWithinSession($dbConnection2, $postgresLockId); + $isLockReleased = $locker->releaseLock($dbConnection2, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); @@ -298,7 +298,7 @@ public function testItCanReleaseAllLocksInConnection(): void PostgresAdvisoryLockScopeEnum::Session, ); - $locker->releaseAllLocksWithinSession($dbConnection); + $locker->releaseAllLocks($dbConnection); $this->assertPgAdvisoryLocksCount(0); } @@ -308,7 +308,7 @@ public function testItCanReleaseAllLocksInConnectionIfNoLocksWereAcquired(): voi $locker = $this->initLocker(); $dbConnection = $this->initPostgresPdoConnection(); - $locker->releaseAllLocksWithinSession($dbConnection); + $locker->releaseAllLocks($dbConnection); $this->assertPgAdvisoryLocksCount(0); } @@ -343,7 +343,7 @@ public function testItCanReleaseAllLocksInConnectionButKeepsOtherLocks(): void PostgresAdvisoryLockScopeEnum::Session, ); - $locker->releaseAllLocksWithinSession($dbConnection1); + $locker->releaseAllLocks($dbConnection1); $this->assertPgAdvisoryLocksCount(2); $this->assertPgAdvisoryLockExistsInConnection($dbConnection2, $postgresLockId3); @@ -475,7 +475,7 @@ public function testItCannotReleaseLockAcquiredWithinTransaction(): void PostgresAdvisoryLockScopeEnum::Transaction, ); - $isLockReleased = $locker->releaseLockWithinSession($dbConnection, $postgresLockId); + $isLockReleased = $locker->releaseLock($dbConnection, $postgresLockId); $this->assertFalse($isLockReleased); $this->assertPgAdvisoryLocksCount(1); @@ -500,13 +500,76 @@ public function testItCannotReleaseAllLocksAcquiredWithinTransaction(): void PostgresAdvisoryLockScopeEnum::Transaction, ); - $locker->releaseAllLocksWithinSession($dbConnection); + $locker->releaseAllLocks($dbConnection); $this->assertPgAdvisoryLocksCount(1); $this->assertPgAdvisoryLockMissingInConnection($dbConnection, $postgresLockId1); $this->assertPgAdvisoryLockExistsInConnection($dbConnection, $postgresLockId2); } + public function testItCannotReleaseAllLocksWithTransactionScope(): void + { + $locker = $this->initLocker(); + $dbConnection = $this->initPostgresPdoConnection(); + $postgresLockId1 = PostgresLockId::fromKeyValue('test'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); + $locker->acquireLock( + $dbConnection, + $postgresLockId1, + PostgresAdvisoryLockScopeEnum::Session, + ); + $dbConnection->beginTransaction(); + $locker->acquireLock( + $dbConnection, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Transaction, + ); + + try { + $locker->releaseAllLocks( + $dbConnection, + PostgresAdvisoryLockScopeEnum::Transaction, + ); + } catch (\InvalidArgumentException $exception) { + $this->assertSame( + 'Transaction-level advisory lock cannot be released', + $exception->getMessage(), + ); + } + } + + public function testItCannotReleaseLocksWithTransactionScope(): void + { + $locker = $this->initLocker(); + $dbConnection = $this->initPostgresPdoConnection(); + $postgresLockId1 = PostgresLockId::fromKeyValue('test'); + $postgresLockId2 = PostgresLockId::fromKeyValue('test2'); + $locker->acquireLock( + $dbConnection, + $postgresLockId1, + PostgresAdvisoryLockScopeEnum::Session, + ); + $dbConnection->beginTransaction(); + $locker->acquireLock( + $dbConnection, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Transaction, + ); + + try { + $locker->releaseLock( + $dbConnection, + $postgresLockId2, + PostgresAdvisoryLockScopeEnum::Transaction, + ); + } catch (\InvalidArgumentException $exception) { + $this->assertSame( + 'Transaction-level advisory lock cannot be released', + $exception->getMessage(), + ); + } + } + private function initLocker(): PostgresAdvisoryLocker { return new PostgresAdvisoryLocker(); From f9a67a8a7dd2ae5b46640b302c5dee8833c220ac Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 16:52:22 +0300 Subject: [PATCH 10/13] Rewrite logic to two keyed lock --- src/Locker/PostgresAdvisoryLockModeEnum.php | 17 ------ src/Locker/PostgresAdvisoryLockTypeEnum.php | 17 ++++++ src/Locker/PostgresAdvisoryLocker.php | 60 +++++++++++++++---- src/Locker/PostgresLockModeEnum.php | 14 +++++ .../AbstractIntegrationTestCase.php | 8 +-- 5 files changed, 81 insertions(+), 35 deletions(-) delete mode 100644 src/Locker/PostgresAdvisoryLockModeEnum.php create mode 100644 src/Locker/PostgresAdvisoryLockTypeEnum.php create mode 100644 src/Locker/PostgresLockModeEnum.php diff --git a/src/Locker/PostgresAdvisoryLockModeEnum.php b/src/Locker/PostgresAdvisoryLockModeEnum.php deleted file mode 100644 index d36cfbe..0000000 --- a/src/Locker/PostgresAdvisoryLockModeEnum.php +++ /dev/null @@ -1,17 +0,0 @@ -inTransaction() === false) { throw new LogicException( @@ -34,16 +35,49 @@ public function acquireLock( ); } - $sql = match ([$scope, $mode]) { - [PostgresAdvisoryLockScopeEnum::Transaction, PostgresAdvisoryLockModeEnum::Try] => - 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - [PostgresAdvisoryLockScopeEnum::Transaction, PostgresAdvisoryLockModeEnum::Block] => - 'SELECT PG_ADVISORY_XACT_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - [PostgresAdvisoryLockScopeEnum::Session, PostgresAdvisoryLockModeEnum::Try] => - 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, - [PostgresAdvisoryLockScopeEnum::Session, PostgresAdvisoryLockModeEnum::Block] => - 'SELECT PG_ADVISORY_LOCK(:class_id, :object_id); -- ' . $postgresLockId->humanReadableValue, + $sql = match ([$scope, $type, $mode]) { + [ + PostgresAdvisoryLockScopeEnum::Transaction, + PostgresAdvisoryLockTypeEnum::NonBlocking, + PostgresLockModeEnum::Exclusive, + ] => 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Transaction, + PostgresAdvisoryLockTypeEnum::Blocking, + PostgresLockModeEnum::Exclusive, + ] => 'SELECT PG_ADVISORY_XACT_LOCK(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Transaction, + PostgresAdvisoryLockTypeEnum::NonBlocking, + PostgresLockModeEnum::Share, + ] => 'SELECT PG_TRY_ADVISORY_XACT_LOCK_SHARE(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Transaction, + PostgresAdvisoryLockTypeEnum::Blocking, + PostgresLockModeEnum::Share, + ] => 'SELECT PG_ADVISORY_XACT_LOCK_SHARE(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Session, + PostgresAdvisoryLockTypeEnum::NonBlocking, + PostgresLockModeEnum::Exclusive, + ] => 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Session, + PostgresAdvisoryLockTypeEnum::Blocking, + PostgresLockModeEnum::Exclusive, + ] => 'SELECT PG_ADVISORY_LOCK(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Session, + PostgresAdvisoryLockTypeEnum::NonBlocking, + PostgresLockModeEnum::Share, + ] => 'SELECT PG_TRY_ADVISORY_LOCK_SHARE(:class_id, :object_id);', + [ + PostgresAdvisoryLockScopeEnum::Session, + PostgresAdvisoryLockTypeEnum::Blocking, + PostgresLockModeEnum::Share, + ] => 'SELECT PG_ADVISORY_LOCK_SHARE(:class_id, :object_id);', }; + $sql .= " -- $postgresLockId->humanReadableValue"; $statement = $dbConnection->prepare($sql); $statement->execute( @@ -57,7 +91,7 @@ public function acquireLock( } /** - * Release session-level lock. + * Release session level advisory lock. */ public function releaseLock( PDO $dbConnection, @@ -84,7 +118,7 @@ public function releaseLock( } /** - * Release all session-level locks. + * Release all session level advisory locks held by the current session. */ public function releaseAllLocks( PDO $dbConnection, diff --git a/src/Locker/PostgresLockModeEnum.php b/src/Locker/PostgresLockModeEnum.php new file mode 100644 index 0000000..70a4bd3 --- /dev/null +++ b/src/Locker/PostgresLockModeEnum.php @@ -0,0 +1,14 @@ +closeAllPostgresPdoConnections(); @@ -110,7 +108,7 @@ private function findPostgresAdvisoryLockInConnection( 'lock_object_id' => $postgresLockId->objectId, 'lock_object_subid' => 2, // Using two keyed locks 'connection_pid' => $dbConnection->pgsqlGetPid(), - 'mode' => self::MODE_EXCLUSIVE, + 'mode' => PostgresLockModeEnum::Exclusive->value, ], ); @@ -137,7 +135,7 @@ private function findAllPostgresAdvisoryLocks(): array ); $statement->execute( [ - 'mode' => self::MODE_EXCLUSIVE, + 'mode' => PostgresLockModeEnum::Exclusive->value, ], ); From f4bfa6c4e1d16a129b3b5823a3f752374be2f91f Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 16:59:04 +0300 Subject: [PATCH 11/13] Rewrite logic to two keyed lock --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 15c98d5..8da6e35 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ $isLockAcquired = $postgresLocker->acquireLock( $dbConnection, $postgresLockId, \Cog\DbLocker\Locker\PostgresAdvisoryLockScopeEnum::Transaction, + \Cog\DbLocker\Locker\PostgresAdvisoryLockTypeEnum::NonBlocking, + \Cog\DbLocker\Locker\PostgresLockModeEnum::Exclusive, ); if ($isLockAcquired) { // Execute logic if lock was successful @@ -61,6 +63,8 @@ $isLockAcquired = $postgresLocker->acquireLock( $dbConnection, $postgresLockId, \Cog\DbLocker\Locker\PostgresAdvisoryLockScopeEnum::Session, + \Cog\DbLocker\Locker\PostgresAdvisoryLockTypeEnum::NonBlocking, + \Cog\DbLocker\Locker\PostgresLockModeEnum::Exclusive, ); if ($isLockAcquired) { // Execute logic if lock was successful From f0c399a508efd4e6c933fd2b707bbf572d38431f Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 17:16:13 +0300 Subject: [PATCH 12/13] Rewrite logic to two keyed lock --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8da6e35..adb440a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ ## Introduction +> WARNING! This library is currently under development and may not be stable. Use in your services at your own risk. + PHP application-level database locking mechanisms to implement concurrency control patterns. Supported drivers: From d5eb004bc5888e61c53621d11e3ebce1423c6d4c Mon Sep 17 00:00:00 2001 From: Anton Komarev Date: Sun, 27 Jul 2025 17:16:49 +0300 Subject: [PATCH 13/13] Rewrite logic to two keyed lock --- src/Locker/PostgresAdvisoryLockScopeEnum.php | 2 +- src/Locker/PostgresAdvisoryLockTypeEnum.php | 2 +- src/Locker/PostgresLockModeEnum.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Locker/PostgresAdvisoryLockScopeEnum.php b/src/Locker/PostgresAdvisoryLockScopeEnum.php index acaae4f..e573cc5 100644 --- a/src/Locker/PostgresAdvisoryLockScopeEnum.php +++ b/src/Locker/PostgresAdvisoryLockScopeEnum.php @@ -14,4 +14,4 @@ enum PostgresAdvisoryLockScopeEnum { case Session; case Transaction; -} \ No newline at end of file +} diff --git a/src/Locker/PostgresAdvisoryLockTypeEnum.php b/src/Locker/PostgresAdvisoryLockTypeEnum.php index 4426dfc..0941af1 100644 --- a/src/Locker/PostgresAdvisoryLockTypeEnum.php +++ b/src/Locker/PostgresAdvisoryLockTypeEnum.php @@ -14,4 +14,4 @@ enum PostgresAdvisoryLockTypeEnum { case NonBlocking; case Blocking; -} \ No newline at end of file +} diff --git a/src/Locker/PostgresLockModeEnum.php b/src/Locker/PostgresLockModeEnum.php index 70a4bd3..21900a3 100644 --- a/src/Locker/PostgresLockModeEnum.php +++ b/src/Locker/PostgresLockModeEnum.php @@ -11,4 +11,4 @@ enum PostgresLockModeEnum: string { case Exclusive = 'ExclusiveLock'; case Share = 'ShareLock'; -} \ No newline at end of file +}