packages feed

calamity 0.1.28.0 → 0.1.28.1

raw patch · 4 files changed

+270/−87 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Calamity.HTTP.Internal.Types: [$sel:rateLimits:RateLimitState] :: RateLimitState -> Map Route Lock
+ Calamity.HTTP.Internal.Types: Bucket :: Lock -> TVar BucketState -> Bucket
+ Calamity.HTTP.Internal.Types: BucketState :: UTCTime -> Maybe Int -> BucketState
+ Calamity.HTTP.Internal.Types: [$sel:bucketKeys:RateLimitState] :: RateLimitState -> Map Route ByteString
+ Calamity.HTTP.Internal.Types: [$sel:buckets:RateLimitState] :: RateLimitState -> Map ByteString Bucket
+ Calamity.HTTP.Internal.Types: [$sel:lock:Bucket] :: Bucket -> Lock
+ Calamity.HTTP.Internal.Types: [$sel:remaining:BucketState] :: BucketState -> Maybe Int
+ Calamity.HTTP.Internal.Types: [$sel:resetTime:BucketState] :: BucketState -> UTCTime
+ Calamity.HTTP.Internal.Types: [$sel:state:Bucket] :: Bucket -> TVar BucketState
+ Calamity.HTTP.Internal.Types: data Bucket
+ Calamity.HTTP.Internal.Types: data BucketState
+ Calamity.HTTP.Internal.Types: instance GHC.Generics.Generic Calamity.HTTP.Internal.Types.Bucket
+ Calamity.HTTP.Internal.Types: instance GHC.Generics.Generic Calamity.HTTP.Internal.Types.BucketState
- Calamity.Cache.Eff: delDM :: forall r_a9tvv. MemberWithError CacheEff r_a9tvv => Snowflake DMChannel -> Sem r_a9tvv ()
+ Calamity.Cache.Eff: delDM :: forall r_a9tFS. MemberWithError CacheEff r_a9tFS => Snowflake DMChannel -> Sem r_a9tFS ()
- Calamity.Cache.Eff: delGuild :: forall r_a9tvo. MemberWithError CacheEff r_a9tvo => Snowflake Guild -> Sem r_a9tvo ()
+ Calamity.Cache.Eff: delGuild :: forall r_a9tFL. MemberWithError CacheEff r_a9tFL => Snowflake Guild -> Sem r_a9tFL ()
- Calamity.Cache.Eff: delMessage :: forall r_a9tvQ. MemberWithError CacheEff r_a9tvQ => Snowflake Message -> Sem r_a9tvQ ()
+ Calamity.Cache.Eff: delMessage :: forall r_a9tGd. MemberWithError CacheEff r_a9tGd => Snowflake Message -> Sem r_a9tGd ()
- Calamity.Cache.Eff: delUnavailableGuild :: forall r_a9tvJ. MemberWithError CacheEff r_a9tvJ => Snowflake Guild -> Sem r_a9tvJ ()
+ Calamity.Cache.Eff: delUnavailableGuild :: forall r_a9tG6. MemberWithError CacheEff r_a9tG6 => Snowflake Guild -> Sem r_a9tG6 ()
- Calamity.Cache.Eff: delUser :: forall r_a9tvC. MemberWithError CacheEff r_a9tvC => Snowflake User -> Sem r_a9tvC ()
+ Calamity.Cache.Eff: delUser :: forall r_a9tFZ. MemberWithError CacheEff r_a9tFZ => Snowflake User -> Sem r_a9tFZ ()
- Calamity.Cache.Eff: getBotUser :: forall r_a9tvg. MemberWithError CacheEff r_a9tvg => Sem r_a9tvg (Maybe User)
+ Calamity.Cache.Eff: getBotUser :: forall r_a9tFD. MemberWithError CacheEff r_a9tFD => Sem r_a9tFD (Maybe User)
- Calamity.Cache.Eff: getDM :: forall r_a9tvs. MemberWithError CacheEff r_a9tvs => Snowflake DMChannel -> Sem r_a9tvs (Maybe DMChannel)
+ Calamity.Cache.Eff: getDM :: forall r_a9tFP. MemberWithError CacheEff r_a9tFP => Snowflake DMChannel -> Sem r_a9tFP (Maybe DMChannel)
- Calamity.Cache.Eff: getDMs :: forall r_a9tvu. MemberWithError CacheEff r_a9tvu => Sem r_a9tvu [DMChannel]
+ Calamity.Cache.Eff: getDMs :: forall r_a9tFR. MemberWithError CacheEff r_a9tFR => Sem r_a9tFR [DMChannel]
- Calamity.Cache.Eff: getGuild :: forall r_a9tvj. MemberWithError CacheEff r_a9tvj => Snowflake Guild -> Sem r_a9tvj (Maybe Guild)
+ Calamity.Cache.Eff: getGuild :: forall r_a9tFG. MemberWithError CacheEff r_a9tFG => Snowflake Guild -> Sem r_a9tFG (Maybe Guild)
- Calamity.Cache.Eff: getGuildChannel :: forall r_a9tvl. MemberWithError CacheEff r_a9tvl => Snowflake GuildChannel -> Sem r_a9tvl (Maybe GuildChannel)
+ Calamity.Cache.Eff: getGuildChannel :: forall r_a9tFI. MemberWithError CacheEff r_a9tFI => Snowflake GuildChannel -> Sem r_a9tFI (Maybe GuildChannel)
- Calamity.Cache.Eff: getGuilds :: forall r_a9tvn. MemberWithError CacheEff r_a9tvn => Sem r_a9tvn [Guild]
+ Calamity.Cache.Eff: getGuilds :: forall r_a9tFK. MemberWithError CacheEff r_a9tFK => Sem r_a9tFK [Guild]
- Calamity.Cache.Eff: getMessage :: forall r_a9tvN. MemberWithError CacheEff r_a9tvN => Snowflake Message -> Sem r_a9tvN (Maybe Message)
+ Calamity.Cache.Eff: getMessage :: forall r_a9tGa. MemberWithError CacheEff r_a9tGa => Snowflake Message -> Sem r_a9tGa (Maybe Message)
- Calamity.Cache.Eff: getMessages :: forall r_a9tvP. MemberWithError CacheEff r_a9tvP => Sem r_a9tvP [Message]
+ Calamity.Cache.Eff: getMessages :: forall r_a9tGc. MemberWithError CacheEff r_a9tGc => Sem r_a9tGc [Message]
- Calamity.Cache.Eff: getUnavailableGuilds :: forall r_a9tvI. MemberWithError CacheEff r_a9tvI => Sem r_a9tvI [Snowflake Guild]
+ Calamity.Cache.Eff: getUnavailableGuilds :: forall r_a9tG5. MemberWithError CacheEff r_a9tG5 => Sem r_a9tG5 [Snowflake Guild]
- Calamity.Cache.Eff: getUser :: forall r_a9tvz. MemberWithError CacheEff r_a9tvz => Snowflake User -> Sem r_a9tvz (Maybe User)
+ Calamity.Cache.Eff: getUser :: forall r_a9tFW. MemberWithError CacheEff r_a9tFW => Snowflake User -> Sem r_a9tFW (Maybe User)
- Calamity.Cache.Eff: getUsers :: forall r_a9tvB. MemberWithError CacheEff r_a9tvB => Sem r_a9tvB [User]
+ Calamity.Cache.Eff: getUsers :: forall r_a9tFY. MemberWithError CacheEff r_a9tFY => Sem r_a9tFY [User]
- Calamity.Cache.Eff: isUnavailableGuild :: forall r_a9tvG. MemberWithError CacheEff r_a9tvG => Snowflake Guild -> Sem r_a9tvG Bool
+ Calamity.Cache.Eff: isUnavailableGuild :: forall r_a9tG3. MemberWithError CacheEff r_a9tG3 => Snowflake Guild -> Sem r_a9tG3 Bool
- Calamity.Cache.Eff: setBotUser :: forall r_a9tve. MemberWithError CacheEff r_a9tve => User -> Sem r_a9tve ()
+ Calamity.Cache.Eff: setBotUser :: forall r_a9tFB. MemberWithError CacheEff r_a9tFB => User -> Sem r_a9tFB ()
- Calamity.Cache.Eff: setDM :: forall r_a9tvq. MemberWithError CacheEff r_a9tvq => DMChannel -> Sem r_a9tvq ()
+ Calamity.Cache.Eff: setDM :: forall r_a9tFN. MemberWithError CacheEff r_a9tFN => DMChannel -> Sem r_a9tFN ()
- Calamity.Cache.Eff: setGuild :: forall r_a9tvh. MemberWithError CacheEff r_a9tvh => Guild -> Sem r_a9tvh ()
+ Calamity.Cache.Eff: setGuild :: forall r_a9tFE. MemberWithError CacheEff r_a9tFE => Guild -> Sem r_a9tFE ()
- Calamity.Cache.Eff: setMessage :: forall r_a9tvL. MemberWithError CacheEff r_a9tvL => Message -> Sem r_a9tvL ()
+ Calamity.Cache.Eff: setMessage :: forall r_a9tG8. MemberWithError CacheEff r_a9tG8 => Message -> Sem r_a9tG8 ()
- Calamity.Cache.Eff: setUnavailableGuild :: forall r_a9tvE. MemberWithError CacheEff r_a9tvE => Snowflake Guild -> Sem r_a9tvE ()
+ Calamity.Cache.Eff: setUnavailableGuild :: forall r_a9tG1. MemberWithError CacheEff r_a9tG1 => Snowflake Guild -> Sem r_a9tG1 ()
- Calamity.Cache.Eff: setUser :: forall r_a9tvx. MemberWithError CacheEff r_a9tvx => User -> Sem r_a9tvx ()
+ Calamity.Cache.Eff: setUser :: forall r_a9tFU. MemberWithError CacheEff r_a9tFU => User -> Sem r_a9tFU ()
- Calamity.HTTP.Internal.Types: ExhaustedBucket :: ByteString -> Int -> DiscordResponseType
+ Calamity.HTTP.Internal.Types: ExhaustedBucket :: ByteString -> UTCTime -> BucketState -> ByteString -> DiscordResponseType
- Calamity.HTTP.Internal.Types: Good :: ByteString -> DiscordResponseType
+ Calamity.HTTP.Internal.Types: Good :: ByteString -> BucketState -> ByteString -> DiscordResponseType
- Calamity.HTTP.Internal.Types: RateLimitState :: Map Route Lock -> Event -> RateLimitState
+ Calamity.HTTP.Internal.Types: RateLimitState :: Map Route ByteString -> Map ByteString Bucket -> Event -> RateLimitState
- Calamity.HTTP.Internal.Types: Ratelimited :: Int -> Bool -> DiscordResponseType
+ Calamity.HTTP.Internal.Types: Ratelimited :: UTCTime -> Bool -> Maybe (BucketState, ByteString) -> DiscordResponseType

