aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar kwzrd <[email protected]>2020-08-14 19:39:07 +0200
committerGravatar kwzrd <[email protected]>2020-08-14 20:16:35 +0200
commit689e5203f993604f1cb40be65e22b79ceb04d382 (patch)
tree25dce55e7a4d5cd660bf906408db227b44973c57
parentVerification: adjust coroutines to use generic dispatch (diff)
Verification: pause request execution after each batch
The Limit values are mostly assumptions, as this feature is very difficult to test at scale. Please see docstring amendmends for further information.
-rw-r--r--bot/cogs/verification.py27
1 files changed, 23 insertions, 4 deletions
diff --git a/bot/cogs/verification.py b/bot/cogs/verification.py
index 8f1a773a8..14c0abfda 100644
--- a/bot/cogs/verification.py
+++ b/bot/cogs/verification.py
@@ -88,6 +88,13 @@ MENTION_UNVERIFIED = discord.AllowedMentions(
Request = t.Callable[[discord.Member], t.Awaitable]
+class Limit(t.NamedTuple):
+ """Composition over config for throttling requests."""
+
+ batch_size: int # Amount of requests after which to pause
+ sleep_secs: int # Sleep this many seconds after each batch
+
+
def is_verified(member: discord.Member) -> bool:
"""
Check whether `member` is considered verified.
@@ -233,19 +240,22 @@ class Verification(Cog):
return result
- async def _send_requests(self, members: t.Collection[discord.Member], request: Request) -> int:
+ async def _send_requests(self, members: t.Collection[discord.Member], request: Request, limit: Limit) -> int:
"""
Pass `members` one by one to `request` handling Discord exceptions.
This coroutine serves as a generic `request` executor for kicking members and adding
roles, as it allows us to define the error handling logic in one place only.
+ To avoid rate-limits, pass a `limit` configuring the batch size and the amount of seconds
+ to sleep between batches.
+
Returns the amount of successful requests. Failed requests are logged at info level.
"""
log.info(f"Sending {len(members)} requests")
n_success, bad_statuses = 0, set()
- for member in members:
+ for progress, member in enumerate(members, start=1):
if is_verified(member): # Member could have verified in the meantime
continue
try:
@@ -255,6 +265,10 @@ class Verification(Cog):
else:
n_success += 1
+ if progress % limit.batch_size == 0:
+ log.trace(f"Processed {progress} requests, pausing for {limit.sleep_secs} seconds")
+ await asyncio.sleep(limit.sleep_secs)
+
if bad_statuses:
log.info(f"Failed to send {len(members) - n_success} requests due to following statuses: {bad_statuses}")
@@ -264,6 +278,9 @@ class Verification(Cog):
"""
Kick `members` from the PyDis guild.
+ Due to strict ratelimits on sending messages (120 requests / 60 secs), we sleep for a second
+ after each 2 requests to allow breathing room for other features.
+
Note that this is a potentially destructive operation. Returns the amount of successful requests.
"""
log.info(f"Kicking {len(members)} members from the guild (not verified after {KICKED_AFTER} days)")
@@ -274,7 +291,7 @@ class Verification(Cog):
await member.send(KICKED_MESSAGE)
await member.kick(reason=f"User has not verified in {KICKED_AFTER} days")
- n_kicked = await self._send_requests(members, kick_request)
+ n_kicked = await self._send_requests(members, kick_request, Limit(batch_size=2, sleep_secs=1))
self.bot.stats.incr("verification.kicked", count=n_kicked)
return n_kicked
@@ -283,6 +300,8 @@ class Verification(Cog):
"""
Give `role` to all `members`.
+ We pause for a second after batches of 25 requests to ensure ratelimits aren't exceeded.
+
Returns the amount of successful requests.
"""
log.info(f"Assigning {role} role to {len(members)} members (not verified after {UNVERIFIED_AFTER} days)")
@@ -291,7 +310,7 @@ class Verification(Cog):
"""Add `role` to `member`."""
await member.add_roles(role, reason=f"User has not verified in {UNVERIFIED_AFTER} days")
- return await self._send_requests(members, role_request)
+ return await self._send_requests(members, role_request, Limit(batch_size=25, sleep_secs=1))
async def _check_members(self) -> t.Tuple[t.Set[discord.Member], t.Set[discord.Member]]:
"""