Skip to content

Commit 5889d55

Browse files
committed
Decrease lock id collisions
1 parent 1d7c0ef commit 5889d55

File tree

3 files changed

+36
-12
lines changed

3 files changed

+36
-12
lines changed

src/LockId/PostgresLockId.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
final class PostgresLockId
1919
{
20-
private const DB_INT64_VALUE_MIN = 0;
21-
private const DB_INT64_VALUE_MAX = 9223372036854775807;
20+
private const DB_INT64_VALUE_MIN = -9_223_372_036_854_775_808;
21+
private const DB_INT64_VALUE_MAX = 9_223_372_036_854_775_807;
22+
private const DB_INT32_VALUE_MAX = 2_147_483_647;
2223

2324
public function __construct(
2425
public readonly int $id,
@@ -43,9 +44,22 @@ public static function fromLockId(
4344
);
4445
}
4546

47+
/**
48+
* Generates a deterministic signed 32-bit integer ID
49+
* from a string for use as a Postgres advisory lock key.
50+
*
51+
* The crc32 function returns an unsigned 32-bit integer (0 to 4_294_967_295).
52+
* Postgres advisory locks require a signed 32-bit integer (-2_147_483_648 to 2_147_483_647).
53+
*
54+
* This method shifts the crc32 output into the signed int32 range by subtracting (2^31) + 1,
55+
* ensuring the result fits within Postgres's required range and preserves uniqueness.
56+
*
57+
* @param string $string The input string to hash into an int32 lock ID.
58+
* @return int The signed 32-bit integer suitable for Postgres advisory locks.
59+
*/
4660
private static function generateIdFromString(
4761
string $string,
4862
): int {
49-
return crc32($string);
63+
return crc32($string) - self::DB_INT32_VALUE_MAX - 1;
5064
}
5165
}

test/Integration/AbstractIntegrationTestCase.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,36 @@ private function findPostgresAdvisoryLockInConnection(
9191
PDO $dbConnection,
9292
PostgresLockId $postgresLockId,
9393
): object | null {
94-
$id = $postgresLockId->id;
95-
96-
$lockObjectId = $id % self::POSTGRES_BLOCK_SIZE;
97-
$lockCatalogId = ($id - $lockObjectId) / self::POSTGRES_BLOCK_SIZE;
94+
// For one-argument advisory locks, Postgres stores the signed 64-bit key as two 32-bit integers:
95+
// classid = high 32 bits, objid = low 32 bits.
96+
$lockClassId = ($postgresLockId->id >> 32) & 0xFFFFFFFF;
97+
$lockObjectId = $postgresLockId->id & 0xFFFFFFFF;
98+
99+
// Convert to signed 32-bit if necessary (Postgres stores as signed)
100+
if ($lockClassId > 0x7FFFFFFF) {
101+
$lockClassId -= 0x100000000;
102+
}
103+
if ($lockObjectId > 0x7FFFFFFF) {
104+
$lockObjectId -= 0x100000000;
105+
}
98106

99107
$statement = $dbConnection->prepare(
100108
<<<'SQL'
101109
SELECT *
102110
FROM pg_locks
103111
WHERE locktype = 'advisory'
104-
AND classid = :lock_catalog_id
112+
AND classid = :lock_class_id
105113
AND objid = :lock_object_id
114+
AND objsubid = :lock_object_subid
106115
AND pid = :connection_pid
107116
AND mode = :mode
108117
SQL,
109118
);
110119
$statement->execute(
111120
[
112-
'lock_catalog_id' => $lockCatalogId,
121+
'lock_class_id' => $lockClassId,
113122
'lock_object_id' => $lockObjectId,
123+
'lock_object_subid' => 1, // For one keyed value
114124
'connection_pid' => $dbConnection->pgsqlGetPid(),
115125
'mode' => self::MODE_EXCLUSIVE,
116126
],
@@ -150,7 +160,7 @@ private function closeAllPostgresPdoConnections(): void
150160
{
151161
$this->initPostgresPdoConnection()->query(
152162
<<<'SQL'
153-
SELECT PG_TERMINATE_BACKEND(pid)
163+
SELECT PG_TERMINATE_BACKEND(pid)
154164
FROM pg_stat_activity
155165
WHERE pid <> PG_BACKEND_PID()
156166
SQL,

test/Unit/LockId/PostgresLockIdTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function test_it_can_create_postgres_lock_id_from_lock_id(): void
4242

4343
$postgresLockId = PostgresLockId::fromLockId($lockId);
4444

45-
$this->assertSame(3632233996, $postgresLockId->id);
45+
$this->assertSame(1484750348, $postgresLockId->id);
4646
}
4747

4848
public function test_it_can_create_postgres_lock_id_from_lock_id_with_value(): void
@@ -51,6 +51,6 @@ public function test_it_can_create_postgres_lock_id_from_lock_id_with_value(): v
5151

5252
$postgresLockId = PostgresLockId::fromLockId($lockId);
5353

54-
$this->assertSame(782632948, $postgresLockId->id);
54+
$this->assertSame(-1364850700, $postgresLockId->id);
5555
}
5656
}

0 commit comments

Comments
 (0)