Skip to content

Commit a9e0c84

Browse files
committed
Leave only *Handler API
1 parent b72f27b commit a9e0c84

File tree

1 file changed

+65
-22
lines changed

1 file changed

+65
-22
lines changed

src/Postgres/PostgresAdvisoryLocker.php

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ final class PostgresAdvisoryLocker
2828
*/
2929
public function acquireTransactionLevelLock(
3030
PDO $dbConnection,
31-
PostgresLockKey $postgresLockId,
31+
PostgresLockKey $postgresLockKey,
3232
PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking,
3333
PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive,
3434
): TransactionLevelLockHandle {
3535
return new TransactionLevelLockHandle(
3636
wasAcquired: $this->acquireLock(
3737
$dbConnection,
38-
$postgresLockId,
38+
$postgresLockKey,
3939
PostgresLockLevelEnum::Transaction,
4040
$waitMode,
4141
$accessMode,
@@ -49,62 +49,105 @@ public function acquireTransactionLevelLock(
4949
* ⚠️ You MUST retain the returned handle in a variable.
5050
* If the handle is not stored and is immediately garbage collected,
5151
* the lock will be released in the lock handle __destruct method.
52+
* @see SessionLevelLockHandle::__destruct
5253
*
5354
* @example
5455
* $handle = $locker->acquireSessionLevelLock(...); // ✅ Lock held
5556
*
5657
* $locker->acquireSessionLevelLock(...); // ❌ Lock immediately released
5758
*
58-
* ⚠️ Transaction-level advisory locks are strongly preferred over session-level locks.
59-
* Session-level locks persist beyond transactions and may lead to deadlocks
60-
* or require manual cleanup (e.g. `pg_advisory_unlock_all()`).
61-
*
62-
* Use session-level locks only when transactional locks are not suitable
63-
* (transactions are not possible or redundant).
64-
*
65-
* @see SessionLevelLockHandle::__destruct
59+
* ⚠️ Transaction-level advisory locks are strongly preferred whenever possible,
60+
* as they are automatically released at the end of a transaction and are less error-prone.
61+
* Use session-level locks only when transactional context is not available.
6662
* @see acquireTransactionLevelLock() for preferred locking strategy.
6763
*/
6864
public function acquireSessionLevelLock(
6965
PDO $dbConnection,
70-
PostgresLockKey $postgresLockId,
66+
PostgresLockKey $postgresLockKey,
7167
PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking,
7268
PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive,
7369
): SessionLevelLockHandle {
7470
return new SessionLevelLockHandle(
7571
$dbConnection,
7672
$this,
77-
$postgresLockId,
73+
$postgresLockKey,
7874
$accessMode,
7975
wasAcquired: $this->acquireLock(
8076
$dbConnection,
81-
$postgresLockId,
77+
$postgresLockKey,
8278
PostgresLockLevelEnum::Session,
8379
$waitMode,
8480
$accessMode,
8581
),
8682
);
8783
}
8884

85+
/**
86+
* Acquires a session-level advisory lock and ensures its release after executing the callback.
87+
*
88+
* This method guarantees that the lock is released even if an exception is thrown during execution.
89+
* Useful for safely wrapping critical sections that require locking.
90+
*
91+
* If the lock was not acquired (i.e., `wasAcquired` is `false`), it is up to the callback
92+
* to decide how to handle the situation (e.g., retry, throw, log, or silently skip).
93+
*
94+
* ⚠️ Transaction-level advisory locks are strongly preferred whenever possible,
95+
* as they are automatically released at the end of a transaction and are less error-prone.
96+
* Use session-level locks only when transactional context is not available.
97+
* @see acquireTransactionLevelLock() for preferred locking strategy.
98+
*
99+
* @param PDO $dbConnection Active database connection.
100+
* @param PostgresLockKey $postgresLockKey Lock key to be acquired.
101+
* @param callable(SessionLevelLockHandle): TReturn $callback A callback that receives the lock handle.
102+
* @param PostgresLockWaitModeEnum $waitMode Whether to wait for the lock or fail immediately. Default is non-blocking.
103+
* @param PostgresLockAccessModeEnum $accessMode Whether to acquire a shared or exclusive lock. Default is exclusive.
104+
* @return TReturn The return value of the callback.
105+
*
106+
* @template TReturn
107+
*
108+
* TODO: Cover with tests
109+
*/
110+
public function withSessionLevelLock(
111+
PDO $dbConnection,
112+
PostgresLockKey $postgresLockKey,
113+
callable $callback,
114+
PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking,
115+
PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive,
116+
): mixed {
117+
$lockHandle = $this->acquireSessionLevelLock(
118+
$dbConnection,
119+
$postgresLockKey,
120+
$waitMode,
121+
$accessMode,
122+
);
123+
124+
try {
125+
return $callback($lockHandle);
126+
}
127+
finally {
128+
$lockHandle->release();
129+
}
130+
}
131+
89132
/**
90133
* Release session level advisory lock.
91134
*/
92135
public function releaseSessionLevelLock(
93136
PDO $dbConnection,
94-
PostgresLockKey $postgresLockId,
137+
PostgresLockKey $postgresLockKey,
95138
PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive,
96139
): bool {
97140
$sql = match ($accessMode) {
98141
PostgresLockAccessModeEnum::Exclusive => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id);',
99142
PostgresLockAccessModeEnum::Share => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id);',
100143
};
101-
$sql .= " -- $postgresLockId->humanReadableValue";
144+
$sql .= " -- $postgresLockKey->humanReadableValue";
102145

103146
$statement = $dbConnection->prepare($sql);
104147
$statement->execute(
105148
[
106-
'class_id' => $postgresLockId->classId,
107-
'object_id' => $postgresLockId->objectId,
149+
'class_id' => $postgresLockKey->classId,
150+
'object_id' => $postgresLockKey->objectId,
108151
],
109152
);
110153

@@ -127,14 +170,14 @@ public function releaseAllSessionLevelLocks(
127170

128171
private function acquireLock(
129172
PDO $dbConnection,
130-
PostgresLockKey $postgresLockId,
173+
PostgresLockKey $postgresLockKey,
131174
PostgresLockLevelEnum $level,
132175
PostgresLockWaitModeEnum $waitMode = PostgresLockWaitModeEnum::NonBlocking,
133176
PostgresLockAccessModeEnum $accessMode = PostgresLockAccessModeEnum::Exclusive,
134177
): bool {
135178
if ($level === PostgresLockLevelEnum::Transaction && $dbConnection->inTransaction() === false) {
136179
throw new LogicException(
137-
"Transaction-level advisory lock `$postgresLockId->humanReadableValue` cannot be acquired outside of transaction",
180+
"Transaction-level advisory lock `$postgresLockKey->humanReadableValue` cannot be acquired outside of transaction",
138181
);
139182
}
140183

@@ -180,13 +223,13 @@ private function acquireLock(
180223
PostgresLockAccessModeEnum::Share,
181224
] => 'SELECT PG_ADVISORY_LOCK_SHARED(:class_id, :object_id);',
182225
};
183-
$sql .= " -- $postgresLockId->humanReadableValue";
226+
$sql .= " -- $postgresLockKey->humanReadableValue";
184227

185228
$statement = $dbConnection->prepare($sql);
186229
$statement->execute(
187230
[
188-
'class_id' => $postgresLockId->classId,
189-
'object_id' => $postgresLockId->objectId,
231+
'class_id' => $postgresLockKey->classId,
232+
'object_id' => $postgresLockKey->objectId,
190233
],
191234
);
192235

0 commit comments

Comments
 (0)