Created
January 10, 2021 11:02
-
-
Save mryndzionek/f1c69baee6cc9896624d112f71c98605 to your computer and use it in GitHub Desktop.
Haskell implementation of the 'Happy Eyeballs' (also called 'Fast Fallback') algorithm - https://en.wikipedia.org/wiki/Happy_Eyeballs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env stack | |
{- stack script | |
--compile | |
--copy-bins | |
--resolver lts-16.28 | |
--install-ghc | |
--package "async random time monad-loops" | |
--ghc-options=-Wall | |
-} | |
import Control.Concurrent (threadDelay) | |
import Control.Concurrent.Async | |
( Async, | |
AsyncCancelled (..), | |
async, | |
cancel, | |
waitAny, | |
) | |
import Control.Exception (finally, fromException, try) | |
import Control.Monad.Loops (iterateUntilM) | |
import Data.Time.Clock (getCurrentTime) | |
import Data.Time.Format (defaultTimeLocale, formatTime) | |
import System.Random (randomRIO) | |
-- Haskell implementation of the 'Happy Eyeballs' (also called 'Fast Fallback') | |
-- https://en.wikipedia.org/wiki/Happy_Eyeballs | |
-- More info: https://youtu.be/oLkfnc_UMcE?t=332 | |
-- The IO `action` is just a simple delay here. | |
-- Can be replaced with any other long-running IO action | |
-- (waiting on sockets, etc.) | |
-- The algorithm itself is just two functions: | |
-- `happyEyeballs` and `runWithTimeout` | |
timeoutMs :: Int | |
timeoutMs = 1000 | |
numActions :: Int | |
numActions = 10 | |
actionTimeMinMs :: Int | |
actionTimeMinMs = 800 | |
actionTimeMaxMs :: Int | |
actionTimeMaxMs = 4000 | |
logMsg :: String -> IO () | |
logMsg msg = do | |
now <- getCurrentTime | |
let ds = formatTime defaultTimeLocale "%Y/%m/%d %H:%M:%S.%3q" now | |
putStrLn $ "[" ++ ds ++ "] " ++ msg | |
action :: Int -> IO Bool | |
action i = | |
let wait :: IO Bool | |
wait = do | |
delay <- randomRIO (actionTimeMinMs, actionTimeMaxMs) | |
logMsg $ "Starting action " ++ show i ++ " (" ++ show delay ++ "ms)" | |
threadDelay (delay * 1000) | |
r <- randomRIO (1, 4) :: IO Int | |
return (r == 1) | |
in do | |
r <- try wait | |
case r of | |
Right b -> do | |
if b | |
then logMsg ("Action " ++ show i ++ " succeeded") | |
else logMsg ("Action " ++ show i ++ " failed") | |
return b | |
Left e -> do | |
case fromException e of | |
Just AsyncCancelled -> logMsg $ "Action " ++ show i ++ " cancelled" | |
Nothing -> logMsg $ "Unknown exception in action " ++ show i | |
return False | |
timeout :: IO Bool | |
timeout = do | |
threadDelay (timeoutMs * 1000) | |
logMsg "It's taking too long !" | |
return True | |
runWithTimeout :: [Async Bool] -> Async Bool -> IO (Maybe [Async Bool]) | |
runWithTimeout as a = do | |
at <- async timeout | |
(ab, b) <- waitAny (at : a : as) `finally` cancel at | |
return $ | |
if ab == at | |
then Just (a : as) | |
else | |
if b | |
then Nothing | |
else Just $ filter (/= ab) (a : as) | |
happyEyeballs :: [IO Bool] -> IO () | |
happyEyeballs actions = do | |
_ <- iterateUntilM nomore io ([], actions) | |
return () | |
where | |
nomore ([], []) = True | |
nomore _ = False | |
io (as, ios) | |
| not $ null ios = do | |
na <- async $ head ios | |
r <- runWithTimeout as na | |
case r of | |
Just bs -> return (bs, tail ios) | |
Nothing -> do | |
mapM_ cancel (na : as) | |
return ([], []) | |
| otherwise = do | |
(fa, a) <- waitAny as | |
if a | |
then do | |
mapM_ cancel as | |
return ([], []) | |
else return (filter (/= fa) as, []) | |
main :: IO () | |
main = | |
let actions = action <$> [1 .. numActions] | |
in happyEyeballs actions | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some output of this program: