From 00c7cb1a22d5042d98003d85b0e8bc2ff7e62f71 Mon Sep 17 00:00:00 2001 From: Michal Kleczek Date: Mon, 26 Jan 2026 22:26:24 +0100 Subject: [PATCH] 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. --- CHANGELOG.md | 4 ++++ src/PostgREST/Listener.hs | 42 ++++++++++++++++++++++-------------- src/PostgREST/Observation.hs | 3 +++ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb9034f01..ee2cad413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ All notable changes to this project will be documented in this file. From versio - Add a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581 - Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599 +### Fixed + +- Ensure Listener connections are released by @mkleczek in #4614 + ## [14.3] - 2026-01-03 ### Fixed diff --git a/src/PostgREST/Listener.hs b/src/PostgREST/Listener.hs index 362a67eab..b8680a743 100644 --- a/src/PostgREST/Listener.hs +++ b/src/PostgREST/Listener.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RecordWildCards #-} @@ -16,6 +17,7 @@ import PostgREST.Version (prettyVersion) import qualified PostgREST.AppState as AppState import qualified PostgREST.Config as Config +import Data.Either.Combinators (whenRight) import Protolude -- | Starts the Listener in a thread @@ -49,25 +51,31 @@ retryingListen appState = do -- forkFinally allows to detect if the thread dies void . flip forkFinally handleFinally $ do - dbOrError <- SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri) - case dbOrError of - Right db -> do - SQL.listen db $ SQL.toPgIdentifier dbChannel - AppState.putIsListenerOn appState True + -- Make sure we don't leak connections on errors + bracket + -- acquire connection + (SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri)) + -- release connection + (`whenRight` releaseConnection) $ + -- use connection + \case + Right db -> do + SQL.listen db $ SQL.toPgIdentifier dbChannel + AppState.putIsListenerOn appState True - delay <- AppState.getNextListenerDelay appState - when (delay > 1) $ do -- if we did a retry - -- assume we lost notifications, refresh the schema cache - AppState.schemaCacheLoader appState - -- reset the delay - AppState.putNextListenerDelay appState 1 + delay <- AppState.getNextListenerDelay appState + when (delay > 1) $ do -- if we did a retry + -- assume we lost notifications, refresh the schema cache + AppState.schemaCacheLoader appState + -- reset the delay + AppState.putNextListenerDelay appState 1 - observer $ DBListenStart dbChannel - SQL.waitForNotifications handleNotification db + observer $ DBListenStart dbChannel + SQL.waitForNotifications handleNotification db - Left err -> do - observer $ DBListenFail dbChannel (Left err) - exitFailure + Left err -> do + observer $ DBListenFail dbChannel (Left err) + exitFailure where observer = AppState.getObserver appState mainThreadId = AppState.getMainThreadId appState @@ -82,3 +90,5 @@ retryingListen appState = do cacheReloader = AppState.schemaCacheLoader appState + + releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release diff --git a/src/PostgREST/Observation.hs b/src/PostgREST/Observation.hs index 6c7a1cf64..23adac727 100644 --- a/src/PostgREST/Observation.hs +++ b/src/PostgREST/Observation.hs @@ -50,6 +50,7 @@ data Observation | DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147 | DBListenerGotSCacheMsg ByteString | DBListenerGotConfigMsg ByteString + | DBListenerConnectionCleanupFail SomeException | QueryObs MainQuery Status | ConfigReadErrorObs SQL.UsageError | ConfigInvalidObs Text @@ -122,6 +123,8 @@ observationMessage = \case "Received a schema cache reload message on the " <> show channel <> " channel" DBListenerGotConfigMsg channel -> "Received a config reload message on the " <> show channel <> " channel" + DBListenerConnectionCleanupFail ex -> + "Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex) 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. ConfigReadErrorObs usageErr ->