diff --git a/CHANGELOG.md b/CHANGELOG.md index bf67a8390..7a5d5a395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 +### 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 0c5c42d4e..d4d510e79 100644 --- a/src/PostgREST/Listener.hs +++ b/src/PostgREST/Listener.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RecordWildCards #-} @@ -15,6 +16,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 @@ -46,25 +48,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 @@ -79,3 +87,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 af7a6bca2..a75ee667c 100644 --- a/src/PostgREST/Observation.hs +++ b/src/PostgREST/Observation.hs @@ -48,6 +48,7 @@ data Observation | DBListenRetry Int | DBListenerGotSCacheMsg ByteString | DBListenerGotConfigMsg ByteString + | DBListenerConnectionCleanupFail SomeException | QueryObs MainQuery Status | ConfigReadErrorObs SQL.UsageError | ConfigInvalidObs Text @@ -118,6 +119,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 ->