You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add exception safety to withinSessionLevelLock (#11)
- Preserve original exception when release also fails
- Skip release when lock was not acquired (wasAcquired === false)
- Add ADR-002 documenting the design decision
# ADR-002: Exception safety in withinSessionLevelLock
2
+
3
+
## Status
4
+
5
+
Accepted
6
+
7
+
## Context
8
+
9
+
`withinSessionLevelLock()` acquires a session-level advisory lock, executes a user callback, and releases the lock. The release must happen reliably, but two edge cases require careful handling:
10
+
11
+
1.**Double failure.** The callback throws, and then the release also throws (e.g. the database connection was lost). In PHP, an exception thrown from a `finally` block replaces the in-flight exception. A naive `try/finally` would expose the caller to a `PDOException` from release instead of the real error.
12
+
13
+
2.**Release of an unacquired lock.** With a zero timeout the lock may not be acquired (`wasAcquired === false`). Calling `PG_ADVISORY_UNLOCK` on a key that was never held returns `false` in isolation, but if the same key was previously acquired in the same session (reentrant locking), the call decrements the counter and silently releases someone else's lock.
14
+
15
+
## Decision
16
+
17
+
Use `try/catch/finally` with two guards:
18
+
19
+
-**Skip release when the lock was not acquired** — `if ($lockHandle->wasAcquired)`.
20
+
-**Protect the original exception** — catch the callback exception, store it, re-throw; wrap release in its own `try/catch` so a release failure cannot mask the original.
### Why suppress the release exception (not chain it)
52
+
53
+
PHP sets `previous` only in the exception constructor — there is no `addPrevious()`. Wrapping the original in a new exception would change its type and break typed `catch` blocks in user code. Suppressing the secondary release error is the lesser evil: the root cause (callback failure) is always preserved.
54
+
55
+
### Why not log the suppressed exception
56
+
57
+
The library has no logger dependency and adding one for a single edge case is not justified. Users who need visibility into release failures can wrap `withinSessionLevelLock` in their own try/catch or use `acquireSessionLevelLock` / `releaseSessionLevelLock` directly.
58
+
59
+
## Consequences
60
+
61
+
- Original exceptions from user callbacks are never masked by release failures.
62
+
-`PG_ADVISORY_UNLOCK` is not called when the lock was never acquired, preventing reentrant counter corruption.
63
+
- Release failures are silently suppressed when a callback exception is already in flight — acceptable trade-off given no logger.
0 commit comments