Skip to content

Commit 0ec31f5

Browse files
committed
fix: Listener retries should be performed with exceptions unmasked
forkFinally calls error handler with exceptions masked. Our handleFinally does not restore exception state before calling retryingListener so after first error the whole listener is running with exceptions masked. This patch ensures handleFinally is run with exceptions unmasked.
1 parent 1682677 commit 0ec31f5

File tree

2 files changed

+16
-10
lines changed

2 files changed

+16
-10
lines changed

src/PostgREST/Listener.hs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ retryingListen appState = do
3939
AppConfig{..} <- AppState.getConfig appState
4040
let
4141
dbChannel = toS configDbChannel
42-
handleFinally err = do
42+
onError err = do
4343
AppState.putIsListenerOn appState False
4444
observer $ DBListenFail dbChannel (Right err)
4545
when (isDbListenerBug err) $
@@ -53,10 +53,16 @@ retryingListen appState = do
5353
threadDelay (delay * oneSecondInMicro)
5454
unless (delay == maxDelay) $
5555
AppState.putNextListenerDelay appState (delay * 2)
56+
-- loop running the listener
5657
retryingListen appState
5758

58-
-- forkFinally allows to detect if the thread dies
59-
void . flip forkFinally handleFinally $ do
59+
-- Execute the listener with with error handling
60+
-- We use mask to make sure onError is invoked in presence of asynchronous exceptions
61+
-- but we execute the onError handler in an unmasked state to
62+
-- 1. allow it to be interrupted
63+
-- 2. make sure recursive call to retryingListen is not masked
64+
-- TODO is masking really necessary here? We unmask the onError handler anyway...
65+
void $ mask $ \unmask -> handle (unmask . onError) $ unmask $ do
6066
-- Make sure we don't leak connections on errors
6167
bracket
6268
-- acquire connection
@@ -81,6 +87,8 @@ retryingListen appState = do
8187

8288
observer $ DBListenStart pqHost pqPort pgFullName dbChannel
8389

90+
-- wait for notifications indefinitely
91+
-- this will never return, in case of an error it will throw and be caught by onError
8492
SQL.waitForNotifications handleNotification db
8593

8694
Left err -> do

src/PostgREST/Observation.hs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ data Observation
4545
| SchemaCacheLoadedObs Double
4646
| ConnectionRetryObs Int
4747
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
48-
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ()))
48+
| DBListenFail Text (Either SQL.ConnectionError SomeException)
4949
| DBListenRetry Int
5050
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
5151
| DBListenerGotSCacheMsg ByteString
@@ -171,14 +171,12 @@ observationMessage = \case
171171
showListenerConnError :: SQL.ConnectionError -> Text
172172
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
173173

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

178177

179178
showOnSingleLine :: Char -> Text -> Text
180179
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
181180

182-
isDbListenerBug :: Either SomeException () -> Bool
183-
isDbListenerBug (Left e) = "could not access status of transaction" `T.isInfixOf` show e
184-
isDbListenerBug _ = False
181+
isDbListenerBug :: SomeException -> Bool
182+
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e

0 commit comments

Comments
 (0)