1414namespace Cog \DbLocker \Postgres ;
1515
1616use Cog \DbLocker \ConnectionAdapterInterface ;
17+ use Cog \DbLocker \Exception \LockAcquireException ;
18+ use Cog \DbLocker \Exception \LockReleaseException ;
1719use Cog \DbLocker \Postgres \Enum \PostgresLockAccessModeEnum ;
1820use Cog \DbLocker \Postgres \Enum \PostgresLockLevelEnum ;
1921use Cog \DbLocker \Postgres \LockHandle \SessionLevelLockHandle ;
@@ -26,6 +28,10 @@ final class PostgresAdvisoryLocker
2628 * Acquire a transaction-level advisory lock with configurable timeout and access mode.
2729 *
2830 * @param TimeoutDuration $timeoutDuration Maximum wait time. Use TimeoutDuration::zero() for an immediate (non-blocking) attempt.
31+ * @return TransactionLevelLockHandle Handle with wasAcquired=false if lock is held by another process (normal competition).
32+ *
33+ * @throws LockAcquireException If a database error occurs (connection failures, query errors, etc.). NOT thrown for normal lock contention.
34+ * @throws \LogicException If attempting to acquire outside of an active transaction.
2935 */
3036 public function acquireTransactionLevelLock (
3137 ConnectionAdapterInterface $ dbConnection ,
@@ -63,6 +69,9 @@ public function acquireTransactionLevelLock(
6369 * @param TimeoutDuration $timeoutDuration Maximum wait time. Use TimeoutDuration::zero() for an immediate (non-blocking) attempt.
6470 * @return TReturn The return value of the callback.
6571 *
72+ * @throws LockAcquireException If a database error occurs during lock acquisition. NOT thrown for normal lock contention.
73+ * @throws LockReleaseException If a database error occurs during lock release (only thrown if no other exception occurred during callback execution).
74+ *
6675 * @see acquireTransactionLevelLock() for preferred locking strategy.
6776 *
6877 * @template TReturn
@@ -117,6 +126,9 @@ public function withinSessionLevelLock(
117126 * lock handle's release() method.
118127 *
119128 * @param TimeoutDuration $timeoutDuration Maximum wait time. Use TimeoutDuration::zero() for an immediate (non-blocking) attempt.
129+ * @return SessionLevelLockHandle Handle with wasAcquired=false if lock is held by another process (normal competition).
130+ *
131+ * @throws LockAcquireException If a database error occurs (connection failures, query errors, etc.). NOT thrown for normal lock contention.
120132 *
121133 * @see acquireTransactionLevelLock() for preferred locking strategy.
122134 * @see withinSessionLevelLock() for automatic session lock management.
@@ -144,25 +156,33 @@ public function acquireSessionLevelLock(
144156
145157 /**
146158 * Release session level advisory lock.
159+ *
160+ * @return bool True if the lock was successfully released, false if it was not held by this session.
161+ *
162+ * @throws LockReleaseException If a database error occurs during release.
147163 */
148164 public function releaseSessionLevelLock (
149165 ConnectionAdapterInterface $ dbConnection ,
150166 PostgresLockKey $ key ,
151167 PostgresLockAccessModeEnum $ accessMode = PostgresLockAccessModeEnum::Exclusive,
152168 ): bool {
153- $ sql = match ($ accessMode ) {
154- PostgresLockAccessModeEnum::Exclusive
155- => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id); ' ,
169+ try {
170+ $ sql = match ($ accessMode ) {
171+ PostgresLockAccessModeEnum::Exclusive
172+ => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id); ' ,
156173
157- PostgresLockAccessModeEnum::Share
158- => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id); ' ,
159- };
160- $ sql .= " -- $ key ->humanReadableValue " ;
174+ PostgresLockAccessModeEnum::Share
175+ => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id); ' ,
176+ };
177+ $ sql .= " -- $ key ->humanReadableValue " ;
161178
162- return $ dbConnection ->fetchColumn ($ sql , [
163- 'class_id ' => $ key ->classId ,
164- 'object_id ' => $ key ->objectId ,
165- ]);
179+ return $ dbConnection ->fetchColumn ($ sql , [
180+ 'class_id ' => $ key ->classId ,
181+ 'object_id ' => $ key ->objectId ,
182+ ]);
183+ } catch (\Exception $ exception ) {
184+ throw LockReleaseException::fromDatabaseError ($ key , $ exception );
185+ }
166186 }
167187
168188 /**
@@ -209,25 +229,29 @@ private function tryAcquireLock(
209229 PostgresLockLevelEnum $ level ,
210230 PostgresLockAccessModeEnum $ accessMode ,
211231 ): bool {
212- $ sql = match ([$ level , $ accessMode ]) {
213- [PostgresLockLevelEnum::Session, PostgresLockAccessModeEnum::Exclusive]
214- => 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); ' ,
232+ try {
233+ $ sql = match ([$ level , $ accessMode ]) {
234+ [PostgresLockLevelEnum::Session, PostgresLockAccessModeEnum::Exclusive]
235+ => 'SELECT PG_TRY_ADVISORY_LOCK(:class_id, :object_id); ' ,
215236
216- [PostgresLockLevelEnum::Session, PostgresLockAccessModeEnum::Share]
217- => 'SELECT PG_TRY_ADVISORY_LOCK_SHARED(:class_id, :object_id); ' ,
237+ [PostgresLockLevelEnum::Session, PostgresLockAccessModeEnum::Share]
238+ => 'SELECT PG_TRY_ADVISORY_LOCK_SHARED(:class_id, :object_id); ' ,
218239
219- [PostgresLockLevelEnum::Transaction, PostgresLockAccessModeEnum::Exclusive]
220- => 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); ' ,
240+ [PostgresLockLevelEnum::Transaction, PostgresLockAccessModeEnum::Exclusive]
241+ => 'SELECT PG_TRY_ADVISORY_XACT_LOCK(:class_id, :object_id); ' ,
221242
222- [PostgresLockLevelEnum::Transaction, PostgresLockAccessModeEnum::Share]
223- => 'SELECT PG_TRY_ADVISORY_XACT_LOCK_SHARED(:class_id, :object_id); ' ,
224- };
225- $ sql .= " -- $ key ->humanReadableValue " ;
243+ [PostgresLockLevelEnum::Transaction, PostgresLockAccessModeEnum::Share]
244+ => 'SELECT PG_TRY_ADVISORY_XACT_LOCK_SHARED(:class_id, :object_id); ' ,
245+ };
246+ $ sql .= " -- $ key ->humanReadableValue " ;
226247
227- return $ dbConnection ->fetchColumn ($ sql , [
228- 'class_id ' => $ key ->classId ,
229- 'object_id ' => $ key ->objectId ,
230- ]);
248+ return $ dbConnection ->fetchColumn ($ sql , [
249+ 'class_id ' => $ key ->classId ,
250+ 'object_id ' => $ key ->objectId ,
251+ ]);
252+ } catch (\Exception $ exception ) {
253+ throw LockAcquireException::fromDatabaseError ($ key , $ exception );
254+ }
231255 }
232256
233257 private function acquireLockWithTimeout (
@@ -276,32 +300,36 @@ private function acquireTransactionLockWithTimeout(
276300 PostgresLockKey $ key ,
277301 TimeoutDuration $ timeoutDuration ,
278302 ): bool {
279- $ timeoutMs = $ timeoutDuration ->toMilliseconds ();
280- $ dbConnection ->execute ("SET LOCAL lock_timeout = ' $ timeoutMs' " );
303+ try {
304+ $ timeoutMs = $ timeoutDuration ->toMilliseconds ();
305+ $ dbConnection ->execute ("SET LOCAL lock_timeout = ' $ timeoutMs' " );
281306
282- /**
283- * Use a savepoint so that a lock_timeout error does not abort the entire transaction.
284- * PostgreSQL handles same-name savepoints as a stack, so nested calls are safe.
285- */
286- $ dbConnection ->execute ('SAVEPOINT _lock_timeout_savepoint ' );
307+ /**
308+ * Use a savepoint so that a lock_timeout error does not abort the entire transaction.
309+ * PostgreSQL handles same-name savepoints as a stack, so nested calls are safe.
310+ */
311+ $ dbConnection ->execute ('SAVEPOINT _lock_timeout_savepoint ' );
287312
288- try {
289- $ dbConnection ->execute ($ sql , [
290- 'class_id ' => $ key ->classId ,
291- 'object_id ' => $ key ->objectId ,
292- ]);
313+ try {
314+ $ dbConnection ->execute ($ sql , [
315+ 'class_id ' => $ key ->classId ,
316+ 'object_id ' => $ key ->objectId ,
317+ ]);
293318
294- $ dbConnection ->execute ('RELEASE SAVEPOINT _lock_timeout_savepoint ' );
319+ $ dbConnection ->execute ('RELEASE SAVEPOINT _lock_timeout_savepoint ' );
295320
296- return true ;
297- } catch (\Throwable $ exception ) {
298- if ($ dbConnection ->isLockNotAvailable ($ exception )) {
299- $ dbConnection ->execute ('ROLLBACK TO SAVEPOINT _lock_timeout_savepoint ' );
321+ return true ;
322+ } catch (\Exception $ exception ) {
323+ if ($ dbConnection ->isLockNotAvailable ($ exception )) {
324+ $ dbConnection ->execute ('ROLLBACK TO SAVEPOINT _lock_timeout_savepoint ' );
300325
301- return false ;
302- }
326+ return false ;
327+ }
303328
304- throw $ exception ;
329+ throw $ exception ;
330+ }
331+ } catch (\Exception $ exception ) {
332+ throw LockAcquireException::fromDatabaseError ($ key , $ exception );
305333 }
306334 }
307335
@@ -311,26 +339,30 @@ private function acquireSessionLockWithTimeout(
311339 PostgresLockKey $ key ,
312340 TimeoutDuration $ timeoutDuration ,
313341 ): bool {
314- $ timeoutMs = $ timeoutDuration ->toMilliseconds ();
315- $ originalLockTimeout = $ dbConnection ->fetchColumn ('SHOW lock_timeout ' );
316- $ dbConnection ->execute ("SET lock_timeout = ' $ timeoutMs' " );
317-
318342 try {
319- $ dbConnection ->execute ($ sql , [
320- 'class_id ' => $ key ->classId ,
321- 'object_id ' => $ key ->objectId ,
322- ]);
343+ $ timeoutMs = $ timeoutDuration ->toMilliseconds ();
344+ $ originalLockTimeout = $ dbConnection ->fetchColumn ('SHOW lock_timeout ' );
345+ $ dbConnection ->execute ("SET lock_timeout = ' $ timeoutMs' " );
346+
347+ try {
348+ $ dbConnection ->execute ($ sql , [
349+ 'class_id ' => $ key ->classId ,
350+ 'object_id ' => $ key ->objectId ,
351+ ]);
352+
353+ return true ;
354+ } catch (\Exception $ exception ) {
355+ if ($ dbConnection ->isLockNotAvailable ($ exception )) {
356+ return false ;
357+ }
323358
324- return true ;
325- } catch (\Throwable $ exception ) {
326- if ($ dbConnection ->isLockNotAvailable ($ exception )) {
327- return false ;
359+ throw $ exception ;
328360 }
329-
330- throw $ exception ;
331- }
332- finally {
333- $ dbConnection -> execute ( " SET lock_timeout = ' $ originalLockTimeout ' " );
361+ finally {
362+ $ dbConnection -> execute ( " SET lock_timeout = ' $ originalLockTimeout ' " ) ;
363+ }
364+ } catch ( \ Exception $ exception ) {
365+ throw LockAcquireException:: fromDatabaseError ( $ key , $ exception );
334366 }
335367 }
336368}
0 commit comments