Skip to content

Commit

Permalink
Introduce ReconnectTo
Browse files Browse the repository at this point in the history
Reconnect policies can now specify whether they want to attempt reconnection
with the original server given to `withConnection`, the last server we attempted
connection with, or a new server specified by the policy itself.
  • Loading branch information
FinleyMcIlwaine committed Aug 30, 2024
1 parent 592ec92 commit bab1fdd
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/Network/GRPC/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Network.GRPC.Client (

-- ** Reconnection policy
, ReconnectPolicy(..)
, ReconnectTo(..)
, exponentialBackoff

-- ** Connection parameters
Expand Down
38 changes: 30 additions & 8 deletions src/Network/GRPC/Client/Connection.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module Network.GRPC.Client.Connection (
, SslKeyLog(..)
, ConnParams(..)
, ReconnectPolicy(..)
, ReconnectTo(..)
, exponentialBackoff
-- * Using the connection
, connParams
Expand All @@ -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
Expand Down Expand Up @@ -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'
--
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 ()
Expand Down
2 changes: 1 addition & 1 deletion test-grapesy/Test/Driver/ClientServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 6 additions & 3 deletions test-grapesy/Test/Sanity/Disconnect.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bab1fdd

Please sign in to comment.