Files

Calamity/HTTP/Internal/Ratelimit.hs view
@@ -9,20 +9,19 @@ import Calamity.HTTP.Internal.Types import Calamity.Internal.Utils +import Control.Applicative import Control.Concurrent import Control.Concurrent.Event (Event) import qualified Control.Concurrent.Event as E import Control.Concurrent.STM-import Control.Concurrent.STM.Lock (Lock) import qualified Control.Concurrent.STM.Lock as L import Control.Lens import Control.Monad  import Data.Aeson import Data.Aeson.Lens-import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as LB-import Data.Functor+import qualified Data.ByteString          as B import Data.Maybe import qualified Data.Text.Lazy as LT import Data.Time@@ -30,10 +29,7 @@  import Fmt -import Focus- import Network.HTTP.Client (responseStatus)-import Network.HTTP.Date import Network.HTTP.Req import Network.HTTP.Types @@ -42,25 +38,108 @@ import qualified Polysemy.Async as P  import Prelude hiding (error)+import qualified Prelude  import qualified Control.Exception.Safe as Ex import qualified StmContainers.Map as SC  newRateLimitState :: IO RateLimitState-newRateLimitState = RateLimitState <$> SC.newIO <*> E.newSet+newRateLimitState = RateLimitState <$> SC.newIO <*> SC.newIO <*> E.newSet -lookupOrInsertDefaultM :: Monad m => m a -> Focus a m a-lookupOrInsertDefaultM aM =-  casesM-    ( do-        a <- aM-        pure (a, Set a)-    )-    (\a -> pure (a, Leave))+data Ratelimit+  = KnownRatelimit Bucket+  | UnknownRatelimit Route -getRateLimit :: RateLimitState -> Route -> STM Lock-getRateLimit s h = SC.focus (lookupOrInsertDefaultM L.new) h (rateLimits s)+{- performing rate limits+  1. try to fetch the lock from the ratelimiter state+  1.1. if we know the bucket then we retrieve the lock (KnownRatelimit)+  1.2. if we don't know the bucket, we just store the route (UnknownRatelimit)+  2.1. wait fot the global event (the global event is set unless we're globally ratelimited)+  2.2. wait for the local lock (locks are unlocked unless the bucket is exhausted)+  2.3. try to wait for the ratelimit to expire if it's exhausted+  3. we then perform the request, after this we'll know the bucket,+     so we update the ratelimit state with the bucket. if while performing+     the current request another request completed that made the bucket known,+     we don't try to overwrite it+  4.1. if we exceeded the local ratelimit we take the lock and release it after a delay+  4.2. if we exceeded the global ratelimit, we unset the global rl event,+       wait for it to expire, and then try again+-} +getRateLimit :: RateLimitState -> Route -> STM Ratelimit+getRateLimit s h = do+  bucketKey <- SC.lookup h $ bucketKeys s+  bucket <- join <$> sequenceA (flip SC.lookup (buckets s) <$> bucketKey)+  case bucket of+    Just bucket' ->+      pure $ KnownRatelimit bucket'+    Nothing ->+      pure $ UnknownRatelimit h++-- | Knowing the bucket for a route, and the ratelimit info, map the route to+-- the bucket key and retrieve the bucket+updateBucket :: RateLimitState -> Route -> B.ByteString -> BucketState -> STM Bucket+updateBucket s h b bucketState = do+  bucketKey <- SC.lookup h $ bucketKeys s+  case bucketKey of+    Just bucketKey' -> do+      -- if we know the bucket key here, then the bucket has already been made+      bucket <- SC.lookup bucketKey' $ buckets s+      case bucket of+        Just bucket' -> do+          modifyTVar' (bucket' ^. #state) (`mergeBucket` bucketState)+          pure bucket'+        Nothing -> Prelude.error "Not possible"+    Nothing -> do+      -- the bucket key wasn't known, make a new bucket and insert it+      lock <- L.new+      bs <- newTVar bucketState+      let bucket = Bucket lock bs+      SC.insert bucket b $ buckets s+      SC.insert b h $ bucketKeys s+      pure bucket+  where+    mergeBucket :: BucketState -> BucketState -> BucketState+    mergeBucket old new = new { remaining = new ^. #remaining <|> old ^. #remaining }++resetBucket :: Bucket -> STM ()+resetBucket bucket = modifyTVar' (bucket ^. #state) (#remaining .~ Nothing)++-- | Maybe wait for a bucket, updating its state to say we used it+useBucketOnce :: Bucket -> IO ()+useBucketOnce bucket = do+  now <- getCurrentTime+  mWaitUntil <- atomically $ do+    -- first wait on the lock+    L.wait $ bucket ^. #lock++    -- now try to estimate an expiry+    s <- readTVar $ bucket ^. #state++    let remaining = if now > s ^. #resetTime+          then Nothing+          else s ^. #remaining++    case remaining of+      Just n | n > 0 -> do+        modifyTVar (bucket ^. #state) (#remaining ?~ n - 1)+        pure Nothing+      Just _ -> do+        -- expired, lock and then reset the remaining+        L.acquire $ bucket ^. #lock+        modifyTVar (bucket ^. #state) (#remaining .~ Nothing)+        pure . Just $ s ^. #resetTime+      Nothing ->+        -- unknown, just assume we're good+        pure Nothing++  case mWaitUntil of+    Just when -> do+      threadDelayUntil when+      atomically . L.release $ bucket ^. #lock+    Nothing ->+      pure ()+ doDiscordRequest :: BotC r => IO LbsResponse -> Sem r DiscordResponseType doDiscordRequest r = do   r'' <- P.embed $ Ex.catchAny (Right <$> r) (pure . Left . Ex.displayException)@@ -71,18 +150,27 @@           | statusIsSuccessful status -> do             let resp = responseBody r'             debug $ "Got good response from discord: " +|| status ||+ ""-            pure $-              if isExhausted r'-                then case parseRateLimitHeader r' of-                  Just !sleepTime -> ExhaustedBucket resp sleepTime-                  Nothing -> ServerError (statusCode status)-                else Good resp+            now <- P.embed getCurrentTime+            if isExhausted r'+              then case (parseRateLimitHeader now r', buildBucketState now r') of+                (Just !when, Just (!bs, !key)) ->+                  pure $ ExhaustedBucket resp when bs key+                _ ->+                  pure $ ServerError (statusCode status)+              else case buildBucketState now r' of+                Just (!bs, !key) ->+                  pure $ Good resp bs key+                Nothing ->+                  pure $ ServerError (statusCode status)           | status == status429 -> do             debug "Got 429 from discord, retrying."+            now <- P.embed getCurrentTime             let resp = responseBody r'-            case resp ^? _Value of-              Just rv -> pure $ Ratelimited (parseRetryAfter rv) (isGlobal rv)-              Nothing -> pure $ ClientError (statusCode status) "429 with invalid json???"+            case (resp ^? _Value, buildBucketState now r') of+              (Just !rv, bs) ->+                pure $ Ratelimited (parseRetryAfter now rv) (isGlobal rv) bs+              _ ->+                pure $ ServerError (statusCode status)           | statusIsClientError status -> do             let err = responseBody r'             error $ "Something went wrong: " +|| err ||+ " response: " +|| r' ||+ ""@@ -94,26 +182,35 @@       error $ "Something went wrong with the http client: " +| LT.pack e |+ ""       pure . InternalResponseError $ LT.pack e -parseDiscordTime :: ByteString -> Maybe UTCTime-parseDiscordTime s = httpDateToUTC <$> parseHTTPDate s+-- | Parse a ratelimit header returning when it unlocks+parseRateLimitHeader :: HttpResponse r => UTCTime -> r -> Maybe UTCTime+parseRateLimitHeader now r = computedEnd <|> end+  where+    computedEnd :: Maybe UTCTime+    computedEnd = flip addUTCTime now <$> resetAfter -computeDiscordTimeDiff :: Double -> UTCTime -> Int-computeDiscordTimeDiff end !now = round . (* 1000.0) $! diffUTCTime end' now- where-  !end' = end & toRational & fromRational & posixSecondsToUTCTime+    resetAfter :: Maybe NominalDiffTime+    resetAfter = realToFrac <$> responseHeader r "X-Ratelimit-Reset-After" ^? _Just . _Double --- | Parse a ratelimit header returning the number of milliseconds until it resets-parseRateLimitHeader :: HttpResponse r => r -> Maybe Int-parseRateLimitHeader r = computeDiscordTimeDiff <$> end <*> now- where-  end = responseHeader r "X-Ratelimit-Reset" ^? _Just . _Double-  now = parseDiscordTime =<< responseHeader r "Date"+    end :: Maybe UTCTime+    end = posixSecondsToUTCTime . realToFrac <$>+      responseHeader r "X-Ratelimit-Reset" ^? _Just . _Double +buildBucketState :: HttpResponse r => UTCTime -> r -> Maybe (BucketState, B.ByteString)+buildBucketState now r = (bs,) <$> bucketKey+  where+    remaining = responseHeader r "X-RateLimit-Remaining" ^? _Just . _Integral+    bs = BucketState now remaining+    bucketKey = responseHeader r "X-RateLimit-Bucket"+ isExhausted :: HttpResponse r => r -> Bool isExhausted r = responseHeader r "X-RateLimit-Remaining" == Just "0" -parseRetryAfter :: Value -> Int-parseRetryAfter r = r ^?! key "retry_after" . _Integral+-- | Parse the retry after field, returning when to retry+parseRetryAfter :: UTCTime -> Value -> UTCTime+parseRetryAfter now r = addUTCTime retryAfter now+  where+    retryAfter = realToFrac $ r ^?! key "retry_after" . _Double  isGlobal :: Value -> Bool isGlobal r = r ^? key "global" . _Bool == Just True@@ -130,80 +227,126 @@   Int ->   -- | action to perform   Sem r (ShouldRetry a b) ->-  -- | action to run if max number of retries was reached-  Sem r () ->   Sem r (Either a b)-retryRequest max_retries action failAction = retryInner 0+retryRequest maxRetries action = retryInner 0  where-  retryInner num_retries = do+  retryInner numRetries = do     res <- action     case res of-      Retry r | num_retries > max_retries -> do-        debug $ "Request failed after " +| max_retries |+ " retries."-        doFail $ Left r-      Retry _ -> retryInner (num_retries + 1)+      Retry r | numRetries > maxRetries -> do+        debug $ "Request failed after " +| maxRetries |+ " retries."+        pure $ Left r+      Retry _ ->+        retryInner (numRetries + 1)       RFail r -> do         debug "Request failed due to error response."-        doFail $ Left r-      RGood r -> pure $ Right r-   where-    doFail v = failAction $> v+        pure $ Left r+      RGood r ->+        pure $ Right r +threadDelayMS :: Int -> IO ()+threadDelayMS ms = threadDelay (1000 * ms)++threadDelayUntil :: UTCTime -> IO ()+threadDelayUntil when = do+  now <- getCurrentTime+  let msUntil = round . (* 1000) . realToFrac @_ @Double $ diffUTCTime when now+  threadDelayMS msUntil+ -- Run a single request--- NOTE: this function will only unlock the ratelimit lock if the request--- gave a response, otherwise it will stay locked so that it can be retried again doSingleRequest ::   BotC r =>+  RateLimitState ->+  Route ->   -- | Global lock   Event ->-  -- | Local lock-  Lock ->   -- | Request action   IO LbsResponse ->   Sem r (ShouldRetry RestError LB.ByteString)-doSingleRequest gl l r = do+doSingleRequest rlstate route gl r = do+  P.embed $ E.wait (globalLock rlstate)++  rl <- P.embed . atomically $ getRateLimit rlstate route++  case rl of+    KnownRatelimit bucket ->+      P.embed $ useBucketOnce bucket++    _ -> pure ()+   r' <- doDiscordRequest r+   case r' of-    Good v -> do-      P.embed . atomically $ L.release l+    Good v bs bk -> do+      void . P.embed . atomically $ updateBucket rlstate route bk bs       pure $ RGood v-    ExhaustedBucket v d -> do-      debug $ "Exhausted bucket, unlocking after " +| d |+ "ms"++    ExhaustedBucket v unlockWhen bs bk -> do+      debug $ "Exhausted bucket, unlocking at" +| unlockWhen |+ ""++      bucket <- P.embed . atomically $ do+        bucket <- updateBucket rlstate route bk bs+        L.acquire $ bucket ^. #lock+        pure bucket+       void . P.async $ do         P.embed $ do-          threadDelay $ 1000 * d-          atomically $ L.release l+          threadDelayUntil unlockWhen+          atomically $ do+            L.release $ bucket ^. #lock+            resetBucket bucket         debug "unlocking bucket"+       pure $ RGood v-    Ratelimited d False -> do-      debug $ "429 ratelimited on route, sleeping for " +| d |+ " ms"-      P.embed . threadDelay $ 1000 * d++    Ratelimited unlockWhen False (Just (bs, bk)) -> do+      debug $ "429 ratelimited on route, retrying at " +| unlockWhen |+ ""++      bucket <- P.embed . atomically $ do+        bucket <- updateBucket rlstate route bk bs+        L.acquire $ bucket ^. #lock+        pure bucket++      P.embed $ do+        threadDelayUntil unlockWhen+        atomically $ do+          L.release $ bucket ^. #lock+          resetBucket bucket+       pure $ Retry (HTTPError 429 Nothing)-    Ratelimited d True -> do++    Ratelimited unlockWhen False _ -> do+      debug "Internal error (ratelimited but no headers), retrying"+      P.embed $ threadDelayUntil unlockWhen+      pure $ Retry (HTTPError 429 Nothing)++    Ratelimited unlockWhen True bs -> do       debug "429 ratelimited globally"+       P.embed $ do+        case bs of+          Just (bs', bk) ->+            void . atomically $ updateBucket rlstate route bk bs'+          Nothing ->+            pure ()+         E.clear gl-        threadDelay $ 1000 * d+        threadDelayUntil unlockWhen         E.set gl       pure $ Retry (HTTPError 429 Nothing)+     ServerError c -> do       debug "Server failed, retrying"       pure $ Retry (HTTPError c Nothing)+     InternalResponseError c -> do       debug "Internal error, retrying"       pure $ Retry (InternalClientError c)+     ClientError c v -> pure $ RFail (HTTPError c $ decode v)  doRequest :: BotC r => RateLimitState -> Route -> IO LbsResponse -> Sem r (Either RestError LB.ByteString)-doRequest rlState route action = do-  P.embed $ E.wait (globalLock rlState)--  ratelimit <- P.embed . atomically $ do-    lock <- getRateLimit rlState route-    L.acquire lock-    pure lock-+doRequest rlstate route action =   retryRequest     5-    (doSingleRequest (globalLock rlState) ratelimit action)-    (P.embed . atomically $ L.release ratelimit)+    (doSingleRequest rlstate route (globalLock rlstate) action)
Calamity/HTTP/Internal/Types.hs view
@@ -3,6 +3,8 @@     ( RestError(..)     , RateLimitState(..)     , DiscordResponseType(..)+    , Bucket(..)+    , BucketState(..)     , GatewayResponse     , BotGatewayResponse ) where @@ -11,9 +13,12 @@  import           Control.Concurrent.Event      ( Event ) import           Control.Concurrent.STM.Lock   ( Lock )+import           Control.Concurrent.STM.TVar   ( TVar ) +import Data.Time import           Data.Aeson import qualified Data.ByteString.Lazy          as LB+import qualified Data.ByteString          as B import           Data.Text.Lazy  import           GHC.Generics@@ -30,19 +35,49 @@   | InternalClientError Text   deriving ( Show, Generic ) +data BucketState = BucketState+  { resetTime :: UTCTime+    -- ^ The time when the bucket resets, used to heuristically wait out ratelimits+  , remaining  :: Maybe Int+    -- ^ The number of uses left in the bucket, used to heuristically wait out ratelimits+  }+  deriving ( Generic )++data Bucket = Bucket+  { lock :: Lock+  , state :: TVar BucketState+  }+  deriving ( Generic )+ data RateLimitState = RateLimitState-  { rateLimits :: SC.Map Route Lock+  { bucketKeys :: SC.Map Route B.ByteString+  , buckets    :: SC.Map B.ByteString Bucket   , globalLock :: Event   }   deriving ( Generic )  data DiscordResponseType-  = Good LB.ByteString -- ^ A good response-  | ExhaustedBucket -- ^ We got a response but also exhausted the bucket-      LB.ByteString Int -- ^ Retry after (milliseconds)-  | Ratelimited -- ^ We hit a 429, no response and ratelimited-      Int -- ^ Retry after (milliseconds)-      Bool -- ^ Global ratelimit+  = Good+    -- ^ A good response+    LB.ByteString+    BucketState+    B.ByteString+    -- ^ The bucket the route is in+  | ExhaustedBucket+    -- ^ We got a response but also exhausted the bucket+    LB.ByteString+    UTCTime+    -- ^ Retry after+    BucketState+    B.ByteString+    -- ^ The bucket the route is in+  | Ratelimited+    -- ^ We hit a 429, no response and ratelimited+    UTCTime+    -- ^ Retry after+    Bool+    -- ^ Global ratelimit+    (Maybe (BucketState, B.ByteString))   | ServerError Int -- ^ Discord's error, we should retry (HTTP 5XX)   | ClientError Int LB.ByteString -- ^ Our error, we should fail   | InternalResponseError Text -- ^ Something went wrong with the http client
ChangeLog.md view
@@ -1,5 +1,10 @@ # Changelog for Calamity +## 0.1.28.1+++ Rework the ratelimit implementation to use X-Ratelimit-Bucket++ Fix incorrect interpretation of the retry-after for 429s+ ## 0.1.28.0  + Added support for message types 19 (reply) and 20 (application command)
calamity.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 3c5b52b22636e00ca4b9cc43f1349fce19deee44cd599e47312642322256f9ab+-- hash: 2482ee40637f3fe615af0564b33ebb2dcbb4e67d1ec98d152f59206b977c3686  name:           calamity-version:        0.1.28.0+version:        0.1.28.1 synopsis:       A library for writing discord bots in haskell description:    Please see the README on GitHub at <https://github.com/simmsb/calamity#readme> category:       Network, Web