Skip to content

Commit 49941fb

Browse files
committed
refactor: Provide API to stop listener programatically
This change introduces possibility to stop listener thread programatically. It is a prerequisite for HSpec tests starting the listener as they have to be able to stop it when cleaning up.
1 parent fd3f937 commit 49941fb

8 files changed

Lines changed: 57 additions & 25 deletions

File tree

postgrest.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ test-suite observability
310310
main-is: Main.hs
311311
other-modules: ObsHelper
312312
Observation.JwtCache
313+
Observation.ListenerSpec
313314
Observation.MetricsSpec
314315
Observation.SchemaCacheSpec
315316
build-depends: base >= 4.9 && < 4.22

src/PostgREST/App.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ run appState = do
8585
NS.close mainSocket
8686
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
8787

88-
Listener.runListener appState
88+
void $ Listener.runListener appState
8989

9090
Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
9191

src/PostgREST/Listener.hs

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{-# LANGUAGE LambdaCase #-}
1+
{-# LANGUAGE DeriveAnyClass #-}
22
{-# LANGUAGE MultiWayIf #-}
33
{-# LANGUAGE RecordWildCards #-}
44

@@ -24,24 +24,30 @@ import qualified Hasql.Session as SQL
2424
import PostgREST.Config.Database (queryPgVersion)
2525
import PostgREST.Config.PgVersion (pgvFullName)
2626
import Protolude
27+
import System.Mem.Weak (deRefWeak)
28+
import Data.Monoid.Extra (mwhen)
29+
30+
data ListenerStopped = ListenerStopped deriving (Show, Exception)
31+
newtype ListenerConnectionError = ListenerConnectionError SQL.ConnectionError deriving (Show, Exception)
2732

2833
-- | Starts the Listener in a thread
29-
runListener :: AppState -> IO ()
34+
runListener :: AppState -> IO (IO ())
3035
runListener appState = do
3136
AppConfig{..} <- getConfig appState
32-
when configDbChannelEnabled $
33-
void . forkIO . void $ retryingListen appState False
37+
mwhen configDbChannelEnabled $ do
38+
listenerThreadId <- mkWeakThreadId =<< forkIO (retryingListen appState False)
39+
pure $ deRefWeak listenerThreadId >>= foldMap (`throwTo` ListenerStopped)
3440

3541
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
36-
-- | This function never returns (but can throw) and return type enforces that.
37-
retryingListen :: AppState -> Bool -> IO Void
42+
-- | This function returns upon receiving of ListenerStopped async exception.
43+
retryingListen :: AppState -> Bool -> IO ()
3844
retryingListen appState hasDbListenerBug = do
3945
cfg@AppConfig{..} <- AppState.getConfig appState
4046
let
4147
dbChannel = toS configDbChannel
4248
onError err = do
4349
AppState.putIsListenerOn appState False
44-
observer $ DBListenFail dbChannel (Right err)
50+
observer $ DBListenFail dbChannel err
4551
when (isDbListenerBug err) $
4652
observer DBListenBugCallQueryFix
4753
unless configDbPoolAutomaticRecovery $
@@ -54,24 +60,24 @@ retryingListen appState hasDbListenerBug = do
5460
unless (delay == maxDelay) $
5561
AppState.putNextListenerDelay appState (delay * 2)
5662
-- loop running the listener
57-
retryingListen appState (isDbListenerBug err)
63+
pure $ retryingListen appState (isDbListenerBug err)
5864

5965
-- Execute the listener with with error handling
60-
handle onError $ do
61-
-- Make sure we don't leak connections on errors
66+
join $ handle onError $ handle (\ListenerStopped -> mempty) $
6267
bracket
6368
-- acquire connection
6469
(SQL.acquire $
6570
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
6671
-- release connection
6772
(`whenRight` releaseConnection) $
6873
-- use connection
69-
\case
70-
Right db -> do
74+
either (throwIO . ListenerConnectionError) $ \db -> do
7175
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
7276
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
7377
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
7478
SQL.listen db $ SQL.toPgIdentifier dbChannel
79+
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
80+
SQL.listen db $ SQL.toPgIdentifier dbChannel
7581

7682
AppState.putIsListenerOn appState True
7783

@@ -88,9 +94,6 @@ retryingListen appState hasDbListenerBug = do
8894
-- this will never return, in case of an error it will throw and be caught by onError
8995
forever $ SQL.waitForNotifications handleNotification db
9096

91-
Left err -> do
92-
observer $ DBListenFail dbChannel (Left err)
93-
exitFailure
9497
where
9598
observer = AppState.getObserver appState
9699
mainThreadId = AppState.getMainThreadId appState

src/PostgREST/Logger.hs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import PostgREST.SchemaCache (queryTimingsWLabels)
4040

4141
import qualified Data.ByteString.Lazy as LBS
4242
import qualified Data.Text as T
43-
import qualified Hasql.Connection as SQL
4443
import qualified Hasql.Pool as SQL
4544
import qualified Hasql.Pool.Observation as SQL
4645
import Numeric (showFFloat)
@@ -185,7 +184,7 @@ observationMessages = \case
185184
pure $ "Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
186185
DBListenFail channel listenErr ->
187186
pure $ "Failed listening for database notifications on the " <> show channel <> " channel. " <>
188-
either showListenerConnError showListenerException listenErr
187+
showListenerException listenErr
189188
DBListenRetry delay ->
190189
pure $ "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
191190
DBListenBugCallQueryFix ->
@@ -249,10 +248,6 @@ observationMessages = \case
249248

250249
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err
251250

252-
253-
showListenerConnError :: SQL.ConnectionError -> Text
254-
showListenerConnError = maybe "Connection error" (showOnSingleLine '\t' . T.decodeUtf8)
255-
256251
showListenerException :: SomeException -> Text
257252
showListenerException = showOnSingleLine '\t' . show
258253

src/PostgREST/Observation.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ module PostgREST.Observation
1212
, ObservationHandler
1313
) where
1414

15-
import qualified Hasql.Connection as SQL
1615
import qualified Hasql.Pool as SQL
1716
import qualified Hasql.Pool.Observation as SQL
1817
import Network.HTTP.Types.Status (Status)
@@ -36,7 +35,7 @@ data Observation
3635
| SchemaCacheLoadedObs Double Text
3736
| ConnectionRetryObs Int
3837
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
39-
| DBListenFail Text (Either SQL.ConnectionError SomeException)
38+
| DBListenFail Text SomeException
4039
| DBListenRetry Int
4140
| DBListenBugCallQueryFix
4241
| DBListenerGotSCacheMsg ByteString

test/observability/Main.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import PostgREST.SchemaCache (querySchemaCache)
1818
import qualified Observation.JwtCache
1919
import qualified Observation.MetricsSpec
2020

21+
import qualified Observation.ListenerSpec
2122
import qualified Observation.SchemaCacheSpec
2223
import ObsHelper
2324
import PostgREST.Observation (Observation (HasqlPoolObs))
@@ -68,6 +69,8 @@ main = do
6869
describe "Feature.MetricsSpec" Observation.MetricsSpec.spec
6970
before (initApp baseSchemaCache testCfg) $
7071
describe "Feature.SchemaCacheSpec" Observation.SchemaCacheSpec.spec
72+
before (initApp baseSchemaCache testCfg) $
73+
describe "Observation.ListenerSpec" Observation.ListenerSpec.spec
7174

7275
where
7376
loadSCache pool conf =

test/observability/ObsHelper.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
7474
, configClientErrorVerbosity = Verbose
7575
, configDbAggregates = False
7676
, configDbAnonRole = Just "postgrest_test_anonymous"
77-
, configDbChannel = mempty
77+
, configDbChannel = "pgrst"
7878
, configDbChannelEnabled = True
7979
, configDbExtraSearchPath = []
8080
, configDbHoistedTxSettings = ["default_transaction_isolation","plan_filter.statement_cost_limit","statement_timeout"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE MonadComprehensions #-}
3+
{-# LANGUAGE NamedFieldPuns #-}
4+
{-# LANGUAGE ScopedTypeVariables #-}
5+
module Observation.ListenerSpec where
6+
7+
import Network.Wai (Application)
8+
9+
import ObsHelper
10+
11+
import qualified PostgREST.Listener as Listener
12+
import PostgREST.Observation
13+
14+
import Test.Hspec (SpecWith, describe, it)
15+
import Test.Hspec.Wai (getState)
16+
17+
import Protolude
18+
19+
spec :: SpecWith (SpecState, Application)
20+
spec = describe "Listener tests" $ do
21+
22+
it "Should start the listener" $ do
23+
SpecState{specAppState = appState, specObsChan} <- getState
24+
let waitFor = waitForObs specObsChan
25+
26+
liftIO $ withListener appState $ do
27+
waitFor (1 * sec) "DBListenStart" $ \x -> [ o | o@(DBListenStart {}) <- pure x ]
28+
29+
where
30+
sec = 1000000
31+
withListener appState = bracket (Listener.runListener appState) identity . const

0 commit comments

Comments
 (0)