@@ -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