fix: listener running with exception masked after first failure

This commit is contained in:
Michal Kleczek
2026-01-29 13:22:46 -05:00
committed by steve-chavez
parent 0a8b836435
commit 16c767134c
3 changed files with 15 additions and 10 deletions
+1
View File
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. From versio
- Ensure Listener connections are released by @mkleczek in #4614
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
- Fix listener running with exception masked after first failure #4615
## [14.3] - 2026-01-03
+11 -6
View File
@@ -24,15 +24,16 @@ runListener :: AppState -> IO ()
runListener appState = do
AppConfig{..} <- getConfig appState
when configDbChannelEnabled $
void . forkIO $ retryingListen appState
void . forkIO . void $ retryingListen appState
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
retryingListen :: AppState -> IO ()
-- | This function never returns (but can throw) and return type enforces that.
retryingListen :: AppState -> IO Void
retryingListen appState = do
AppConfig{..} <- AppState.getConfig appState
let
dbChannel = toS configDbChannel
handleFinally err = do
onError err = do
AppState.putIsListenerOn appState False
observer $ DBListenFail dbChannel (Right err)
unless configDbPoolAutomaticRecovery $
@@ -44,10 +45,11 @@ retryingListen appState = do
threadDelay (delay * oneSecondInMicro)
unless (delay == maxDelay) $
AppState.putNextListenerDelay appState (delay * 2)
-- loop running the listener
retryingListen appState
-- forkFinally allows to detect if the thread dies
void . flip forkFinally handleFinally $ do
-- Execute the listener with with error handling
handle onError $ do
-- Make sure we don't leak connections on errors
bracket
-- acquire connection
@@ -68,7 +70,10 @@ retryingListen appState = do
AppState.putNextListenerDelay appState 1
observer $ DBListenStart dbChannel
SQL.waitForNotifications handleNotification db
-- wait for notifications
-- this will never return, in case of an error it will throw and be caught by onError
forever $ SQL.waitForNotifications handleNotification db
Left err -> do
observer $ DBListenFail dbChannel (Left err)
+3 -4
View File
@@ -44,7 +44,7 @@ data Observation
| SchemaCacheLoadedObs Double
| ConnectionRetryObs Int
| DBListenStart Text
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
| DBListenFail Text (Either SQL.ConnectionError SomeException)
| DBListenRetry Int
| DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString
@@ -167,9 +167,8 @@ observationMessage = \case
showListenerConnError :: SQL.ConnectionError -> Text
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
showListenerException :: Either SomeException () -> Text
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
showListenerException (Left e) = showOnSingleLine '\t' $ show e
showListenerException :: SomeException -> Text
showListenerException = showOnSingleLine '\t' . show
showOnSingleLine :: Char -> Text -> Text