packages feed

craze 0.1.2.0 → 0.1.3.0

raw patch · 4 files changed

+449/−228 lines, 4 filesdep +lensdep +lifted-asyncdep +lifted-base

Dependencies added: lens, lifted-async, lifted-base, monad-control, mtl, transformers-base

Files

craze.cabal view
@@ -1,9 +1,9 @@--- This file has been generated from package.yaml by hpack version 0.9.0.+-- This file has been generated from package.yaml by hpack version 0.14.0. -- -- see: https://github.com/sol/hpack  name:                craze-version:             0.1.2.0+version:             0.1.3.0 synopsis:            HTTP Racing Library description:         A micro-library for racing HTTP GET requests homepage:            https://github.com/etcinit/craze#readme@@ -29,11 +29,19 @@     , bytestring     , transformers     , text+    , mtl+    , lifted-async+    , lifted-base+    , monad-control+    , transformers-base+    , lens     , async     , data-default-class     , containers   exposed-modules:       Network.Craze+      Network.Craze.Internal+      Network.Craze.Types   default-language: Haskell2010  executable craze-example@@ -47,13 +55,19 @@     , bytestring     , transformers     , text+    , mtl+    , lifted-async+    , lifted-base+    , monad-control+    , transformers-base+    , lens     , craze     , optparse-generic   default-language: Haskell2010 -test-suite craze-spec+test-suite craze-doctest   type: exitcode-stdio-1.0-  main-is: Spec.hs+  main-is: Doctest.hs   hs-source-dirs:       test   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wall -fno-warn-unused-do-bind@@ -63,20 +77,23 @@     , bytestring     , transformers     , text+    , mtl+    , lifted-async+    , lifted-base+    , monad-control+    , transformers-base+    , lens     , craze-    , hspec-    , hspec-discover-    , http-types-    , haxy-    , HTTP+    , doctest+    , doctest-discover   other-modules:-      Doctest       Network.CrazeSpec+      Spec   default-language: Haskell2010 -test-suite craze-doctest+test-suite craze-spec   type: exitcode-stdio-1.0-  main-is: Doctest.hs+  main-is: Spec.hs   hs-source-dirs:       test   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wall -fno-warn-unused-do-bind@@ -86,10 +103,19 @@     , bytestring     , transformers     , text+    , mtl+    , lifted-async+    , lifted-base+    , monad-control+    , transformers-base+    , lens     , craze-    , doctest-    , doctest-discover+    , hspec+    , hspec-discover+    , http-types+    , haxy+    , HTTP   other-modules:+      Doctest       Network.CrazeSpec-      Spec   default-language: Haskell2010
src/Network/Craze.hs view
@@ -1,22 +1,25 @@-{-# LANGUAGE FlexibleInstances  #-}-{-# LANGUAGE GADTs              #-}-{-# LANGUAGE ImpredicativeTypes #-}-{-# LANGUAGE OverloadedStrings  #-}-{-# LANGUAGE RankNTypes         #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE GADTs                 #-}+{-# LANGUAGE ImpredicativeTypes    #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE RankNTypes            #-}+{-# LANGUAGE RecordWildCards       #-}  -- | 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+-- a certain check, meaning 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.+-- latency, random failures) and there are no limitations on performing+-- 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.@@ -26,11 +29,17 @@ -- Performing two parallel GET requests against https://chromabits.com and -- returning the status code of the first successful one: --+-- The providers generate two client configurations. The handler "parses" the+-- response (in this case it just gets the status code). Finally, the checker+-- filters out responses that we don't consider valid (anything that is not+-- HTTP 200 in this case).+--+-- >>> :set -XOverloadedStrings -- >>> :{ --  let racer = (Racer --                { racerProviders =---                    [ return defaultProviderOptions---                    , return defaultProviderOptions+--                    [ simpleTagged [] "Client A"+--                    , simpleTagged [] "Client B" --                    ] --                , racerHandler = return . respStatus --                , racerChecker = (200 ==)@@ -46,150 +55,39 @@     RacerHandler   , RacerChecker   , Racer(..)+  , RacerProvider   , ProviderOptions(..)-  , RacerResult+  , RacerResult(..)+  , ClientStatus(..)   -- * Functions-  , defaultRacer-  , defaultProviderOptions   , raceGet   , raceGetResult   -- * Providers+  -- $providers   , simple   , simpleTagged   , delayed   , delayedTagged-  -- * Results-  , rrResponse-  , rrWinner-  , rrProviders+  -- * Deprecated+  , defaultRacer+  , defaultProviderOptions   ) where -import           Control.Concurrent       (threadDelay)+import Control.Monad (when)+import Data.Map.Lazy (keys, lookup)+import Prelude       hiding (lookup)+ import           Control.Concurrent.Async-import           Control.Monad            (when)+import           Control.Monad.State      (runStateT)+import           Control.Monad.Trans      (MonadIO, liftIO) import           Data.ByteString          (ByteString)-import           Data.Default.Class       (Default, def)-import           Data.Map.Lazy            (Map, delete, elems, fromList, keys,-                                           lookup, mapWithKey)-import           Data.Monoid              ((<>))+import           Data.Default.Class       (def) import           Data.Text                (Text, pack) import qualified Data.Text.IO             as TIO import           Network.Curl-import           Prelude                  hiding (lookup) --- | 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-    -- | A tag to identify this type provider.-  , poTag     :: Text-  } deriving (Show)--instance Default ProviderOptions where-  def = ProviderOptions-    { poOptions = []-    , poDelay = Nothing-    , poTag = "default"-    }---- | The result of a racing operation. This can be used to collect statistics--- on which providers win more often, etc.-data RacerResult a = RacerResult-  { rrResponse  :: Maybe a-  , rrWinner    :: Maybe ProviderOptions-  , rrProviders :: [RacerProvider]-  }---- | 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-  -- | When set to `True`, the Racer will attempt to return the last response-  -- in the event that all responses failed to pass the checker. This can be-  -- used for identifying error conditions.-  , racerReturnLast :: Bool-  }--instance Default (Racer [(String,String)] ByteString ByteString) where-  def = Racer-    { racerHandler = return . respBody-    , racerChecker = const True-    , racerProviders = []-    , racerDebug = False-    , racerReturnLast = 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 }---- | Like @simple@, but with a tag for identification.-simpleTagged :: [CurlOption] -> Text -> IO ProviderOptions-simpleTagged xs t = do-  opts <- simple xs-  pure $ opts { poTag = t }---- | 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-  }---- | Like @delayed@, but with a tag for identification.-delayedTagged :: [CurlOption] -> Int -> Text -> IO ProviderOptions-delayedTagged xs d t = do-  opts <- delayed xs d-  pure $ opts { poTag = t }+import Network.Craze.Internal+import Network.Craze.Types  -- | Perform a GET request on the provided URL using all providers in -- parallel.@@ -210,105 +108,91 @@ --       finish. -- raceGet-  :: (Eq a, CurlHeader ht, CurlBuffer bt)+  :: (Eq a, CurlHeader ht, CurlBuffer bt, MonadIO m)   => Racer ht bt a   -> URLString-  -> IO (Maybe a)+  -> m (Maybe a) raceGet r url = rrResponse <$> raceGetResult r url  -- | Same as @raceGet@, but returns a @RacerResult@ which contains more -- information about the race performed. raceGetResult-  :: (Eq a, CurlHeader ht, CurlBuffer bt)+  :: (Eq a, CurlHeader ht, CurlBuffer bt, MonadIO m)   => Racer ht bt a   -> URLString-  -> IO (RacerResult a)-raceGetResult r url = do-  asyncs <- fromList <$> mapM performGetAsync_ (racerProviders r)+  -> m (RacerResult a)+raceGetResult r@Racer{..} url = do+  initialState@RaceState{..} <- makeRaceState (pack url) r -  when (racerDebug r) $ do+  let asyncs = keys _rsClientMap++  when racerDebug . liftIO $ do     TIO.putStr "[racer] Created Asyncs: "-    print . elems $ mapWithKey identifier asyncs+    print $ asyncThreadId <$> asyncs -  maybeResponse <- waitForOne-    asyncs (racerHandler r) (racerChecker r) (racerDebug r) (racerReturnLast r)+  (maybeResponse, finalState) <- runStateT waitForOne initialState    pure $ case maybeResponse of     Nothing -> RacerResult       { rrResponse = Nothing       , rrWinner = Nothing-      , rrProviders = racerProviders r+      , rrProviders = racerProviders+      , rrStatuses = extractStatuses finalState       }     Just (as, response) -> RacerResult       { rrResponse = Just response-      , rrWinner = lookup as asyncs-      , rrProviders = racerProviders r+      , rrWinner = _csOptions <$> lookup as _rsClientMap+      , rrProviders = racerProviders+      , rrStatuses = extractStatuses finalState       }-    where-      performGetAsync_-        :: (CurlHeader ht, CurlBuffer bt)-        => RacerProvider-        -> IO (Async (CurlResponse_ ht bt), ProviderOptions)-      performGetAsync_ = performGetAsync url -waitForOne-  :: (Eq a)-  => Map (Async (CurlResponse_ ht bt)) ProviderOptions-  -> RacerHandler ht bt a-  -> RacerChecker a-  -> Bool-  -> Bool-  -> IO (Maybe (Async (CurlResponse_ ht bt), a))-waitForOne asyncs handler check debug returnLast-  = if null asyncs then pure Nothing else do-    winner <- waitAnyCatch (keys asyncs)--    case winner of-      (as, Right a) -> do-        result <- handler a--        let remaining = delete as asyncs--        if check result then do-          cancelAll (keys remaining)--          when debug $ do-            TIO.putStr "[racer] Winner: "-            print (asyncThreadId as)--          pure $ Just (as, result)-          else (if returnLast && null remaining-            then do-              when debug $ do-                TIO.putStr "[racer] Reached last. Returning: "-                print (asyncThreadId as)--              pure $ Just (as, result)-            else waitForOne remaining handler check debug returnLast-          )-      (as, Left _) -> waitForOne-        (delete as asyncs) handler check debug returnLast--cancelAll :: [Async a] -> IO ()-cancelAll = mapM_ (async . cancel)+-- $providers+--+-- 'RacerProvider' provide client configurations. Craze comes bundled with a+-- few built-in providers which can be used for quickly building client+-- configurations. -identifier :: Async (CurlResponse_ ht bt) -> ProviderOptions -> Text-identifier a o = poTag o <> ":" <> (pack . show . asyncThreadId $ a)+-- | A simple provider. It does not delay requests.+simple :: Monad m => [CurlOption] -> m ProviderOptions+simple xs = pure $ def { poOptions = xs } -performGetAsync-  :: (CurlHeader ht, CurlBuffer bt)-  => URLString-  -> RacerProvider-  -> IO (Async (CurlResponse_ ht bt), ProviderOptions)-performGetAsync url provider = do-  options <- provider+-- | Like @simple@, but with a tag for identification.+simpleTagged :: Monad m => [CurlOption] -> Text -> m ProviderOptions+simpleTagged xs t = do+  opts <- simple xs+  pure $ opts { poTag = t } -  responseAsync <- async $ do-    case poDelay options of-      Nothing -> pure ()-      Just delay -> threadDelay delay+-- | A provider which will delay a request by the provided number of+-- microseconds.+delayed :: Monad m => [CurlOption] -> Int -> m ProviderOptions+delayed xs d = pure $ def+  { poOptions = xs+  , poDelay = Just d+  } -    curlGetResponse_ url (poOptions options)+-- | Like @delayed@, but with a tag for identification.+delayedTagged :: Monad m => [CurlOption] -> Int -> Text -> m ProviderOptions+delayedTagged xs d t = do+  opts <- delayed xs d+  pure $ opts { poTag = t } -  pure (responseAsync, options)+-- | 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+{-# DEPRECATED defaultRacer "Use Data.Default.Class.def instead" #-} +-- | A default set of options for a provider.+defaultProviderOptions :: ProviderOptions+defaultProviderOptions = def+{-# DEPRECATED defaultProviderOptions "Use Data.Default.Class.def instead" #-}
+ src/Network/Craze/Internal.hs view
@@ -0,0 +1,201 @@+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE RecordWildCards       #-}+{-# LANGUAGE TemplateHaskell       #-}++module Network.Craze.Internal where++import           Control.Exception (SomeException)+import           Control.Monad     (forM, when)+import           Data.Map.Lazy     (Map)+import qualified Data.Map.Lazy     as M+import           Data.Monoid       ((<>))++import           Control.Concurrent.Async.Lifted (Async, async, asyncThreadId,+                                                  cancel, waitAnyCatch)+import           Control.Concurrent.Lifted       (threadDelay)+import           Control.Lens                    (at, makeLenses, use, (&),+                                                  (.~), (?=))+import           Control.Monad.State             (MonadState)+import           Control.Monad.Trans             (MonadIO, liftIO)+import           Data.Text                       (Text)+import qualified Data.Text                       as T+import qualified Data.Text.IO                    as TIO+import           Network.Curl                    (CurlBuffer, CurlHeader,+                                                  CurlResponse_,+                                                  curlGetResponse_)++import Network.Craze.Types++type ClientMap ht bt a = Map (Async (CurlResponse_ ht bt)) (ClientState a)++data ClientState a = ClientState+  { _csOptions :: ProviderOptions+  , _csStatus  :: ClientStatus a+  }++data RaceState ht bt a = RaceState+  { _rsClientMap  :: ClientMap ht bt a+  , _rsChecker    :: RacerChecker a+  , _rsHandler    :: RacerHandler ht bt a+  , _rsDebug      :: Bool+  , _rsReturnLast :: Bool+  }++makeLenses ''ClientState+makeLenses ''RaceState++extractStatuses :: RaceState ht bt a -> [(Text, ClientStatus a)]+extractStatuses RaceState{..} = M.elems $ makeTuple <$>  _rsClientMap+  where+    makeTuple :: ClientState a -> (Text, ClientStatus a)+    makeTuple ClientState{..} = (poTag _csOptions, _csStatus)++makeRaceState+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)+  => Text+  -> Racer ht bt a+  -> m (RaceState ht bt a)+makeRaceState url Racer{..} = do+  providerMap <- makeClientMap url racerProviders++  pure $ RaceState+    providerMap+    racerChecker+    racerHandler+    racerDebug+    racerReturnLast++makeClientMap+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)+  => Text+  -> [RacerProvider]+  -> m (ClientMap ht bt a)+makeClientMap url providers = M.fromList <$> forM providers (makeClient url)++makeClient+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)+  => Text+  -> RacerProvider+  -> m (Async (CurlResponse_ ht bt), ClientState a)+makeClient url provider = liftIO $ do+  options <- provider+  future <- async $ performGet url options++  pure (future, ClientState options Pending)++performGet+  :: (CurlHeader ht, CurlBuffer bt)+  => Text+  -> ProviderOptions+  -> IO (CurlResponse_ ht bt)+performGet url ProviderOptions{..} = do+  case poDelay of+    Nothing -> pure ()+    Just delay -> threadDelay delay++  curlGetResponse_ (T.unpack url) poOptions++cancelAll :: MonadIO m => [Async a] -> m ()+cancelAll = liftIO . mapM_ (async . cancel)++cancelRemaining+  :: (MonadIO m, MonadState (RaceState ht bt a) m)+  => m ()+cancelRemaining = do+  remaining <- onlyPending <$> use rsClientMap++  cancelAll $ M.keys remaining++identifier :: Async (CurlResponse_ ht bt) -> ProviderOptions -> Text+identifier a o = poTag o <> ":" <> (T.pack . show . asyncThreadId $ a)++onlyPending :: ClientMap ht bt a -> ClientMap ht bt a+onlyPending = M.filter (isPending . _csStatus)++isPending :: ClientStatus a -> Bool+isPending Pending = True+isPending _ = False++markAsSuccessful+  :: (MonadState (RaceState ht bt a) m)+  => Async (CurlResponse_ ht bt)+  -> a+  -> m ()+markAsSuccessful key result = do+  maybePrevious <- use $ rsClientMap . at key++  case maybePrevious of+    Just previous -> (rsClientMap . at key)+      ?= (previous & csStatus .~ Successful result)+    Nothing -> pure ()++markAsFailure+  :: (MonadState (RaceState ht bt a) m)+  => Async (CurlResponse_ ht bt)+  -> a+  -> m ()+markAsFailure key result = do+  maybePrevious <- use $ rsClientMap . at key++  case maybePrevious of+    Just previous -> (rsClientMap . at key)+      ?= (previous & csStatus .~ Failed result)+    Nothing -> pure ()++markAsErrored+  :: (MonadState (RaceState ht bt a) m)+  => Async (CurlResponse_ ht bt)+  -> SomeException+  -> m ()+markAsErrored key result = do+  maybePrevious <- use $ rsClientMap . at key++  case maybePrevious of+    Just previous -> (rsClientMap . at key)+      ?= (previous & csStatus .~ Errored result)+    Nothing -> pure ()++waitForOne+  :: (Eq a, MonadIO m, MonadState (RaceState ht bt a) m)+  => m (Maybe (Async (CurlResponse_ ht bt), a))+waitForOne = do+  debug <- use rsDebug+  providerMap <- use rsClientMap++  let asyncs = _csOptions <$> onlyPending providerMap++  if null asyncs then pure Nothing else do+    winner <- liftIO $ waitAnyCatch (M.keys asyncs)++    case winner of+      (as, Right a) -> do+        handler <- use rsHandler+        check <- use rsChecker+        returnLast <- use rsReturnLast+        result <- liftIO $ handler a++        if check result then do+          markAsSuccessful as result+          cancelRemaining++          when debug . liftIO $ do+            TIO.putStr "[racer] Winner: "+            print (asyncThreadId as)++          pure $ Just (as, result)+          else do+            markAsFailure as result++            remaining <- M.keys . onlyPending <$> use rsClientMap++            if returnLast && null remaining+              then do+                when debug . liftIO $ do+                  TIO.putStr "[racer] Reached last. Returning: "+                  print (asyncThreadId as)++                pure $ Just (as, result)+              else waitForOne+      (as, Left ex) -> markAsErrored as ex >> waitForOne
+ src/Network/Craze/Types.hs view
@@ -0,0 +1,110 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++module Network.Craze.Types where++import Control.Exception (SomeException)++import Data.ByteString    (ByteString)+import Data.Default.Class (Default, def)+import Data.Text          (Text)+import Network.Curl       (CurlOption, CurlResponse_, respBody)++-- | 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 functions like+-- 'raceGet'.+--+type RacerHandler headerTy bodyTy a = CurlResponse_ headerTy bodyTy -> IO a++-- | A function that computes whether or not a result is valid or not.+--+-- A racer will discard successful responses it get from its clients if they do+-- not pass the checker.+--+-- This step allows the racer to potentially discard responses that, while+-- technically successful, do not contain the expected result (e.g. APIs that+-- return errors as HTTP 200s, rate limitting messages, or unexpected formats).+--+type RacerChecker a = a -> Bool++-- | A provider is simply a factory function for 'ProviderOptions', which are+-- used to configure a client.+type RacerProvider = IO ProviderOptions++-- | Configuration used to set up an individual client in the race.+data ProviderOptions = ProviderOptions+  { -- | Options to pass down to Curl.+    poOptions :: [CurlOption]+    -- | Number of microseconds to delay the request by.+    --+    -- Delays can be used to give other clients a headstart. This is useful+    -- in cases were some clients are more costly to use than others (e.g.+    -- Bandwidth costs, resource usage, etc).+    --+  , poDelay   :: Maybe Int+    -- | A tag to identify this type provider.+    --+    -- Tags are not required to be unique, but they are generally more helpful+    -- if they are.+  , poTag     :: Text+  } deriving (Show)++instance Default ProviderOptions where+  def = ProviderOptions+    { poOptions = []+    , poDelay = Nothing+    , poTag = "default"+    }++-- | The status of running a single client.+data ClientStatus a+  -- | A successful response (passed the checker). A race will usually only+  -- have one successful response.+  = Successful a+  -- | A response that was received but failed to pass the checker.+  | Failed a+  -- | An exception thrown while using the client.+  | Errored SomeException+  -- | The operation is still pending, was cancelled, or was never started.+  | Pending+  deriving (Show)++-- | The result of a racing operation. This can be used to collect statistics+-- on which providers win more often, etc.+data RacerResult a = RacerResult+  { rrResponse  :: Maybe a+  , rrWinner    :: Maybe ProviderOptions+  , rrProviders :: [RacerProvider]+  , rrStatuses  :: [(Text, ClientStatus a)]+  }++-- | 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+  -- | When set to `True`, the Racer will attempt to return the last response+  -- in the event that all responses failed to pass the checker. This can be+  -- used for identifying error conditions.+  , racerReturnLast :: Bool+  }++instance Default (Racer [(String,String)] ByteString ByteString) where+  def = Racer+    { racerHandler = pure . respBody+    , racerChecker = const True+    , racerProviders = []+    , racerDebug = False+    , racerReturnLast = False+    }