Skip to content

Commit 16c7671

Browse files
mkleczeksteve-chavez
authored andcommitted
fix: listener running with exception masked after first failure
1 parent 0a8b836 commit 16c7671

File tree

3 files changed

+15
-10
lines changed

3 files changed

+15
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. From versio
1212

1313
- Ensure Listener connections are released by @mkleczek in #4614
1414
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
15+
- Fix listener running with exception masked after first failure #4615
1516

1617
## [14.3] - 2026-01-03
1718

src/PostgREST/Listener.hs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@ runListener :: AppState -> IO ()
2424
runListener appState = do
2525
AppConfig{..} <- getConfig appState
2626
when configDbChannelEnabled $
27-
void . forkIO $ retryingListen appState
27+
void . forkIO . void $ retryingListen appState
2828

2929
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
30-
retryingListen :: AppState -> IO ()
30+
-- | This function never returns (but can throw) and return type enforces that.
31+
retryingListen :: AppState -> IO Void
3132
retryingListen appState = do
3233
AppConfig{..} <- AppState.getConfig appState
3334
let
3435
dbChannel = toS configDbChannel
35-
handleFinally err = do
36+
onError err = do
3637
AppState.putIsListenerOn appState False
3738
observer $ DBListenFail dbChannel (Right err)
3839
unless configDbPoolAutomaticRecovery $
@@ -44,10 +45,11 @@ retryingListen appState = do
4445
threadDelay (delay * oneSecondInMicro)
4546
unless (delay == maxDelay) $
4647
AppState.putNextListenerDelay appState (delay * 2)
48+
-- loop running the listener
4749
retryingListen appState
4850

49-
-- forkFinally allows to detect if the thread dies
50-
void . flip forkFinally handleFinally $ do
51+
-- Execute the listener with with error handling
52+
handle onError $ do
5153
-- Make sure we don't leak connections on errors
5254
bracket
5355
-- acquire connection
@@ -68,7 +70,10 @@ retryingListen appState = do
6870
AppState.putNextListenerDelay appState 1
6971

7072
observer $ DBListenStart dbChannel
71-
SQL.waitForNotifications handleNotification db
73+
74+
-- wait for notifications
75+
-- this will never return, in case of an error it will throw and be caught by onError
76+
forever $ SQL.waitForNotifications handleNotification db
7277

7378
Left err -> do
7479
observer $ DBListenFail dbChannel (Left err)

src/PostgREST/Observation.hs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ data Observation
4444
| SchemaCacheLoadedObs Double
4545
| ConnectionRetryObs Int
4646
| DBListenStart Text
47-
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
47+
| DBListenFail Text (Either SQL.ConnectionError SomeException)
4848
| DBListenRetry Int
4949
| DBListenerGotSCacheMsg ByteString
5050
| DBListenerGotConfigMsg ByteString
@@ -167,9 +167,8 @@ observationMessage = \case
167167
showListenerConnError :: SQL.ConnectionError -> Text
168168
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
169169

170-
showListenerException :: Either SomeException () -> Text
171-
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
172-
showListenerException (Left e) = showOnSingleLine '\t' $ show e
170+
showListenerException :: SomeException -> Text
171+
showListenerException = showOnSingleLine '\t' . show
173172

174173

175174
showOnSingleLine :: Char -> Text -> Text

0 commit comments

Comments
 (0)