fix: ensure Listener connections are released

retryingListen function potentially leaks database connections. This patch ensures the connections are released in case of listen/notify errors.

(cherry picked from commit 00c7cb1a22)
This commit is contained in:
Michal Kleczek
2026-01-28 18:26:15 -05:00
committed by Steve Chavez
parent 101eac1cce
commit 5796f86100
3 changed files with 33 additions and 16 deletions
+4
View File
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. From versio
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359 - Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
### Fixed
- Ensure Listener connections are released by @mkleczek in #4614
## [14.3] - 2026-01-03 ## [14.3] - 2026-01-03
### Fixed ### Fixed
+26 -16
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@@ -15,6 +16,7 @@ import PostgREST.Version (prettyVersion)
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config import qualified PostgREST.Config as Config
import Data.Either.Combinators (whenRight)
import Protolude import Protolude
-- | Starts the Listener in a thread -- | Starts the Listener in a thread
@@ -46,25 +48,31 @@ retryingListen appState = do
-- forkFinally allows to detect if the thread dies -- forkFinally allows to detect if the thread dies
void . flip forkFinally handleFinally $ do void . flip forkFinally handleFinally $ do
dbOrError <- SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri) -- Make sure we don't leak connections on errors
case dbOrError of bracket
Right db -> do -- acquire connection
SQL.listen db $ SQL.toPgIdentifier dbChannel (SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))
AppState.putIsListenerOn appState True -- release connection
(`whenRight` releaseConnection) $
-- use connection
\case
Right db -> do
SQL.listen db $ SQL.toPgIdentifier dbChannel
AppState.putIsListenerOn appState True
delay <- AppState.getNextListenerDelay appState delay <- AppState.getNextListenerDelay appState
when (delay > 1) $ do -- if we did a retry when (delay > 1) $ do -- if we did a retry
-- assume we lost notifications, refresh the schema cache -- assume we lost notifications, refresh the schema cache
AppState.schemaCacheLoader appState AppState.schemaCacheLoader appState
-- reset the delay -- reset the delay
AppState.putNextListenerDelay appState 1 AppState.putNextListenerDelay appState 1
observer $ DBListenStart dbChannel observer $ DBListenStart dbChannel
SQL.waitForNotifications handleNotification db SQL.waitForNotifications handleNotification db
Left err -> do Left err -> do
observer $ DBListenFail dbChannel (Left err) observer $ DBListenFail dbChannel (Left err)
exitFailure exitFailure
where where
observer = AppState.getObserver appState observer = AppState.getObserver appState
mainThreadId = AppState.getMainThreadId appState mainThreadId = AppState.getMainThreadId appState
@@ -79,3 +87,5 @@ retryingListen appState = do
cacheReloader = cacheReloader =
AppState.schemaCacheLoader appState AppState.schemaCacheLoader appState
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
+3
View File
@@ -48,6 +48,7 @@ data Observation
| DBListenRetry Int | DBListenRetry Int
| DBListenerGotSCacheMsg ByteString | DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| DBListenerConnectionCleanupFail SomeException
| QueryObs MainQuery Status | QueryObs MainQuery Status
| ConfigReadErrorObs SQL.UsageError | ConfigReadErrorObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
@@ -118,6 +119,8 @@ observationMessage = \case
"Received a schema cache reload message on the " <> show channel <> " channel" "Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel -> DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel" "Received a config reload message on the " <> show channel <> " channel"
DBListenerConnectionCleanupFail ex ->
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
QueryObs{} -> QueryObs{} ->
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty. mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
ConfigReadErrorObs usageErr -> ConfigReadErrorObs usageErr ->