@@ -28,14 +28,14 @@ final class PostgresAdvisoryLocker
28
28
*/
29
29
public function acquireTransactionLevelLock (
30
30
PDO $ dbConnection ,
31
- PostgresLockKey $ postgresLockId ,
31
+ PostgresLockKey $ postgresLockKey ,
32
32
PostgresLockWaitModeEnum $ waitMode = PostgresLockWaitModeEnum::NonBlocking,
33
33
PostgresLockAccessModeEnum $ accessMode = PostgresLockAccessModeEnum::Exclusive,
34
34
): TransactionLevelLockHandle {
35
35
return new TransactionLevelLockHandle (
36
36
wasAcquired: $ this ->acquireLock (
37
37
$ dbConnection ,
38
- $ postgresLockId ,
38
+ $ postgresLockKey ,
39
39
PostgresLockLevelEnum::Transaction,
40
40
$ waitMode ,
41
41
$ accessMode ,
@@ -49,62 +49,105 @@ public function acquireTransactionLevelLock(
49
49
* ⚠️ You MUST retain the returned handle in a variable.
50
50
* If the handle is not stored and is immediately garbage collected,
51
51
* the lock will be released in the lock handle __destruct method.
52
+ * @see SessionLevelLockHandle::__destruct
52
53
*
53
54
* @example
54
55
* $handle = $locker->acquireSessionLevelLock(...); // ✅ Lock held
55
56
*
56
57
* $locker->acquireSessionLevelLock(...); // ❌ Lock immediately released
57
58
*
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.
66
62
* @see acquireTransactionLevelLock() for preferred locking strategy.
67
63
*/
68
64
public function acquireSessionLevelLock (
69
65
PDO $ dbConnection ,
70
- PostgresLockKey $ postgresLockId ,
66
+ PostgresLockKey $ postgresLockKey ,
71
67
PostgresLockWaitModeEnum $ waitMode = PostgresLockWaitModeEnum::NonBlocking,
72
68
PostgresLockAccessModeEnum $ accessMode = PostgresLockAccessModeEnum::Exclusive,
73
69
): SessionLevelLockHandle {
74
70
return new SessionLevelLockHandle (
75
71
$ dbConnection ,
76
72
$ this ,
77
- $ postgresLockId ,
73
+ $ postgresLockKey ,
78
74
$ accessMode ,
79
75
wasAcquired: $ this ->acquireLock (
80
76
$ dbConnection ,
81
- $ postgresLockId ,
77
+ $ postgresLockKey ,
82
78
PostgresLockLevelEnum::Session,
83
79
$ waitMode ,
84
80
$ accessMode ,
85
81
),
86
82
);
87
83
}
88
84
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
+
89
132
/**
90
133
* Release session level advisory lock.
91
134
*/
92
135
public function releaseSessionLevelLock (
93
136
PDO $ dbConnection ,
94
- PostgresLockKey $ postgresLockId ,
137
+ PostgresLockKey $ postgresLockKey ,
95
138
PostgresLockAccessModeEnum $ accessMode = PostgresLockAccessModeEnum::Exclusive,
96
139
): bool {
97
140
$ sql = match ($ accessMode ) {
98
141
PostgresLockAccessModeEnum::Exclusive => 'SELECT PG_ADVISORY_UNLOCK(:class_id, :object_id); ' ,
99
142
PostgresLockAccessModeEnum::Share => 'SELECT PG_ADVISORY_UNLOCK_SHARED(:class_id, :object_id); ' ,
100
143
};
101
- $ sql .= " -- $ postgresLockId ->humanReadableValue " ;
144
+ $ sql .= " -- $ postgresLockKey ->humanReadableValue " ;
102
145
103
146
$ statement = $ dbConnection ->prepare ($ sql );
104
147
$ statement ->execute (
105
148
[
106
- 'class_id ' => $ postgresLockId ->classId ,
107
- 'object_id ' => $ postgresLockId ->objectId ,
149
+ 'class_id ' => $ postgresLockKey ->classId ,
150
+ 'object_id ' => $ postgresLockKey ->objectId ,
108
151
],
109
152
);
110
153
@@ -127,14 +170,14 @@ public function releaseAllSessionLevelLocks(
127
170
128
171
private function acquireLock (
129
172
PDO $ dbConnection ,
130
- PostgresLockKey $ postgresLockId ,
173
+ PostgresLockKey $ postgresLockKey ,
131
174
PostgresLockLevelEnum $ level ,
132
175
PostgresLockWaitModeEnum $ waitMode = PostgresLockWaitModeEnum::NonBlocking,
133
176
PostgresLockAccessModeEnum $ accessMode = PostgresLockAccessModeEnum::Exclusive,
134
177
): bool {
135
178
if ($ level === PostgresLockLevelEnum::Transaction && $ dbConnection ->inTransaction () === false ) {
136
179
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 " ,
138
181
);
139
182
}
140
183
@@ -180,13 +223,13 @@ private function acquireLock(
180
223
PostgresLockAccessModeEnum::Share,
181
224
] => 'SELECT PG_ADVISORY_LOCK_SHARED(:class_id, :object_id); ' ,
182
225
};
183
- $ sql .= " -- $ postgresLockId ->humanReadableValue " ;
226
+ $ sql .= " -- $ postgresLockKey ->humanReadableValue " ;
184
227
185
228
$ statement = $ dbConnection ->prepare ($ sql );
186
229
$ statement ->execute (
187
230
[
188
- 'class_id ' => $ postgresLockId ->classId ,
189
- 'object_id ' => $ postgresLockId ->objectId ,
231
+ 'class_id ' => $ postgresLockKey ->classId ,
232
+ 'object_id ' => $ postgresLockKey ->objectId ,
190
233
],
191
234
);
192
235
0 commit comments