packages feed

calamity 0.1.28.1 → 0.1.28.2

raw patch · 5 files changed

+208/−141 lines, 5 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Calamity.HTTP.Internal.Types: ExhaustedBucket :: ByteString -> UTCTime -> BucketState -> ByteString -> DiscordResponseType
+ Calamity.HTTP.Internal.Ratelimit: instance GHC.Show.Show Calamity.HTTP.Internal.Ratelimit.WaitDelay
+ Calamity.HTTP.Internal.Types: [$sel:limit:BucketState] :: BucketState -> Int
+ Calamity.HTTP.Internal.Types: [$sel:ongoing:BucketState] :: BucketState -> Int
+ Calamity.HTTP.Internal.Types: [$sel:resetKey:BucketState] :: BucketState -> Int
+ Calamity.HTTP.Internal.Types: instance GHC.Show.Show Calamity.HTTP.Internal.Types.BucketState
- Calamity.Cache.Eff: delDM :: forall r_a9tFS. MemberWithError CacheEff r_a9tFS => Snowflake DMChannel -> Sem r_a9tFS ()
+ Calamity.Cache.Eff: delDM :: forall r_a9tNM. MemberWithError CacheEff r_a9tNM => Snowflake DMChannel -> Sem r_a9tNM ()
- Calamity.Cache.Eff: delGuild :: forall r_a9tFL. MemberWithError CacheEff r_a9tFL => Snowflake Guild -> Sem r_a9tFL ()
+ Calamity.Cache.Eff: delGuild :: forall r_a9tNF. MemberWithError CacheEff r_a9tNF => Snowflake Guild -> Sem r_a9tNF ()
- Calamity.Cache.Eff: delMessage :: forall r_a9tGd. MemberWithError CacheEff r_a9tGd => Snowflake Message -> Sem r_a9tGd ()
+ Calamity.Cache.Eff: delMessage :: forall r_a9tO7. MemberWithError CacheEff r_a9tO7 => Snowflake Message -> Sem r_a9tO7 ()
- Calamity.Cache.Eff: delUnavailableGuild :: forall r_a9tG6. MemberWithError CacheEff r_a9tG6 => Snowflake Guild -> Sem r_a9tG6 ()
+ Calamity.Cache.Eff: delUnavailableGuild :: forall r_a9tO0. MemberWithError CacheEff r_a9tO0 => Snowflake Guild -> Sem r_a9tO0 ()
- Calamity.Cache.Eff: delUser :: forall r_a9tFZ. MemberWithError CacheEff r_a9tFZ => Snowflake User -> Sem r_a9tFZ ()
+ Calamity.Cache.Eff: delUser :: forall r_a9tNT. MemberWithError CacheEff r_a9tNT => Snowflake User -> Sem r_a9tNT ()
- Calamity.Cache.Eff: getBotUser :: forall r_a9tFD. MemberWithError CacheEff r_a9tFD => Sem r_a9tFD (Maybe User)
+ Calamity.Cache.Eff: getBotUser :: forall r_a9tNx. MemberWithError CacheEff r_a9tNx => Sem r_a9tNx (Maybe User)
- Calamity.Cache.Eff: getDM :: forall r_a9tFP. MemberWithError CacheEff r_a9tFP => Snowflake DMChannel -> Sem r_a9tFP (Maybe DMChannel)
+ Calamity.Cache.Eff: getDM :: forall r_a9tNJ. MemberWithError CacheEff r_a9tNJ => Snowflake DMChannel -> Sem r_a9tNJ (Maybe DMChannel)
- Calamity.Cache.Eff: getDMs :: forall r_a9tFR. MemberWithError CacheEff r_a9tFR => Sem r_a9tFR [DMChannel]
+ Calamity.Cache.Eff: getDMs :: forall r_a9tNL. MemberWithError CacheEff r_a9tNL => Sem r_a9tNL [DMChannel]
- Calamity.Cache.Eff: getGuild :: forall r_a9tFG. MemberWithError CacheEff r_a9tFG => Snowflake Guild -> Sem r_a9tFG (Maybe Guild)
+ Calamity.Cache.Eff: getGuild :: forall r_a9tNA. MemberWithError CacheEff r_a9tNA => Snowflake Guild -> Sem r_a9tNA (Maybe Guild)
- Calamity.Cache.Eff: getGuildChannel :: forall r_a9tFI. MemberWithError CacheEff r_a9tFI => Snowflake GuildChannel -> Sem r_a9tFI (Maybe GuildChannel)
+ Calamity.Cache.Eff: getGuildChannel :: forall r_a9tNC. MemberWithError CacheEff r_a9tNC => Snowflake GuildChannel -> Sem r_a9tNC (Maybe GuildChannel)
- Calamity.Cache.Eff: getGuilds :: forall r_a9tFK. MemberWithError CacheEff r_a9tFK => Sem r_a9tFK [Guild]
+ Calamity.Cache.Eff: getGuilds :: forall r_a9tNE. MemberWithError CacheEff r_a9tNE => Sem r_a9tNE [Guild]
- Calamity.Cache.Eff: getMessage :: forall r_a9tGa. MemberWithError CacheEff r_a9tGa => Snowflake Message -> Sem r_a9tGa (Maybe Message)
+ Calamity.Cache.Eff: getMessage :: forall r_a9tO4. MemberWithError CacheEff r_a9tO4 => Snowflake Message -> Sem r_a9tO4 (Maybe Message)
- Calamity.Cache.Eff: getMessages :: forall r_a9tGc. MemberWithError CacheEff r_a9tGc => Sem r_a9tGc [Message]
+ Calamity.Cache.Eff: getMessages :: forall r_a9tO6. MemberWithError CacheEff r_a9tO6 => Sem r_a9tO6 [Message]
- Calamity.Cache.Eff: getUnavailableGuilds :: forall r_a9tG5. MemberWithError CacheEff r_a9tG5 => Sem r_a9tG5 [Snowflake Guild]
+ Calamity.Cache.Eff: getUnavailableGuilds :: forall r_a9tNZ. MemberWithError CacheEff r_a9tNZ => Sem r_a9tNZ [Snowflake Guild]
- Calamity.Cache.Eff: getUser :: forall r_a9tFW. MemberWithError CacheEff r_a9tFW => Snowflake User -> Sem r_a9tFW (Maybe User)
+ Calamity.Cache.Eff: getUser :: forall r_a9tNQ. MemberWithError CacheEff r_a9tNQ => Snowflake User -> Sem r_a9tNQ (Maybe User)
- Calamity.Cache.Eff: getUsers :: forall r_a9tFY. MemberWithError CacheEff r_a9tFY => Sem r_a9tFY [User]
+ Calamity.Cache.Eff: getUsers :: forall r_a9tNS. MemberWithError CacheEff r_a9tNS => Sem r_a9tNS [User]
- Calamity.Cache.Eff: isUnavailableGuild :: forall r_a9tG3. MemberWithError CacheEff r_a9tG3 => Snowflake Guild -> Sem r_a9tG3 Bool
+ Calamity.Cache.Eff: isUnavailableGuild :: forall r_a9tNX. MemberWithError CacheEff r_a9tNX => Snowflake Guild -> Sem r_a9tNX Bool
- Calamity.Cache.Eff: setBotUser :: forall r_a9tFB. MemberWithError CacheEff r_a9tFB => User -> Sem r_a9tFB ()
+ Calamity.Cache.Eff: setBotUser :: forall r_a9tNv. MemberWithError CacheEff r_a9tNv => User -> Sem r_a9tNv ()
- Calamity.Cache.Eff: setDM :: forall r_a9tFN. MemberWithError CacheEff r_a9tFN => DMChannel -> Sem r_a9tFN ()
+ Calamity.Cache.Eff: setDM :: forall r_a9tNH. MemberWithError CacheEff r_a9tNH => DMChannel -> Sem r_a9tNH ()
- Calamity.Cache.Eff: setGuild :: forall r_a9tFE. MemberWithError CacheEff r_a9tFE => Guild -> Sem r_a9tFE ()
+ Calamity.Cache.Eff: setGuild :: forall r_a9tNy. MemberWithError CacheEff r_a9tNy => Guild -> Sem r_a9tNy ()
- Calamity.Cache.Eff: setMessage :: forall r_a9tG8. MemberWithError CacheEff r_a9tG8 => Message -> Sem r_a9tG8 ()
+ Calamity.Cache.Eff: setMessage :: forall r_a9tO2. MemberWithError CacheEff r_a9tO2 => Message -> Sem r_a9tO2 ()
- Calamity.Cache.Eff: setUnavailableGuild :: forall r_a9tG1. MemberWithError CacheEff r_a9tG1 => Snowflake Guild -> Sem r_a9tG1 ()
+ Calamity.Cache.Eff: setUnavailableGuild :: forall r_a9tNV. MemberWithError CacheEff r_a9tNV => Snowflake Guild -> Sem r_a9tNV ()
- Calamity.Cache.Eff: setUser :: forall r_a9tFU. MemberWithError CacheEff r_a9tFU => User -> Sem r_a9tFU ()
+ Calamity.Cache.Eff: setUser :: forall r_a9tNO. MemberWithError CacheEff r_a9tNO => User -> Sem r_a9tNO ()
- Calamity.Commands.ParsePrefix: parsePrefix :: forall r_a4Vif. MemberWithError ParsePrefix r_a4Vif => Message -> Sem r_a4Vif (Maybe (Text, Text))
+ Calamity.Commands.ParsePrefix: parsePrefix :: forall r_a4VkQ. MemberWithError ParsePrefix r_a4VkQ => Message -> Sem r_a4VkQ (Maybe (Text, Text))
- Calamity.HTTP.Internal.Types: BucketState :: UTCTime -> Maybe Int -> BucketState
+ Calamity.HTTP.Internal.Types: BucketState :: Maybe UTCTime -> Int -> Int -> Int -> Int -> BucketState
- Calamity.HTTP.Internal.Types: [$sel:remaining:BucketState] :: BucketState -> Maybe Int
+ Calamity.HTTP.Internal.Types: [$sel:remaining:BucketState] :: BucketState -> Int
- Calamity.HTTP.Internal.Types: [$sel:resetTime:BucketState] :: BucketState -> UTCTime
+ Calamity.HTTP.Internal.Types: [$sel:resetTime:BucketState] :: BucketState -> Maybe UTCTime
- Calamity.Internal.LocalWriter: llisten :: forall o_arRe r_arYL a_XrRe. MemberWithError (LocalWriter o_arRe) r_arYL => Sem r_arYL a_XrRe -> Sem r_arYL (o_arRe, a_XrRe)
+ Calamity.Internal.LocalWriter: llisten :: forall o_arTP r_as1m a_XrTP. MemberWithError (LocalWriter o_arTP) r_as1m => Sem r_as1m a_XrTP -> Sem r_as1m (o_arTP, a_XrTP)
- Calamity.Internal.LocalWriter: ltell :: forall o_arRa r_arYJ. MemberWithError (LocalWriter o_arRa) r_arYJ => o_arRa -> Sem r_arYJ ()
+ Calamity.Internal.LocalWriter: ltell :: forall o_arTL r_as1k. MemberWithError (LocalWriter o_arTL) r_as1k => o_arTL -> Sem r_as1k ()
- Calamity.Metrics.Eff: addCounter :: forall r_avva. MemberWithError MetricEff r_avva => Int -> Counter -> Sem r_avva Int
+ Calamity.Metrics.Eff: addCounter :: forall r_avxL. MemberWithError MetricEff r_avxL => Int -> Counter -> Sem r_avxL Int
- Calamity.Metrics.Eff: modifyGauge :: forall r_avvd. MemberWithError MetricEff r_avvd => (Double -> Double) -> Gauge -> Sem r_avvd Double
+ Calamity.Metrics.Eff: modifyGauge :: forall r_avxO. MemberWithError MetricEff r_avxO => (Double -> Double) -> Gauge -> Sem r_avxO Double
- Calamity.Metrics.Eff: observeHistogram :: forall r_avvg. MemberWithError MetricEff r_avvg => Double -> Histogram -> Sem r_avvg HistogramSample
+ Calamity.Metrics.Eff: observeHistogram :: forall r_avxR. MemberWithError MetricEff r_avxR => Double -> Histogram -> Sem r_avxR HistogramSample
- Calamity.Metrics.Eff: registerCounter :: forall r_avv0. MemberWithError MetricEff r_avv0 => Text -> [(Text, Text)] -> Sem r_avv0 Counter
+ Calamity.Metrics.Eff: registerCounter :: forall r_avxB. MemberWithError MetricEff r_avxB => Text -> [(Text, Text)] -> Sem r_avxB Counter
- Calamity.Metrics.Eff: registerGauge :: forall r_avv3. MemberWithError MetricEff r_avv3 => Text -> [(Text, Text)] -> Sem r_avv3 Gauge
+ Calamity.Metrics.Eff: registerGauge :: forall r_avxE. MemberWithError MetricEff r_avxE => Text -> [(Text, Text)] -> Sem r_avxE Gauge
- Calamity.Metrics.Eff: registerHistogram :: forall r_avv6. MemberWithError MetricEff r_avv6 => Text -> [(Text, Text)] -> [Double] -> Sem r_avv6 Histogram
+ Calamity.Metrics.Eff: registerHistogram :: forall r_avxH. MemberWithError MetricEff r_avxH => Text -> [(Text, Text)] -> [Double] -> Sem r_avxH Histogram

