glyde/identify_queue
Discord’s IDENTIFY concurrency limit, as a pure state machine.
max_concurrency from GET /gateway/bot divides shards into buckets by
shard_id % max_concurrency, and one shard per bucket may IDENTIFY every
five seconds. Separately a bot has a daily budget of session starts, and
exhausting that resets the bot token. Two limits, counted apart.
RESUME never comes here: Discord meters IDENTIFY and not RESUME, so a shard holding a session goes straight to the wire.
A grant is a loan. Release gives it back; if nobody does it lapses after
hold_ms, so a lost message costs one bucket half a minute, not forever.
Types
pub type Input {
Request(shard: Int)
Release(shard: Int)
Tick
}
Constructors
-
Request(shard: Int)This shard is at the point in its handshake where the next frame it writes is IDENTIFY.
-
Release(shard: Int)The shard is done with its place, however it went. Cancels a queued request and clears a grant already given.
-
TickRe-evaluate. Idempotent and always safe.
pub type Notice {
BudgetLow(remaining: Int)
Waiting(shard: Int, behind: Int)
}
Constructors
-
BudgetLow(remaining: Int) -
Waiting(shard: Int, behind: Int)behindcounts the shards ahead of this one in its bucket. Zero means it is next and the bucket is only waiting out its clock.
pub type Output {
Grant(shard: Int)
Deny(shard: Int, refresh_at_ms: Int)
Wake(in_ms: Int)
Note(notice: Notice)
}
Constructors
-
Grant(shard: Int)Write IDENTIFY for this shard now, as the very next frame.
-
Deny(shard: Int, refresh_at_ms: Int)Ask
GET /gateway/botagain no sooner thanrefresh_at_ms, the earliest instant it could report a refilled budget, and build a new queue from the answer. The daily session budget is gone; this queue never grants again, so waiting on it buys nothing and the request is dropped, not parked. -
Wake(in_ms: Int)Call
step(_, Tick)again in about this long. A latency optimisation, not a liveness requirement. -
Note(notice: Notice)
Values
pub fn new(
max_concurrency max_concurrency: Int,
remaining remaining: Int,
reset_at_ms reset_at_ms: Int,
) -> Queue
From GET /gateway/bot’s session_start_limit. Counts off the wire are
clamped, not rejected: a state machine may not crash on a peer’s numbers.
reset_at_ms is not clamped. It is an instant on the caller’s clock, which
nothing here can judge, so the denial that reads it is held to the present
instead.
pub fn rate_limit_key(queue: Queue, shard shard: Int) -> Int
Which bucket a shard identifies through. Discord calls it rate_limit_key
and a bot split over several processes routes on it.
pub fn remaining(queue: Queue) -> Int
Session starts left, as this gate has counted them. Never rises: a gate that refills itself hands out the thousandth IDENTIFY twice.
pub fn solo() -> Queue
A single-shard bot, still spacing IDENTIFYs five seconds apart. 1000 is
Discord’s default daily budget and a placeholder: past 150 000 guilds the
real number is larger, so pass it once GET /gateway/bot answers.
pub fn wake_after(
queue: Queue,
now_ms now_ms: Int,
) -> option.Option(Int)
When the queue would next do something useful, in milliseconds from
now_ms. None means no shard is waiting.