From d8aac192d8c7f2d8753ca378083e14f2a8767346 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Thu, 12 Feb 2026 10:42:06 -0600 Subject: [PATCH] Harden Postgresql AdvisoryLock against closed connections during shutdown Catch exceptions in ReleaseLockAsync and DisposeAsync when the connection is already broken or closed, which occurs during graceful shutdown in multi-replica environments. Fixes double-dispose bug and checks connection state before attempting close operations. Closes GH-2146 Co-Authored-By: Claude Opus 4.6 --- .../PostgresqlNodePersistence.cs | 63 +++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/src/Persistence/Wolverine.Postgresql/PostgresqlNodePersistence.cs b/src/Persistence/Wolverine.Postgresql/PostgresqlNodePersistence.cs index 61e711716..3a5eaaa83 100644 --- a/src/Persistence/Wolverine.Postgresql/PostgresqlNodePersistence.cs +++ b/src/Persistence/Wolverine.Postgresql/PostgresqlNodePersistence.cs @@ -427,23 +427,30 @@ public async Task ReleaseLockAsync(int lockId) return; } - if (_conn == null || _conn.State == ConnectionState.Closed) + if (_conn == null || _conn.State != ConnectionState.Open) { _locks.Remove(lockId); return; } - var cancellation = new CancellationTokenSource(); - cancellation.CancelAfter(1.Seconds()); + try + { + var cancellation = new CancellationTokenSource(); + cancellation.CancelAfter(1.Seconds()); + + await _conn.ReleaseGlobalLock(lockId, cancellation.Token).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogDebug(e, "Error trying to release advisory lock {LockId} for database {Identifier}", + lockId, _databaseName); + } - await _conn.ReleaseGlobalLock(lockId, cancellation.Token).ConfigureAwait(false); _locks.Remove(lockId); if (!_locks.Any()) { - await _conn.CloseAsync().ConfigureAwait(false); - await _conn.DisposeAsync().ConfigureAwait(false); - _conn = null; + await safeCloseConnectionAsync().ConfigureAwait(false); } } @@ -456,19 +463,53 @@ public async ValueTask DisposeAsync() try { - foreach (var i in _locks) await _conn.ReleaseGlobalLock(i, CancellationToken.None).ConfigureAwait(false); + if (_conn.State == ConnectionState.Open) + { + foreach (var i in _locks) + { + try + { + await _conn.ReleaseGlobalLock(i, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogDebug(e, + "Error trying to release advisory lock {LockId} during dispose for database {Identifier}", + i, _databaseName); + } + } + } + + await safeCloseConnectionAsync().ConfigureAwait(false); + } + catch (Exception e) + { + _logger.LogDebug(e, "Error trying to dispose of advisory locks for database {Identifier}", + _databaseName); + } + } + + private async Task safeCloseConnectionAsync() + { + if (_conn == null) return; + + try + { + if (_conn.State == ConnectionState.Open) + { + await _conn.CloseAsync().ConfigureAwait(false); + } - await _conn.CloseAsync().ConfigureAwait(false); await _conn.DisposeAsync().ConfigureAwait(false); } catch (Exception e) { - _logger.LogError(e, "Error trying to dispose of advisory locks for database {Identifier}", + _logger.LogDebug(e, "Error trying to close advisory lock connection for database {Identifier}", _databaseName); } finally { - await _conn.DisposeAsync().ConfigureAwait(false); + _conn = null; } } }