diff --git a/src/Network/GRPC/Client.hs b/src/Network/GRPC/Client.hs index 271733f..9738d19 100644 --- a/src/Network/GRPC/Client.hs +++ b/src/Network/GRPC/Client.hs @@ -9,6 +9,7 @@ module Network.GRPC.Client ( -- ** Reconnection policy , ReconnectPolicy(..) + , ReconnectTo(..) , exponentialBackoff -- ** Connection parameters diff --git a/src/Network/GRPC/Client/Connection.hs b/src/Network/GRPC/Client/Connection.hs index a9f4ffa..070474e 100644 --- a/src/Network/GRPC/Client/Connection.hs +++ b/src/Network/GRPC/Client/Connection.hs @@ -16,6 +16,7 @@ module Network.GRPC.Client.Connection ( , SslKeyLog(..) , ConnParams(..) , ReconnectPolicy(..) + , ReconnectTo(..) , exponentialBackoff -- * Using the connection , connParams @@ -29,7 +30,6 @@ import Control.Concurrent.STM import Control.Monad import Control.Monad.Catch import Data.Default -import Data.Maybe import GHC.Stack import Network.HPACK qualified as HPACK import Network.HTTP2.Client qualified as HTTP2.Client @@ -169,15 +169,27 @@ data ReconnectPolicy = -- | Reconnect to the (potentially different) server after the IO action -- returns -- - -- If the 'Maybe' is 'Just', we'll attempt to reconnect to a server at the - -- new address. If 'Nothing', we'll attempt to connect to the original - -- server that 'withConnection' was given. + -- The 'ReconnectTo' can be used to implement a rudimentary redundancy + -- scheme. For example, you could decide to reconnect to a known fallback + -- server after connection to a main server fails a certain number of times. -- -- This is a very general API: typically the IO action will call -- 'threadDelay' after some amount of time (which will typically involve -- some randomness), but it can be used to do things such as display a -- message to the user somewhere that the client is reconnecting. - | ReconnectAfter (Maybe Server) (IO ReconnectPolicy) + | ReconnectAfter ReconnectTo (IO ReconnectPolicy) + +-- | What server should we attempt to reconnect to? +-- +-- * 'ReconnectToPrevious' will attempt to reconnect to the last server we +-- attempted to connect to, whether or not that attempt was successful. +-- * 'ReconnectToOriginal' will attempt to reconnect to the original server that +-- 'withConnection' was given. +-- * 'ReconnectToNew' will attempt to connect to the newly specified server. +data ReconnectTo = + ReconnectToPrevious + | ReconnectToOriginal + | ReconnectToNew Server -- | The default policy is 'DontReconnect' -- @@ -186,6 +198,9 @@ data ReconnectPolicy = instance Default ReconnectPolicy where def = DontReconnect +instance Default ReconnectTo where + def = ReconnectToPrevious + -- | Exponential backoff -- -- If the exponent is @1@, the delay interval will be the same every step; @@ -213,7 +228,7 @@ exponentialBackoff waitFor e = go where go :: (Double, Double) -> Word -> ReconnectPolicy go _ 0 = DontReconnect - go (lo, hi) n = ReconnectAfter Nothing $ do + go (lo, hi) n = ReconnectAfter def $ do delay <- randomRIO (lo, hi) waitFor $ round $ delay * 1_000_000 return $ go (lo * e, hi * e) (pred n) @@ -431,9 +446,16 @@ stayConnected connParams initialServer connStateVar connOutOfScope = do atomically $ writeTVar connStateVar $ ConnectionAbandoned err (False, DontReconnect) -> do atomically $ writeTVar connStateVar $ ConnectionAbandoned err - (False, ReconnectAfter mNewServer f) -> do + atomically $ writeTVar connStateVar $ ConnectionAbandoned err + (False, ReconnectAfter to f) -> do + let + nextServer = + case to of + ReconnectToPrevious -> server + ReconnectToOriginal -> initialServer + ReconnectToNew new -> new atomically $ writeTVar connStateVar $ ConnectionNotReady - loop (fromMaybe initialServer mNewServer) =<< f + loop nextServer =<< f -- | Insecure connection (no TLS) connectInsecure :: ConnParams -> Attempt -> Address -> IO () diff --git a/test-grapesy/Test/Driver/ClientServer.hs b/test-grapesy/Test/Driver/ClientServer.hs index 11388ca..bbde47c 100644 --- a/test-grapesy/Test/Driver/ClientServer.hs +++ b/test-grapesy/Test/Driver/ClientServer.hs @@ -533,7 +533,7 @@ runTestClient cfg firstTestFailure port clientRun = do -- This avoids a race condition between the server starting first -- and the client starting first. , connReconnectPolicy = - Client.ReconnectAfter Nothing $ do + Client.ReconnectAfter def $ do threadDelay 100_000 return Client.DontReconnect } diff --git a/test-grapesy/Test/Sanity/Disconnect.hs b/test-grapesy/Test/Sanity/Disconnect.hs index ea7c44a..4a26527 100644 --- a/test-grapesy/Test/Sanity/Disconnect.hs +++ b/test-grapesy/Test/Sanity/Disconnect.hs @@ -199,16 +199,16 @@ test_serverDisconnect = withTemporaryFile $ \ipcFile -> do go :: Int -> Client.ReconnectPolicy go n | n == 5 - = Client.ReconnectAfter Nothing $ do + = Client.ReconnectAfter def $ do killRestarted <- startServer port2 <- ipcRead putMVar signalRestart killRestarted return $ Client.ReconnectAfter - (Just $ serverAddress port2) + (Client.ReconnectToNew $ serverAddress port2) (pure Client.DontReconnect) | otherwise - = Client.ReconnectAfter Nothing $ do + = Client.ReconnectAfter def $ do threadDelay 10000 return $ go (n + 1) @@ -296,6 +296,9 @@ echoHandler disconnectCounter call = trackDisconnects disconnectCounter $ do Auxiliary -------------------------------------------------------------------------------} +-- We need to use this to properly simulate the execution environment crashing +-- in an unrecoverable way. In particular, we don't want to give the program a +-- chance to do any of its normal exception handling/cleanup behavior. foreign import ccall unsafe "exit" c_exit :: CInt -> IO () data ClientStep = KeepGoing (Maybe (IO ())) ClientStep | Done