1
0
Fork 0

[haskell] Prevent que networking code from triggering the API rate limiting

This commit is contained in:
Julien Dessaux 2023-07-22 00:10:02 +02:00
parent dcd0a7a9b2
commit d1f0faf30f
Signed by: adyxax
GPG key ID: F92E51B86E07177E
4 changed files with 39 additions and 8 deletions

View file

@ -27,6 +27,7 @@ dependencies:
- sqlite-simple - sqlite-simple
- text - text
- time - time
- unbounded-delays
- unix - unix
ghc-options: ghc-options:

View file

@ -1,28 +1,46 @@
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
module SpaceTraders module SpaceTraders
( SpaceTradersT ( SpaceTradersT
, microSinceEpoch
, newEnv
, runSpaceTradersT , runSpaceTradersT
, Env(..) , Env(..)
, HasDatabaseConn , HasDatabaseConn
, HasRequest , HasRequest
, ask , ask
, getConn , getConn
, getLastAPICall
, getRequest , getRequest
, liftIO , liftIO
) where ) where
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.Reader import Control.Monad.Reader
import Data.IORef
import Data.Time.Clock
import Data.Time.Clock.POSIX
import qualified Database.SQLite.Simple as S import qualified Database.SQLite.Simple as S
import Network.HTTP.Simple import Network.HTTP.Simple
type SpaceTradersT a = ReaderT Env IO a type SpaceTradersT a = ReaderT Env IO a
newEnv :: S.Connection -> Request -> IO Env
newEnv conn req = do
r <- newIORef 0
return $ Env conn r req
runSpaceTradersT :: SpaceTradersT a -> Env -> IO a runSpaceTradersT :: SpaceTradersT a -> Env -> IO a
runSpaceTradersT = runReaderT runSpaceTradersT = runReaderT
microSinceEpoch :: IO Integer
microSinceEpoch = do
t <- getCurrentTime
return $ floor . (1e6 *) . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds t
data Env = Env { envConn :: S.Connection data Env = Env { envConn :: S.Connection
, envLastAPICall :: IORef Integer
, envRequest :: Request } , envRequest :: Request }
class HasDatabaseConn a where class HasDatabaseConn a where
@ -33,6 +51,8 @@ instance HasDatabaseConn Env where
getConn = envConn getConn = envConn
class HasRequest a where class HasRequest a where
getLastAPICall :: a -> IORef Integer
getRequest :: a -> Request getRequest :: a -> Request
instance HasRequest Env where instance HasRequest Env where
getLastAPICall = envLastAPICall
getRequest = envRequest getRequest = envRequest

View file

@ -11,9 +11,11 @@ module SpaceTraders.APIClient.Client
, tokenReq , tokenReq
) where ) where
import Control.Concurrent import Control.Concurrent.Thread.Delay
import Control.Monad
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson import Data.Aeson
import Data.IORef
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import Network.HTTP.Simple import Network.HTTP.Simple
@ -62,9 +64,17 @@ sendPaginated pagination requestBuilder = do
Nothing -> request Nothing -> request
sendPaginated' request' sendPaginated' request'
where where
sendPaginated' :: (FromJSON a, MonadIO m) => Request -> m (APIPaginatedResponse a) sendPaginated' :: (FromJSON a, HasRequest env, MonadIO m, MonadReader env m) => Request -> m (APIPaginatedResponse a)
sendPaginated' request = do sendPaginated' request = do
response <- liftIO $ httpLbs request env <- ask
let iref = getLastAPICall env
response <- liftIO $ do
now <- microSinceEpoch
prev <- readIORef iref
let diff = prev + 400_000 - now
when (diff > 0) (delay diff)
writeIORef iref now
httpLbs request
let status = statusCode $ getResponseStatus response let status = statusCode $ getResponseStatus response
body = getResponseBody response body = getResponseBody response
if status >= 200 && status <= 299 if status >= 200 && status <= 299
@ -74,6 +84,6 @@ sendPaginated pagination requestBuilder = do
else case eitherDecode body of else case eitherDecode body of
Left e -> return . Left $ APIError (-status) (T.pack $ concat ["Error decoding JSON APIError: ", e, ". Got HTTP body: ", show body]) Null Left e -> return . Left $ APIError (-status) (T.pack $ concat ["Error decoding JSON APIError: ", e, ". Got HTTP body: ", show body]) Null
Right (APIRateLimit r) -> do Right (APIRateLimit r) -> do
liftIO $ threadDelay (1_000_000 * (round $ retryAfter r)) liftIO $ delay (1_000_000 * (round $ retryAfter r))
sendPaginated' request sendPaginated' request
Right e -> return $ Left e Right e -> return $ Left e

View file

@ -30,7 +30,7 @@ initST :: IO Env
initST = do initST = do
conn <- open conn <- open
t <- runReaderT getToken conn `catch` handleNoToken conn t <- runReaderT getToken conn `catch` handleNoToken conn
let env = Env conn (tokenReq t) env <- newEnv conn (tokenReq t)
ma <- runReaderT getAgent conn -- We compare the agent state in the database ma <- runReaderT getAgent conn -- We compare the agent state in the database
ma' <- runSpaceTradersT myAgent env -- with the one on the servers ma' <- runSpaceTradersT myAgent env -- with the one on the servers
case ma' of case ma' of
@ -44,7 +44,7 @@ initST = do
return $ env return $ env
where where
handleNoToken :: S.Connection -> SomeException -> IO T.Text handleNoToken :: S.Connection -> SomeException -> IO T.Text
handleNoToken conn _ = runReaderT registerST (Env conn defaultReq) handleNoToken conn _ = newEnv conn defaultReq >>= runReaderT registerST
registerST :: SpaceTradersT (T.Text) registerST :: SpaceTradersT (T.Text)
registerST = do registerST = do
@ -64,5 +64,5 @@ wipe c = do
close c close c
removeFile "spacetraders.db" removeFile "spacetraders.db"
conn' <- open conn' <- open
t <- runReaderT registerST (Env conn' defaultReq) t <- newEnv conn' defaultReq >>= runReaderT registerST
return $ Env conn' (tokenReq t) newEnv conn' (tokenReq t)