From 7da3c6764eff436bea095eaaaf4149c8aed36973 Mon Sep 17 00:00:00 2001 From: chronark Date: Mon, 13 Jan 2025 20:29:49 +0100 Subject: [PATCH 1/3] revert: use durable objects for sync ratelimiting --- apps/api/src/pkg/env.ts | 1 + apps/api/src/pkg/middleware/init.ts | 6 +- apps/api/src/pkg/ratelimit/do_client.ts | 240 ++++++++++++++++++ apps/api/src/pkg/ratelimit/durable_object.ts | 72 ++++++ apps/api/src/pkg/ratelimit/index.ts | 2 + ..._keys_verifyKey.ratelimit_accuracy.test.ts | 63 +++-- .../v1_ratelimits_limit.accuracy.test.ts | 66 +++-- apps/api/src/worker.ts | 2 + apps/api/wrangler.custom.toml | 24 +- apps/api/wrangler.toml | 77 +++--- packages/api/package.json | 11 +- packages/hono/package.json | 11 +- packages/nextjs/package.json | 11 +- packages/ratelimit/package.json | 12 +- 14 files changed, 479 insertions(+), 119 deletions(-) create mode 100644 apps/api/src/pkg/ratelimit/do_client.ts create mode 100644 apps/api/src/pkg/ratelimit/durable_object.ts diff --git a/apps/api/src/pkg/env.ts b/apps/api/src/pkg/env.ts index 6a2dc8a8cf..01ab879cd3 100644 --- a/apps/api/src/pkg/env.ts +++ b/apps/api/src/pkg/env.ts @@ -18,6 +18,7 @@ export const zEnv = z.object({ CLOUDFLARE_API_KEY: z.string().optional(), CLOUDFLARE_ZONE_ID: z.string().optional(), ENVIRONMENT: z.enum(["development", "preview", "canary", "production"]).default("development"), + DO_RATELIMIT: z.custom((ns) => typeof ns === "object"), // pretty loose check but it'll do I think DO_USAGELIMIT: z.custom((ns) => typeof ns === "object"), KEY_MIGRATIONS: z.custom>((q) => typeof q === "object").optional(), EMIT_METRICS_LOGS: z diff --git a/apps/api/src/pkg/middleware/init.ts b/apps/api/src/pkg/middleware/init.ts index 9e5b3df5ab..50042fe269 100644 --- a/apps/api/src/pkg/middleware/init.ts +++ b/apps/api/src/pkg/middleware/init.ts @@ -2,7 +2,7 @@ import { Analytics } from "@/pkg/analytics"; import { createConnection } from "@/pkg/db"; import { KeyService } from "@/pkg/keys/service"; -import { AgentRatelimiter } from "@/pkg/ratelimit"; +import { DurableRateLimiter } from "@/pkg/ratelimit"; import { DurableUsageLimiter, NoopUsageLimiter } from "@/pkg/usagelimit"; import { RBAC } from "@unkey/rbac"; import { ConsoleLogger } from "@unkey/worker-logging"; @@ -100,8 +100,8 @@ export function init(): MiddlewareHandler { clickhouseUrl: c.env.CLICKHOUSE_URL, clickhouseInsertUrl: c.env.CLICKHOUSE_INSERT_URL, }); - const rateLimiter = new AgentRatelimiter({ - agent: { url: c.env.AGENT_URL, token: c.env.AGENT_TOKEN }, + const rateLimiter = new DurableRateLimiter({ + namespace: c.env.DO_RATELIMIT, cache: rlMap, logger, metrics, diff --git a/apps/api/src/pkg/ratelimit/do_client.ts b/apps/api/src/pkg/ratelimit/do_client.ts new file mode 100644 index 0000000000..4f43253a7f --- /dev/null +++ b/apps/api/src/pkg/ratelimit/do_client.ts @@ -0,0 +1,240 @@ +import { Err, Ok, type Result } from "@unkey/error"; +import type { Logger } from "@unkey/worker-logging"; +import type { Context } from "hono"; +import { z } from "zod"; +import type { Metrics } from "../metrics"; +import { + type RateLimiter, + RatelimitError, + type RatelimitRequest, + type RatelimitResponse, +} from "./interface"; + +export class DurableRateLimiter implements RateLimiter { + private readonly namespace: DurableObjectNamespace; + private readonly domain: string; + private readonly logger: Logger; + private readonly metrics: Metrics; + private readonly cache: Map; + constructor(opts: { + namespace: DurableObjectNamespace; + + domain?: string; + logger: Logger; + metrics: Metrics; + cache: Map; + }) { + this.namespace = opts.namespace; + this.domain = opts.domain ?? "unkey.dev"; + this.logger = opts.logger; + this.metrics = opts.metrics; + this.cache = opts.cache; + } + + private getId(req: RatelimitRequest): string { + const now = Date.now(); + const window = Math.floor(now / req.interval); + + return [req.identifier, window, req.shard].join("::"); + } + + private setCacheMax(id: string, i: number): number { + const current = this.cache.get(id) ?? 0; + if (i > current) { + this.cache.set(id, i); + return i; + } + return current; + } + + /** + * Do not use + */ + public async multiLimit( + c: Context, + req: Array, + ): Promise> { + const res = await Promise.all(req.map((r) => this.limit(c, r))); + for (const r of res) { + if (r.err) { + return r; + } + if (!r.val.passed) { + return r; + } + } + if (res.length > 0) { + return Ok(res[0].val!); + } + + return Ok({ + current: -1, + passed: true, + reset: -1, + remaining: -1, + triggered: null, + }); + } + + public async limit( + c: Context, + req: RatelimitRequest, + ): Promise> { + const start = performance.now(); + const res = await this._limit(c, req); + this.metrics.emit({ + metric: "metric.ratelimit", + workspaceId: req.workspaceId, + namespaceId: req.namespaceId, + latency: performance.now() - start, + identifier: req.identifier, + mode: req.async ? "async" : "sync", + error: !!res.err, + success: res?.val?.passed, + source: "durable_object", + }); + return res; + } + + private async _limit( + c: Context, + req: RatelimitRequest, + ): Promise> { + const window = Math.floor(Date.now() / req.interval); + const reset = (window + 1) * req.interval; + const cost = req.cost ?? 1; + const id = this.getId(req); + + /** + * Catching identifiers that exceeded the limit already + * + * This might not happen too often, but in extreme cases the cache should hit and we can skip + * the request to the durable object entirely, which speeds everything up and is cheper for us + */ + let current = this.cache.get(id) ?? 0; + if (current >= req.limit) { + return Ok({ + remaining: -1, + passed: false, + current, + reset, + triggered: req.name, + }); + } + + const p = this.callDurableObject({ + trigger: req.name, + identifier: req.identifier, + objectName: id, + window, + reset, + cost, + limit: req.limit, + }); + + if (!req.async) { + const res = await p; + if (res.val) { + this.setCacheMax(id, res.val.current); + } + return res; + } + + c.executionCtx.waitUntil( + p.then(async (res) => { + if (res.err) { + console.error(res.err.message); + return; + } + this.setCacheMax(id, res.val.current); + + this.metrics.emit({ + workspaceId: req.workspaceId, + metric: "metric.ratelimit.accuracy", + identifier: req.identifier, + namespaceId: req.namespaceId, + responded: current + cost <= req.limit, + correct: res.val.current + cost <= req.limit, + }); + await this.metrics.flush(); + }), + ); + if (current + cost > req.limit) { + return Ok({ + current, + passed: false, + reset, + remaining: req.limit - current, + triggered: req.name, + }); + } + current += cost; + this.cache.set(id, current); + + return Ok({ + passed: true, + current, + reset, + remaining: req.limit - current, + triggered: null, + }); + } + + private getStub(name: string): DurableObjectStub { + return this.namespace.get(this.namespace.idFromName(name)); + } + + private async callDurableObject(req: { + identifier: string; + objectName: string; + window: number; + reset: number; + cost: number; + limit: number; + trigger: string; + }): Promise> { + try { + const url = `https://${this.domain}/limit`; + const res = await this.getStub(req.objectName) + .fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reset: req.reset, cost: req.cost, limit: req.limit }), + }) + .catch(async (e) => { + this.logger.warn("calling the ratelimit DO failed, retrying ...", { + identifier: req.identifier, + error: (e as Error).message, + }); + + return this.getStub(req.objectName).fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reset: req.reset }), + }); + }); + + const json = await res.json(); + const { current, success } = z + .object({ current: z.number(), success: z.boolean() }) + .parse(json); + + return Ok({ + current, + reset: req.reset, + passed: success, + remaining: req.limit - current, + triggered: success ? null : req.trigger, + }); + } catch (e) { + const err = e as Error; + this.logger.error("ratelimit failed", { + identifier: req.identifier, + error: err.message, + stack: err.stack, + cause: err.cause, + }); + return Err(new RatelimitError({ message: err.message })); + } + } +} diff --git a/apps/api/src/pkg/ratelimit/durable_object.ts b/apps/api/src/pkg/ratelimit/durable_object.ts new file mode 100644 index 0000000000..1bf31d97a2 --- /dev/null +++ b/apps/api/src/pkg/ratelimit/durable_object.ts @@ -0,0 +1,72 @@ +import { zValidator } from "@hono/zod-validator"; +import { Hono } from "hono"; +import { z } from "zod"; + +type Memory = { + current: number; + alarmScheduled?: number; +}; + +export class DurableObjectRatelimiter { + private state: DurableObjectState; + private memory: Memory; + private readonly storageKey = "rl"; + private readonly hono = new Hono(); + constructor(state: DurableObjectState) { + this.state = state; + this.state.blockConcurrencyWhile(async () => { + const m = await this.state.storage.get(this.storageKey); + if (m) { + this.memory = m; + } + }); + this.memory ??= { + current: 0, + }; + + this.hono.post( + "/limit", + zValidator( + "json", + z.object({ + reset: z.number().int(), + cost: z.number().int().default(1), + limit: z.number().int(), + }), + ), + async (c) => { + const { reset, cost, limit } = c.req.valid("json"); + if (!this.memory.alarmScheduled) { + this.memory.alarmScheduled = reset; + await this.state.storage.setAlarm(this.memory.alarmScheduled); + } + if (this.memory.current + cost > limit) { + return c.json({ + success: false, + current: this.memory.current, + }); + } + this.memory.current += cost; + + await this.state.storage.put(this.storageKey, this.memory); + + return c.json({ + success: true, + current: this.memory.current, + }); + }, + ); + } + + // Handle HTTP requests from clients. + async fetch(request: Request) { + return this.hono.fetch(request); + } + + /** + * alarm is called to clean up all state, which will remove the durable object from existence. + */ + public async alarm(): Promise { + await this.state.storage.deleteAll(); + } +} diff --git a/apps/api/src/pkg/ratelimit/index.ts b/apps/api/src/pkg/ratelimit/index.ts index e49f795cef..2f942fc118 100644 --- a/apps/api/src/pkg/ratelimit/index.ts +++ b/apps/api/src/pkg/ratelimit/index.ts @@ -1,3 +1,5 @@ export * from "./client"; export * from "./interface"; export * from "./noop"; +export * from "./durable_object"; +export * from "./do_client"; diff --git a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts index 01a00c833b..beb3332acb 100644 --- a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts +++ b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts @@ -17,35 +17,66 @@ const testCases: { rps: number; seconds: number; }[] = [ + // Very short window, high throughput + { + limit: 50, + duration: 1000, // 1s window + rps: 200, // 4x the limit + seconds: 15, // 15 windows + }, + + // Short window, burst traffic + { + limit: 100, + duration: 5000, // 5s window + rps: 300, // 15x the limit + seconds: 65, // 13 windows + }, + + // Medium window, steady traffic { limit: 200, - duration: 60_000, - rps: 100, - seconds: 60, + duration: 30000, // 30s window + rps: 50, // 7.5x the limit + seconds: 420, // 14 windows }, + + // Edge case: tiny limit { - limit: 10, - duration: 60000, - rps: 10, - seconds: 60, + limit: 5, + duration: 10000, // 10s window + rps: 20, // 40x the limit + seconds: 140, // 14 windows }, + + // Edge case: high limit, short window { - limit: 500, - duration: 10000, - rps: 20, - seconds: 20, + limit: 1000, + duration: 15000, // 15s window + rps: 200, // 3x the limit + seconds: 180, // 12 windows }, + + // Real world: API limit scenario { limit: 500, - duration: 10000, - rps: 100, - seconds: 30, + duration: 60000, // 60s window + rps: 50, // 6x the limit + seconds: 780, // 13 windows + }, + + // High volume, medium window + { + limit: 1000, + duration: 60000, // 60s window + rps: 100, // 6x the limit + seconds: 720, // 12 windows }, ]; for (const { limit, duration, rps, seconds } of testCases) { const name = `[${limit} / ${duration / 1000}s], attacked with ${rps} rps for ${seconds}s`; - test(name, { skip: process.env.TEST_LOCAL, retry: 3, timeout: 600_000 }, async (t) => { + test(name, { retry: 3, timeout: 600_000 }, async (t) => { const h = await IntegrationHarness.init(t); const { key, keyId } = await h.createKey(); @@ -81,7 +112,7 @@ for (const { limit, duration, rps, seconds } of testCases) { }, 0); const exactLimit = Math.min(results.length, (limit / (duration / 1000)) * seconds); - const upperLimit = Math.round(exactLimit * 2.5); + const upperLimit = Math.round(exactLimit * 1.5); const lowerLimit = Math.round(exactLimit * 0.95); console.info({ name, passed, exactLimit, upperLimit, lowerLimit }); t.expect(passed).toBeGreaterThanOrEqual(lowerLimit); diff --git a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts index f441c1383c..7ba9386c65 100644 --- a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts @@ -17,35 +17,65 @@ const testCases: { rps: number; seconds: number; }[] = [ + // Very short window, high throughput + { + limit: 50, + duration: 1000, // 1s window + rps: 200, // 4x the limit + seconds: 15, // 15 windows + }, + + // Short window, burst traffic + { + limit: 100, + duration: 5000, // 5s window + rps: 300, // 15x the limit + seconds: 65, // 13 windows + }, + + // Medium window, steady traffic { limit: 200, - duration: 60_000, - rps: 100, - seconds: 60, + duration: 30000, // 30s window + rps: 50, // 7.5x the limit + seconds: 420, // 14 windows + }, + + // Edge case: tiny limit + { + limit: 5, + duration: 10000, // 10s window + rps: 20, // 40x the limit + seconds: 140, // 14 windows }, + + // Edge case: high limit, short window { - limit: 10, - duration: 60000, - rps: 10, - seconds: 60, + limit: 1000, + duration: 15000, // 15s window + rps: 200, // 3x the limit + seconds: 180, // 12 windows }, + + // Real world: API limit scenario { limit: 500, - duration: 10000, - rps: 20, - seconds: 20, + duration: 60000, // 60s window + rps: 50, // 6x the limit + seconds: 780, // 13 windows }, + + // High volume, medium window { - limit: 500, - duration: 10000, - rps: 100, - seconds: 30, + limit: 1000, + duration: 60000, // 60s window + rps: 100, // 6x the limit + seconds: 720, // 12 windows }, ]; - for (const { limit, duration, rps, seconds } of testCases) { const name = `[${limit} / ${duration / 1000}s], attacked with ${rps} rps for ${seconds}s`; - test(name, { skip: process.env.TEST_LOCAL, retry: 3, timeout: 600_000 }, async (t) => { + test(name, { retry: 3, timeout: 600_000 }, async (t) => { const h = await IntegrationHarness.init(t); const namespace = { id: newId("test"), @@ -84,9 +114,9 @@ for (const { limit, duration, rps, seconds } of testCases) { }, 0); const exactLimit = Math.min(results.length, (limit / (duration / 1000)) * seconds); - const upperLimit = Math.round(exactLimit * 2.5); + const upperLimit = Math.round(exactLimit * 1.5); const lowerLimit = Math.round(exactLimit * 0.95); - console.info({ name, passed, exactLimit, upperLimit, lowerLimit }); + console.info({ requests: results.length, name, passed, exactLimit, upperLimit, lowerLimit }); t.expect(passed).toBeGreaterThanOrEqual(lowerLimit); t.expect(passed).toBeLessThanOrEqual(upperLimit); }); diff --git a/apps/api/src/worker.ts b/apps/api/src/worker.ts index e5c7d6e6de..b9a10a767f 100644 --- a/apps/api/src/worker.ts +++ b/apps/api/src/worker.ts @@ -23,6 +23,8 @@ import { registerLegacyKeysVerifyKey } from "./routes/legacy_keys_verifyKey"; // Export Durable Objects for cloudflare export { DurableObjectUsagelimiter } from "@/pkg/usagelimit/durable_object"; +export { DurableObjectRatelimiter } from "@/pkg/ratelimit/durable_object"; + import { cors, init, metrics } from "@/pkg/middleware"; import type { MessageBatch } from "@cloudflare/workers-types"; import { ConsoleLogger } from "@unkey/worker-logging"; diff --git a/apps/api/wrangler.custom.toml b/apps/api/wrangler.custom.toml index e9dcdf764f..d918bd07db 100644 --- a/apps/api/wrangler.custom.toml +++ b/apps/api/wrangler.custom.toml @@ -14,6 +14,7 @@ route = { pattern = "__CUSTOM_DOMAIN__", custom_domain = true } [durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -27,6 +28,10 @@ tag = "v2" deleted_classes = ["DurableObjectRatelimiter"] +[[migrations]] +tag = "v3" +new_classes = ["DurableObjectRatelimiter"] + [[unsafe.bindings]] # The nameing scheme is important, because we're dynamically constructing @@ -39,53 +44,50 @@ deleted_classes = ["DurableObjectRatelimiter"] name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} - +simple = { limit = 600, period = 60 } [[unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} - - +simple = { limit = 200, period = 10 } diff --git a/apps/api/wrangler.toml b/apps/api/wrangler.toml index bb5e1b2dc5..8741e232c5 100644 --- a/apps/api/wrangler.toml +++ b/apps/api/wrangler.toml @@ -10,6 +10,7 @@ enabled = false ## these are in local dev [durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -25,6 +26,12 @@ new_classes = ["DurableObjectUsagelimiter"] tag = "v3" deleted_classes = ["DurableObjectRatelimiter"] + +[[migrations]] +tag = "v4" +new_classes = ["DurableObjectRatelimiter"] + + [env.development] route = { pattern = "development-api.unkey.dev", custom_domain = true } vars = { ENVIRONMENT = "development" } @@ -32,6 +39,7 @@ vars = { ENVIRONMENT = "development" } [env.development.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -46,54 +54,53 @@ bindings = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} - +simple = { limit = 600, period = 60 } [[unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 } [queues] @@ -112,6 +119,7 @@ route = { pattern = "preview-api.unkey.dev", custom_domain = true } [env.preview.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -134,53 +142,52 @@ consumers = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.preview.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.preview.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.preview.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} - +simple = { limit = 200, period = 10 } # canary is a special environment that is used to test new code by a small percentage of users before it is rolled out to the rest of the world. @@ -192,6 +199,7 @@ route = { pattern = "canary.unkey.dev", custom_domain = true } [env.canary.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -214,52 +222,52 @@ consumers = [ name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.canary.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.canary.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.canary.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 } [env.production] @@ -270,6 +278,7 @@ logpush = true [env.production.durable_objects] bindings = [ + { name = "DO_RATELIMIT", class_name = "DurableObjectRatelimiter" }, { name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" }, ] @@ -297,49 +306,49 @@ enabled = false name = "RL_10_60s" type = "ratelimit" namespace_id = "99001060" -simple = { limit = 10, period = 60} +simple = { limit = 10, period = 60 } [[env.production.unsafe.bindings]] name = "RL_30_60s" type = "ratelimit" namespace_id = "99003060" -simple = { limit = 30, period = 60} +simple = { limit = 30, period = 60 } [[env.production.unsafe.bindings]] name = "RL_50_60s" type = "ratelimit" namespace_id = "99005060" -simple = { limit = 50, period = 60} +simple = { limit = 50, period = 60 } [[env.production.unsafe.bindings]] name = "RL_200_60s" type = "ratelimit" namespace_id = "990020060" -simple = { limit = 200, period = 60} +simple = { limit = 200, period = 60 } [[env.production.unsafe.bindings]] name = "RL_600_60s" type = "ratelimit" namespace_id = "990060060" -simple = { limit = 600, period = 60} +simple = { limit = 600, period = 60 } [[env.production.unsafe.bindings]] name = "RL_1_10s" type = "ratelimit" namespace_id = "9900110" -simple = { limit = 1, period = 10} +simple = { limit = 1, period = 10 } [[env.production.unsafe.bindings]] name = "RL_500_10s" type = "ratelimit" namespace_id = "990050010" -simple = { limit = 500, period = 10} +simple = { limit = 500, period = 10 } [[env.production.unsafe.bindings]] name = "RL_200_10s" type = "ratelimit" namespace_id = "990020010" -simple = { limit = 200, period = 10} +simple = { limit = 200, period = 10 } diff --git a/packages/api/package.json b/packages/api/package.json index 95020dad73..c6fef3285c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -9,19 +9,12 @@ "publishConfig": { "access": "public" }, - "keywords": [ - "unkey", - "client", - "api" - ], + "keywords": ["unkey", "client", "api"], "bugs": { "url": "https://github.com/unkeyed/unkey/issues" }, "homepage": "https://github.com/unkeyed/unkey#readme", - "files": [ - "./dist/**", - "README.md" - ], + "files": ["./dist/**", "README.md"], "author": "Andreas Thomas ", "scripts": { "generate": "openapi-typescript https://api.unkey.dev/openapi.json -o ./src/openapi.d.ts", diff --git a/packages/hono/package.json b/packages/hono/package.json index d5b421df27..eaba9a4e89 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -8,19 +8,12 @@ "publishConfig": { "access": "public" }, - "keywords": [ - "unkey", - "client", - "api", - "hono" - ], + "keywords": ["unkey", "client", "api", "hono"], "bugs": { "url": "https://github.com/unkeyed/unkey/issues" }, "homepage": "https://github.com/unkeyed/unkey#readme", - "files": [ - "./dist/**" - ], + "files": ["./dist/**"], "author": "Andreas Thomas ", "scripts": { "build": "tsup", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index fc63e2e1ef..4786b6ec4d 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -8,19 +8,12 @@ "publishConfig": { "access": "public" }, - "keywords": [ - "unkey", - "client", - "api" - ], + "keywords": ["unkey", "client", "api"], "bugs": { "url": "https://github.com/unkeyed/unkey/issues" }, "homepage": "https://github.com/unkeyed/unkey#readme", - "files": [ - "./dist/**", - "README.md" - ], + "files": ["./dist/**", "README.md"], "author": "Andreas Thomas ", "scripts": { "build": "tsup" diff --git a/packages/ratelimit/package.json b/packages/ratelimit/package.json index dee44c38ec..dbedb47d42 100644 --- a/packages/ratelimit/package.json +++ b/packages/ratelimit/package.json @@ -9,20 +9,12 @@ "publishConfig": { "access": "public" }, - "keywords": [ - "unkey", - "ratelimit", - "global", - "serverless" - ], + "keywords": ["unkey", "ratelimit", "global", "serverless"], "bugs": { "url": "https://github.com/unkeyed/unkey/issues" }, "homepage": "https://github.com/unkeyed/unkey#readme", - "files": [ - "./dist/**", - "README.md" - ], + "files": ["./dist/**", "README.md"], "author": "Andreas Thomas ", "scripts": { "build": "tsup" From 3be1cb0b31df186470b132e90592fcb8b446cfd9 Mon Sep 17 00:00:00 2001 From: chronark Date: Mon, 13 Jan 2025 20:42:11 +0100 Subject: [PATCH 2/3] ci: respect TEST_LOCAL --- .../api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts | 2 +- apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts index beb3332acb..4095c93cdc 100644 --- a/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts +++ b/apps/api/src/routes/v1_keys_verifyKey.ratelimit_accuracy.test.ts @@ -76,7 +76,7 @@ const testCases: { for (const { limit, duration, rps, seconds } of testCases) { const name = `[${limit} / ${duration / 1000}s], attacked with ${rps} rps for ${seconds}s`; - test(name, { retry: 3, timeout: 600_000 }, async (t) => { + test(name, { skip: process.env.TEST_LOCAL, retry: 3, timeout: 600_000 }, async (t) => { const h = await IntegrationHarness.init(t); const { key, keyId } = await h.createKey(); diff --git a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts index 7ba9386c65..bc58da71e7 100644 --- a/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts +++ b/apps/api/src/routes/v1_ratelimits_limit.accuracy.test.ts @@ -75,7 +75,7 @@ const testCases: { ]; for (const { limit, duration, rps, seconds } of testCases) { const name = `[${limit} / ${duration / 1000}s], attacked with ${rps} rps for ${seconds}s`; - test(name, { retry: 3, timeout: 600_000 }, async (t) => { + test(name, { skip: process.env.TEST_LOCAL, retry: 3, timeout: 600_000 }, async (t) => { const h = await IntegrationHarness.init(t); const namespace = { id: newId("test"), From 4843736fcddf59eb0fadda2b625edf753d875a1f Mon Sep 17 00:00:00 2001 From: chronark Date: Mon, 13 Jan 2025 21:08:34 +0100 Subject: [PATCH 3/3] fix: return correct values --- apps/api/src/pkg/ratelimit/do_client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/api/src/pkg/ratelimit/do_client.ts b/apps/api/src/pkg/ratelimit/do_client.ts index 4f43253a7f..e040d09b64 100644 --- a/apps/api/src/pkg/ratelimit/do_client.ts +++ b/apps/api/src/pkg/ratelimit/do_client.ts @@ -114,7 +114,7 @@ export class DurableRateLimiter implements RateLimiter { let current = this.cache.get(id) ?? 0; if (current >= req.limit) { return Ok({ - remaining: -1, + remaining: req.limit - current, passed: false, current, reset, @@ -210,7 +210,7 @@ export class DurableRateLimiter implements RateLimiter { return this.getStub(req.objectName).fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ reset: req.reset }), + body: JSON.stringify({ reset: req.reset, cost: req.cost, limit: req.limit }), }); });