craze-0.1.0.0: src/Network/Craze.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE RankNTypes #-}
-- | Craze is a small module for performing multiple similar HTTP GET requests
-- in parallel. This is performed through the `raceGet` function, which will
-- perform all the requests and pick the first successful response that passes
-- a certain check, menaing that the parallel requests are essentially racing
-- against each other.
--
-- __What is the usefulness of this?__
--
-- If you are dealing with data source or API that is very unreliable (high
-- latency, random failures) and there are no limitations or consequences on
-- perfoming significantly more requests, then performing multiple requests
-- (through direct connections, proxies, VPNs) may increase the chances of
-- getting a successful response faster and more reliably.
--
-- However, if using a different data source or transport is a possibility, it
-- is potentially a better option that this approach.
--
-- __Examples:__
--
-- Performing two parallel GET requests against https://chromabits.com and
-- returning the status code of the first successful one:
--
-- >>> :{
-- let racer = (Racer
-- { racerProviders =
-- [ return defaultProviderOptions
-- , return defaultProviderOptions
-- ]
-- , racerHandler = return . respStatus
-- , racerChecker = (200 ==)
-- , racerDebug = False
-- } :: Racer [(String, String)] ByteString Int)
-- in (raceGet racer "https://chromabits.com" >>= print)
-- :}
-- Just 200
--
module Network.Craze (
-- * Types
RacerHandler
, RacerChecker
, Racer(..)
, ProviderOptions(..)
-- * Functions
, defaultRacer
, defaultProviderOptions
, raceGet
-- * Providers
, simple
, delayed
) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Monad (when)
import Data.ByteString (ByteString)
import Data.Default.Class (Default, def)
import Network.Curl
-- | A `RacerHandler` is simply a function for transforming a response after it
-- is received. The handler is only applied to successful requests before they
-- are checked by the `RacerChecker`.
--
-- This is primarily for extracting or parsing a `CurlResponse_` before doing
-- any further work. The type returned by the handler will be used as the
-- input of the checker and will be the return type of `raceGet`.
type RacerHandler headerTy bodyTy a = CurlResponse_ headerTy bodyTy -> IO a
-- | A function that computes whether or not a result is valid or not.
-- Successful responses that do not pass the checker are discarded.
--
-- This should help filtering out successful responses that do not, for some
-- reason, have the expected result (e.g. Random content changes, Rate
-- Limitting, etc).
type RacerChecker a = a -> Bool
-- | A function that returns the @ProviderOptions@ to use for making a request.
type RacerProvider = IO ProviderOptions
-- | Options for a specific provider.
data ProviderOptions = ProviderOptions
{ -- | Options to pass down to Curl.
poOptions :: [CurlOption]
-- | Number of microseconds to delay the request by.
, poDelay :: Maybe Int
}
instance Default ProviderOptions where
def = ProviderOptions
{ poOptions = []
, poDelay = Nothing
}
-- | A record describing the rules for racing requests.
data Racer headerTy bodyTy a = Racer
{ racerHandler :: RacerHandler headerTy bodyTy a
, racerChecker :: RacerChecker a
-- | On a `Racer`, each `RaceProvider` represents a separate client
-- configuration. When performing a race, each provider will be used to spwan
-- a client and perform a request. This allows one to control the number of
-- requests performed and with which `CurlOption`s.
, racerProviders :: [RacerProvider]
-- | When set to `True`, debugging messages will be written to stdout.
, racerDebug :: Bool
}
instance Default (Racer [(String,String)] ByteString ByteString) where
def = Racer
{ racerHandler = return . respBody
, racerChecker = const True
, racerProviders = []
, racerDebug = False
}
-- | A `Racer` with some default values.
--
-- __Note:__ The handler will extract the response body as a `ByteString` and
-- ignore everything else, hence the type:
--
-- @
-- Racer [(String, String)] ByteString ByteString
-- @
--
-- If this is not the desired behavior, or if the response should be parsed or
-- processed, you should use the `Racer` constructor directly and provide all
-- fields.
defaultRacer :: Racer [(String,String)] ByteString ByteString
defaultRacer = def
-- | A default set of options for a provider.
defaultProviderOptions :: ProviderOptions
defaultProviderOptions = def
-- | A simple provider. It does not delay requests.
simple :: [CurlOption] -> IO ProviderOptions
simple xs = pure $ def { poOptions = xs }
-- | A provider which will delay a request by the provided number of
-- microseconds.
delayed :: [CurlOption] -> Int -> IO ProviderOptions
delayed xs d = pure $ def
{ poOptions = xs
, poDelay = Just d
}
-- | Perform a GET request on the provided URL using all providers in
-- parallel.
--
-- Rough summary of the algorithm:
--
-- - Start all requests
-- - Wait for a request to finish.
--
-- * If the request is successful, apply the handler on it.
--
-- - If the result of the handler passes the checker, cancel all other
-- requests, and return the result.
-- - If the check fails, go back to waiting for another request to finish.
--
-- * If the request fails, go back to waiting for another request to finish.
--
raceGet
:: (Eq a, CurlHeader ht, CurlBuffer bt)
=> Racer ht bt a
-> URLString
-> IO (Maybe a)
raceGet r url = do
asyncs <- mapM performGetAsync_ (racerProviders r)
when (racerDebug r) $ do
putStr "[racer] Created Asyncs: "
print (map asyncThreadId asyncs)
waitForOne asyncs (racerHandler r) (racerChecker r) (racerDebug r)
where
performGetAsync_
:: (CurlHeader ht, CurlBuffer bt)
=> RacerProvider
-> IO (Async (CurlResponse_ ht bt))
performGetAsync_ = performGetAsync url
waitForOne
:: (Eq a)
=> [Async (CurlResponse_ ht bt)]
-> RacerHandler ht bt a
-> RacerChecker a
-> Bool
-> IO (Maybe a)
waitForOne asyncs handler check debug
= if null asyncs then pure Nothing else do
winner <- waitAnyCatch asyncs
case winner of
(as, Right a) -> do
result <- handler a
if check result then do
cancelAll (except as asyncs)
when debug $ putStr "[racer] Winner: " >> print (asyncThreadId as)
pure $ Just result
else waitForOne (except as asyncs) handler check debug
(as, Left _) -> waitForOne (except as asyncs) handler check debug
cancelAll :: [Async a] -> IO ()
cancelAll = mapM_ (async . cancel)
except :: (Eq a) => a -> [a] -> [a]
except x = filter (x /=)
performGetAsync
:: (CurlHeader ht, CurlBuffer bt)
=> URLString
-> RacerProvider
-> IO (Async (CurlResponse_ ht bt))
performGetAsync url provider = async $ do
options <- provider
case poDelay options of
Nothing -> pure ()
Just delay -> threadDelay delay
curlGetResponse_ url (poOptions options)