Files

Calamity/HTTP/Channel.hs view
@@ -204,6 +204,10 @@   mkRouteBuilder // S "channels" // ID @Channel     & giveID id +renderEmoji :: RawEmoji -> Text+renderEmoji (UnicodeEmoji e) = e ^. strict+renderEmoji (CustomEmoji e) = e ^. #name . strict <> ":" <> showt (e ^. #id)+ instance Request (ChannelRequest a) where   type Result (ChannelRequest a) = a @@ -231,20 +235,20 @@       & giveID mid       & buildRoute   route (CreateReaction (getID -> cid) (getID @Message -> mid) emoji) =-    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (showt emoji) // S "@me"+    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (renderEmoji emoji) // S "@me"       & giveID mid       & buildRoute   route (DeleteOwnReaction (getID -> cid) (getID @Message -> mid) emoji) =-    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (showt emoji) // S "@me"+    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (renderEmoji emoji) // S "@me"       & giveID mid       & buildRoute   route (DeleteUserReaction (getID -> cid) (getID @Message -> mid) emoji (getID @User -> uid)) =-    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (showt emoji) // ID @User+    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (renderEmoji emoji) // ID @User       & giveID mid       & giveID uid       & buildRoute   route (GetReactions (getID -> cid) (getID @Message -> mid) emoji _) =-    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (showt emoji)+    baseRoute cid // S "messages" // ID @Message // S "reactions" // S (renderEmoji emoji)       & giveID mid       & buildRoute   route (DeleteAllReactions (getID -> cid) (getID @Message -> mid)) =
Calamity/HTTP/Internal/Ratelimit.hs view
@@ -20,8 +20,8 @@  import Data.Aeson import Data.Aeson.Lens+import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as LB-import qualified Data.ByteString          as B import Data.Maybe import qualified Data.Text.Lazy as LT import Data.Time@@ -35,7 +35,6 @@  import Polysemy (Sem) import qualified Polysemy as P-import qualified Polysemy.Async as P  import Prelude hiding (error) import qualified Prelude@@ -50,22 +49,6 @@   = KnownRatelimit Bucket   | UnknownRatelimit Route -{- 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@@ -76,18 +59,20 @@     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+{- | 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+      -- if the given bucket key is different than the known bucket key then oops       bucket <- SC.lookup bucketKey' $ buckets s       case bucket of         Just bucket' -> do-          modifyTVar' (bucket' ^. #state) (`mergeBucket` bucketState)+          modifyTVar' (bucket' ^. #state) (`mergeStates` bucketState)           pure bucket'         Nothing -> Prelude.error "Not possible"     Nothing -> do@@ -98,48 +83,123 @@       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 }+ where+  mergeStates :: BucketState -> BucketState -> BucketState+  mergeStates old new =+    new+      { ongoing = old ^. #ongoing+      , -- we only ignore the previous 'remaining' if we've not reset yet and the+        -- reset time has changed+        remaining =+          if (isJust $ old ^. #resetTime) && (old ^. #resetKey /= new ^. #resetKey)+            then min (old ^. #remaining) (new ^. #remaining)+            else new ^. #remaining+      , -- only take the new resetTime if it actually changed+        resetTime =+          if old ^. #resetKey /= new ^. #resetKey+            then new ^. #resetTime+            else old ^. #resetTime+      }  resetBucket :: Bucket -> STM ()-resetBucket bucket = modifyTVar' (bucket ^. #state) (#remaining .~ Nothing)+resetBucket bucket =+  modifyTVar'+    (bucket ^. #state)+    ( \bs ->+        bs & #remaining .~ bs ^. #limit+          & #resetTime .~ Nothing+    ) +canResetBucketNow :: UTCTime -> BucketState -> Bool+canResetBucketNow _ BucketState{ongoing} | ongoing > 0 = False+-- don't allow resetting the bucket if there's ongoing requests, we'll wait+-- until another request finishes and updates the counter+canResetBucketNow now bs = case bs ^. #resetTime of+  Just rt -> now > rt+  Nothing -> False++-- canResetBucket :: BucketState -> Bool+-- canResetBucket bs = isNothing $ bs ^. #startedWaitingTime++shouldWaitForUnlock :: BucketState -> Bool+shouldWaitForUnlock BucketState{remaining = 0, ongoing} = ongoing > 0+shouldWaitForUnlock _ = False++data WaitDelay+  = WaitUntil UTCTime+  | WaitRetrySoon+  | GoNow+  deriving (Show)++intoWaitDelay :: Maybe UTCTime -> WaitDelay+intoWaitDelay (Just t) = WaitUntil t+intoWaitDelay Nothing = WaitRetrySoon+ -- | 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+useBucketOnce bucket = go 0+ where+  go :: Int -> IO ()+  go tries = do+    now <- getCurrentTime+    mWaitDelay <- atomically $ do+      s <- readTVar $ bucket ^. #state -    -- now try to estimate an expiry-    s <- readTVar $ bucket ^. #state+      -- -- [0]+      -- -- if there are ongoing requests, wait for them to finish and deliver+      -- -- truth on the current ratelimit state+      when+        (shouldWaitForUnlock s)+        retry -    let remaining = if now > s ^. #resetTime-          then Nothing-          else s ^. #remaining+      -- if there are no ongoing requests, and the bucket reset time has lapsed,+      -- we can just reset the bucket.+      --+      -- if we've already reset the bucket then there should be an ongoing+      -- request so we'll just end up waiting for that to finish+      when+        (canResetBucketNow now s)+        (resetBucket bucket) -    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+      s <- readTVar $ bucket ^. #state -  case mWaitUntil of-    Just when -> do-      threadDelayUntil when-      atomically . L.release $ bucket ^. #lock-    Nothing ->-      pure ()+      if s ^. #remaining - s ^. #ongoing > 0+        then do+          -- there are tokens remaining for us to use+          modifyTVar'+            (bucket ^. #state)+            ( (#remaining -~ 1)+                . (#ongoing +~ 1)+            )+          pure GoNow+        else do+          -- the bucket has expired, there are no ongoing requests because of+          -- [0] wait and then retry after we can unlock the bucket+          pure (intoWaitDelay $ s ^. #resetTime) +    -- putStrLn (show now <> ": Using bucket, waiting until: " <> show mWaitDelay <> ", uses: " <> show s <> ", " <> inf)++    case mWaitDelay of+      WaitUntil waitUntil -> do+        if waitUntil < now+          then threadDelayMS 20+          else -- if the reset is in the past, we're fucked+            threadDelayUntil waitUntil+        -- if we needed to sleep, go again so that multiple concurrent requests+        -- don't exceed the bucket, to ensure we don't sit in a loop if a+        -- request dies on us, bail out after 50 loops+        if tries < 50+          then go (tries + 1)+          else pure () -- print "bailing after number of retries"+      WaitRetrySoon -> do+        threadDelayMS 20+        if tries < 50+          then go (tries + 1)+          else pure () -- print "bailing after number of retries"+      GoNow -> do+        -- print "ok going forward with request"+        pure ()+ doDiscordRequest :: BotC r => IO LbsResponse -> Sem r DiscordResponseType doDiscordRequest r = do   r'' <- P.embed $ Ex.catchAny (Right <$> r) (pure . Left . Ex.displayException)@@ -151,19 +211,12 @@             let resp = responseBody r'             debug $ "Got good response from discord: " +|| status ||+ ""             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)+            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, buildBucketState now r') of@@ -185,32 +238,33 @@ -- | 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+ where+  computedEnd :: Maybe UTCTime+  computedEnd = flip addUTCTime now <$> resetAfter -    resetAfter :: Maybe NominalDiffTime-    resetAfter = realToFrac <$> responseHeader r "X-Ratelimit-Reset-After" ^? _Just . _Double+  resetAfter :: Maybe NominalDiffTime+  resetAfter = realToFrac <$> responseHeader r "X-Ratelimit-Reset-After" ^? _Just . _Double -    end :: Maybe UTCTime-    end = posixSecondsToUTCTime . realToFrac <$>-      responseHeader r "X-Ratelimit-Reset" ^? _Just . _Double+  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"+buildBucketState now r = (,) <$> bs <*> bucketKey+ where+  remaining = responseHeader r "X-RateLimit-Remaining" ^? _Just . _Integral+  limit = responseHeader r "X-RateLimit-Limit" ^? _Just . _Integral+  resetKey = ceiling <$> responseHeader r "X-RateLimit-Reset" ^? _Just . _Double+  resetTime = parseRateLimitHeader now r+  bs = BucketState resetTime <$> resetKey <*> remaining <*> limit <*> pure 0+  bucketKey = responseHeader r "X-RateLimit-Bucket"  -- | 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+ where+  retryAfter = realToFrac $ r ^?! key "retry_after" . _Double  isGlobal :: Value -> Bool isGlobal r = r ^? key "global" . _Bool == Just True@@ -247,10 +301,14 @@ threadDelayMS :: Int -> IO () threadDelayMS ms = threadDelay (1000 * ms) +tenMS :: NominalDiffTime+tenMS = 0.01+ threadDelayUntil :: UTCTime -> IO () threadDelayUntil when = do+  let when' = addUTCTime tenMS when -- lol   now <- getCurrentTime-  let msUntil = round . (* 1000) . realToFrac @_ @Double $ diffUTCTime when now+  let msUntil = ceiling . (* 1000) . realToFrac @_ @Double $ diffUTCTime when' now   threadDelayMS msUntil  -- Run a single request@@ -271,79 +329,81 @@   case rl of     KnownRatelimit bucket ->       P.embed $ useBucketOnce bucket--    _ -> pure ()+    _ -> debug "unknown ratelimit"    r' <- doDiscordRequest r    case r' of     Good v bs bk -> do-      void . P.embed . atomically $ updateBucket rlstate route bk bs-      pure $ RGood v--    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-          threadDelayUntil unlockWhen-          atomically $ do-            L.release $ bucket ^. #lock-            resetBucket bucket-        debug "unlocking bucket"-+      void . P.embed . atomically $ do+        case rl of+          KnownRatelimit bucket ->+            modifyTVar' (bucket ^. #state) (#ongoing -~ 1)+          _ -> pure ()+        updateBucket rlstate route bk bs       pure $ RGood v-     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 . atomically $ do+        case rl of+          KnownRatelimit bucket ->+            modifyTVar' (bucket ^. #state) (#ongoing -~ 1)+          _ -> pure ()+        void $ updateBucket rlstate route bk bs        P.embed $ do         threadDelayUntil unlockWhen-        atomically $ do-          L.release $ bucket ^. #lock-          resetBucket bucket        pure $ Retry (HTTPError 429 Nothing)-     Ratelimited unlockWhen False _ -> do       debug "Internal error (ratelimited but no headers), retrying"+      case rl of+        KnownRatelimit bucket ->+          void . P.embed . atomically $ modifyTVar' (bucket ^. #state) (#ongoing -~ 1)+        _ -> pure ()+       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 ()+        atomically $ do+          case rl of+            KnownRatelimit bucket ->+              modifyTVar' (bucket ^. #state) (#ongoing -~ 1)+            _ -> pure ()+          case bs of+            Just (bs', bk) ->+              void $ updateBucket rlstate route bk bs'+            Nothing ->+              pure ()          E.clear gl         threadDelayUntil unlockWhen         E.set gl       pure $ Retry (HTTPError 429 Nothing)-     ServerError c -> do       debug "Server failed, retrying"+      case rl of+        KnownRatelimit bucket ->+          P.embed $ useBucketOnce bucket+        _ -> debug "unknown ratelimit"       pure $ Retry (HTTPError c Nothing)-     InternalResponseError c -> do       debug "Internal error, retrying"+      case rl of+        KnownRatelimit bucket ->+          P.embed $ useBucketOnce bucket+        _ -> debug "unknown ratelimit"       pure $ Retry (InternalClientError c)--    ClientError c v -> pure $ RFail (HTTPError c $ decode v)+    ClientError c v -> do+      case rl of+        KnownRatelimit bucket ->+          P.embed $ useBucketOnce bucket+        _ -> debug "unknown ratelimit"+      pure $ RFail (HTTPError c $ decode v)  doRequest :: BotC r => RateLimitState -> Route -> IO LbsResponse -> Sem r (Either RestError LB.ByteString) doRequest rlstate route action =
Calamity/HTTP/Internal/Types.hs view
@@ -36,12 +36,18 @@   deriving ( Show, Generic )  data BucketState = BucketState-  { resetTime :: UTCTime+  { resetTime :: Maybe UTCTime     -- ^ The time when the bucket resets, used to heuristically wait out ratelimits-  , remaining  :: Maybe Int+  , resetKey  :: Int+    -- ^ The X-Ratelimit-Reset value discord gave us+  , remaining :: Int     -- ^ The number of uses left in the bucket, used to heuristically wait out ratelimits+  , limit :: Int+    -- ^ The total number of uses for this bucket+  , ongoing :: Int+    -- ^ How many ongoing requests   }-  deriving ( Generic )+  deriving ( Generic, Show )  data Bucket = Bucket   { lock :: Lock@@ -60,14 +66,6 @@   = 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
ChangeLog.md view
@@ -1,5 +1,10 @@ # Changelog for Calamity +## 0.1.28.2+++ Correct the emoji HTTP endpoints to work with `CustomEmoji`s++ Rework the ratelimit implementation more+ ## 0.1.28.1  + Rework the ratelimit implementation to use X-Ratelimit-Bucket
calamity.cabal view
@@ -4,10 +4,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 2482ee40637f3fe615af0564b33ebb2dcbb4e67d1ec98d152f59206b977c3686+-- hash: 83f8a2a236db8e2db8a1000d7a922dc0ad1f4c27ec148e6c0776bdb2fb3be4ae  name:           calamity-version:        0.1.28.1+version:        0.1.28.2 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