1- {-# LANGUAGE LambdaCase #-}
2- {-# LANGUAGE MultiWayIf #-}
3- {-# LANGUAGE RecordWildCards #-}
1+ {-# LANGUAGE DeriveAnyClass #-}
2+ {-# LANGUAGE LambdaCase #-}
3+ {-# LANGUAGE MultiWayIf #-}
4+ {-# LANGUAGE RecordWildCards #-}
5+ {-# LANGUAGE ScopedTypeVariables #-}
6+ {-# LANGUAGE TypeApplications #-}
47
5- module PostgREST.Listener (runListener ) where
8+ module PostgREST.Listener (runListener , runListener' ) where
69
710import qualified Data.ByteString.Char8 as BS
811
@@ -19,28 +22,54 @@ import qualified PostgREST.Config as Config
1922import Control.Arrow ((&&&) )
2023import Data.Bitraversable (bisequence )
2124import Data.Either.Combinators (whenRight )
25+ import Data.IORef (IORef , newIORef ,
26+ readIORef , writeIORef )
2227import qualified Data.Text as T
28+ import Data.Time (UTCTime , diffUTCTime ,
29+ nominalDiffTimeToSeconds )
2330import qualified Database.PostgreSQL.LibPQ as LibPQ
2431import qualified Hasql.Session as SQL
2532import PostgREST.Config.Database (queryPgVersion )
2633import PostgREST.Config.PgVersion (pgvFullName )
2734import Protolude
35+ import System.IO.Error (isResourceVanishedError )
2836
2937-- | Starts the Listener in a thread
30- runListener :: AppState -> IO ()
31- runListener appState = do
38+ -- | Returns IO action to stop the listener thread.
39+ runListener :: AppState -> IO (IO () )
40+ runListener appState = runListener' appState (15 * minute) (30 * minute)
41+ where
42+ minute = 60
43+
44+ data ListenerStopped = ListenerStopped deriving (Show , Exception )
45+
46+ runListener' :: AppState -> Int -> Int -> IO (IO () )
47+ runListener' appState initialTcpKeepAlivesIdleSec maxTcpKeepAlivesIdleSec = do
3248 AppConfig {.. } <- getConfig appState
33- when configDbChannelEnabled $
34- void . forkIO . void $ retryingListen appState
49+ if configDbChannelEnabled then do
50+ started <- newIORef Nothing
51+ listenerThreadId <- forkIO . void $ retryingListen started initialTcpKeepAlivesIdleSec maxTcpKeepAlivesIdleSec False appState
52+ pure $ throwTo listenerThreadId ListenerStopped
53+ else
54+ mempty
3555
3656-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
3757-- | This function never returns (but can throw) and return type enforces that.
38- retryingListen :: AppState -> IO Void
39- retryingListen appState = do
58+ retryingListen :: IORef ( Maybe UTCTime ) -> Int -> Int -> Bool -> AppState -> IO ()
59+ retryingListen lastActivity currentKeepalivesIdle maxKeepalivesIdle retryingOnIdleTimeout appState = do
4060 AppConfig {.. } <- AppState. getConfig appState
4161 let
4262 dbChannel = toS configDbChannel
4363 onError err = do
64+ -- ResourceVanished should be reported when reading from socket fails
65+ -- as long as hasql-notifications does not wrap IOException in something else...
66+ let resourceVanished = maybe False isResourceVanishedError (fromException @ IOException err)
67+ (newTcpIdle, newMaxKeepalivesIdle) <-
68+ if resourceVanished then do
69+ readIORef lastActivity >>= maybe (pure (currentKeepalivesIdle, maxKeepalivesIdle)) adjustTcpIdle
70+ else
71+ pure (currentKeepalivesIdle, maxKeepalivesIdle)
72+ writeIORef lastActivity Nothing
4473 AppState. putIsListenerOn appState False
4574 observer $ DBListenFail dbChannel (Right err)
4675 when (isDbListenerBug err) $
@@ -55,14 +84,14 @@ retryingListen appState = do
5584 unless (delay == maxDelay) $
5685 AppState. putNextListenerDelay appState (delay * 2 )
5786 -- loop running the listener
58- retryingListen appState
87+ retryingListen lastActivity newTcpIdle newMaxKeepalivesIdle resourceVanished appState
5988
6089 -- Execute the listener with with error handling
61- handle onError $ do
90+ handle onError $ handle ( \ ListenerStopped -> pure () ) $ do
6291 -- Make sure we don't leak connections on errors
6392 bracket
6493 -- acquire connection
65- (SQL. acquire $ toUtf8 (Config. addTargetSessionAttrs $ Config. addFallbackAppName prettyVersion configDbUri))
94+ (SQL. acquire $ toUtf8 (addKeepalivesOptions $ Config. addTargetSessionAttrs $ Config. addFallbackAppName prettyVersion configDbUri))
6695 -- release connection
6796 (`whenRight` releaseConnection) $
6897 -- use connection
@@ -82,6 +111,7 @@ retryingListen appState = do
82111 AppState. putNextListenerDelay appState 1
83112
84113 observer $ DBListenStart pqHost pqPort pgFullName dbChannel
114+ AppState. getTime appState >>= writeIORef lastActivity . pure
85115
86116 -- wait for notifications
87117 -- this will never return, in case of an error it will throw and be caught by onError
@@ -96,15 +126,46 @@ retryingListen appState = do
96126 oneSecondInMicro = 1000000
97127 maxDelay = 32
98128
99- handleNotification channel msg =
129+ handleNotification channel msg = do
100130 if | BS. null msg -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
101131 | msg == " reload schema" -> observer (DBListenerGotSCacheMsg channel) >> cacheReloader
102132 | msg == " reload config" -> observer (DBListenerGotConfigMsg channel) >> AppState. readInDbConfig False appState
103133 | otherwise -> pure () -- Do nothing if anything else than an empty message is sent
134+ AppState. getTime appState >>= writeIORef lastActivity . Just
135+
104136
105137 cacheReloader =
106138 AppState. schemaCacheLoader appState
107139
108140 releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail ) . SQL. release
109141
110142 isDbListenerBug e = " could not access status of transaction" `T.isInfixOf` show e
143+
144+ adjustTcpIdle lastActiveTime = do
145+ currentIdleSeconds <- AppState. getTime appState <&> round . nominalDiffTimeToSeconds . (`diffUTCTime` lastActiveTime)
146+ let currentIdleTimeout = currentKeepalivesIdle + keepalivesInterval * keepalivesCount
147+ -- if our idle time == current idle timeout setting it means
148+ -- we have to make it shorter
149+ if currentIdleSeconds `div` currentIdleTimeout <= 1 then
150+ -- only adjust if this is the second idle timeout failure
151+ -- this is to eliminate spurious adjustments (TODO rethink if it is really needed)
152+ if retryingOnIdleTimeout then
153+ -- try with 1/2 of current keepalive idle
154+ -- remember that it is the new maximum we can try later
155+ pure (max 1 $ currentKeepalivesIdle `div` 2 , currentKeepalivesIdle)
156+ else
157+ pure (currentKeepalivesIdle, maxKeepalivesIdle)
158+ else
159+ pure (currentKeepalivesIdle + (maxKeepalivesIdle - currentKeepalivesIdle) `div` 2 , maxKeepalivesIdle)
160+
161+ keepalivesInterval = max 1 $ currentKeepalivesIdle `div` (5 * keepalivesCount)
162+ keepalivesCount = 5
163+
164+ -- (Config.addConnStringOption opt val) is an endomorphism
165+ -- so it is a Monoid under function composition
166+ -- Haskell is awesome
167+ addKeepalivesOptions = appEndo $ foldMap (Endo . uncurry Config. addConnStringOption . fmap show ) [
168+ (" keepalives_count" , keepalivesCount)
169+ , (" keepalives_interval" , keepalivesInterval)
170+ , (" keepalives_idle" , currentKeepalivesIdle)
171+ ]
0 commit comments