fix(discord): prune idle rest route mappings

This commit is contained in:
Peter Steinberger
2026-04-29 14:01:31 +01:00
parent 32db9ff538
commit 47b3530af3
2 changed files with 43 additions and 4 deletions

View File

@@ -89,6 +89,7 @@ export class RestScheduler<TData> {
return {
globalRateLimitUntil: this.globalRateLimitUntil,
activeBuckets: this.buckets.size,
routeBucketMappings: this.routeBuckets.size,
buckets: Array.from(this.buckets.entries()).map(([key, bucket]) => ({
key,
active: bucket.active,
@@ -144,6 +145,34 @@ export class RestScheduler<TData> {
return false;
}
private isBucketRateLimited(bucket: BucketState<TData>, now = Date.now()): boolean {
return bucket.remaining === 0 && bucket.resetAt > now;
}
private pruneRouteMapping(routeKey: string): void {
const bucketKey = this.routeBuckets.get(routeKey);
if (!bucketKey) {
return;
}
this.routeBuckets.delete(routeKey);
this.buckets.get(bucketKey)?.routeKeys.delete(routeKey);
}
private pruneIdleRouteMappings(
bucketKey: string,
bucket: BucketState<TData>,
now = Date.now(),
): void {
if (bucket.active > 0 || bucket.pending.length > 0 || this.isBucketRateLimited(bucket, now)) {
return;
}
for (const routeKey of Array.from(bucket.routeKeys)) {
if (this.routeBuckets.get(routeKey) === bucketKey) {
this.pruneRouteMapping(routeKey);
}
}
}
private shouldPruneIdleBucket(key: string): boolean {
const mappedBucketKey = this.routeBuckets.get(key);
return mappedBucketKey !== key && !this.hasBucketReference(key);
@@ -267,7 +296,15 @@ export class RestScheduler<TData> {
break;
}
if (bucket.pending.length === 0) {
if (bucket.active === 0 && this.shouldPruneIdleBucket(key)) {
if (bucket.active !== 0) {
continue;
}
if (this.isBucketRateLimited(bucket, now)) {
nextDelayMs = Math.min(nextDelayMs, bucket.resetAt - now);
continue;
}
this.pruneIdleRouteMappings(key, bucket, now);
if (this.shouldPruneIdleBucket(key)) {
this.buckets.delete(key);
}
continue;

View File

@@ -83,7 +83,7 @@ describe("RequestClient", () => {
]);
});
it("prunes idle route buckets after Discord bucket remapping", async () => {
it("prunes idle route buckets and mappings after Discord bucket remapping", async () => {
const client = new RequestClient("test-token", {
fetch: async () =>
new Response(JSON.stringify({ id: "first" }), {
@@ -95,8 +95,9 @@ describe("RequestClient", () => {
await expect(client.get("/channels/c1/messages")).resolves.toEqual({ id: "first" });
const metrics = client.getSchedulerMetrics();
expect(metrics.activeBuckets).toBe(1);
expect(metrics.buckets.map((bucket) => bucket.key)).toEqual(["channel-messages:channels/c1"]);
expect(metrics.activeBuckets).toBe(0);
expect(metrics.routeBucketMappings).toBe(0);
expect(metrics.buckets).toEqual([]);
});
it("waits for a learned bucket reset before dispatching the next request", async () => {
@@ -135,6 +136,7 @@ describe("RequestClient", () => {
const client = new RequestClient("test-token", { fetch: fetchSpy });
await expect(client.get("/channels/c1/messages")).resolves.toEqual({ id: "first" });
expect(client.getSchedulerMetrics().routeBucketMappings).toBe(1);
const second = client.get("/channels/c1/messages");
await Promise.resolve();