Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use cloudflare's ratelimiter if possible #2544

Merged
merged 4 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/api/src/pkg/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { z } from "zod";
import type { MessageBody } from "./key_migration/message";

export const cloudflareRatelimiter = z.custom<{
limit: (opts: { key: string }) => Promise<{ success: boolean }>;
}>((r) => typeof r.limit === "function");
chronark marked this conversation as resolved.
Show resolved Hide resolved

export const zEnv = z.object({
VERSION: z.string().default("unknown"),
DATABASE_HOST: z.string(),
Expand Down Expand Up @@ -42,6 +46,11 @@ export const zEnv = z.object({
return 0;
}
}),
RL_10_60s: cloudflareRatelimiter,
RL_30_60s: cloudflareRatelimiter,
RL_200_60s: cloudflareRatelimiter,
RL_500_10s: cloudflareRatelimiter,
RL_200_10s: cloudflareRatelimiter,
});

export type Env = z.infer<typeof zEnv>;
32 changes: 29 additions & 3 deletions apps/api/src/pkg/ratelimit/client.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Err, Ok, type Result } from "@unkey/error";
import type { Logger } from "@unkey/worker-logging";
import type { Metrics } from "../metrics";

import { cloudflareRatelimiter } from "../env";
import type { Context } from "../hono/app";
import type { Metrics } from "../metrics";
import { retry } from "../util/retry";
import { Agent } from "./agent";
import {
Expand All @@ -11,7 +11,6 @@ import {
type RatelimitRequest,
type RatelimitResponse,
} from "./interface";

export class AgentRatelimiter implements RateLimiter {
private readonly logger: Logger;
private readonly metrics: Metrics;
Expand Down Expand Up @@ -67,7 +66,34 @@ export class AgentRatelimiter implements RateLimiter {
req: RatelimitRequest,
): Promise<Result<RatelimitResponse, RatelimitError>> {
const start = performance.now();
try {
if (req.async) {
// Construct a binding key that could match a configured ratelimiter
const lookup = `RL_${req.limit}_${Math.round(req.interval / 1000)}s` as keyof typeof c.env;
const binding = c.env[lookup];

if (binding) {
const res = await cloudflareRatelimiter.parse(binding).limit({ key: req.identifier });

this.metrics.emit({
metric: "metric.ratelimit",
workspaceId: req.workspaceId,
namespaceId: req.namespaceId,
latency: performance.now() - start,
identifier: req.identifier,
mode: "async",
error: false,
success: res.success,
source: "cloudflare",
});
return Ok({ pass: res.success, reset: -1, current: -1, remaining: -1, triggered: null });
}
}
} catch (err) {
this.logger.error("cfrl failed, falling back to agent", {
error: (err as Error).message,
});
}
const res = await this._limit(c, req);
this.metrics.emit({
metric: "metric.ratelimit",
Expand Down
155 changes: 155 additions & 0 deletions apps/api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,44 @@ bindings = [
{ name = "DO_USAGELIMIT", class_name = "DurableObjectUsagelimiter" },
]

[[unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


[queues]
consumers = [
{ queue = "key-migrations-development", max_batch_size = 10, max_retries = 10, dead_letter_queue = "key-migrations-development-dlq" },
Expand Down Expand Up @@ -62,6 +100,45 @@ consumers = [
{ queue = "key-migrations-preview-dlq", max_batch_size = 10, max_retries = 10 },
]

[[env.preview.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.preview.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.preview.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
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.
# all settings must be the same as production, except for the route pattern
[env.canary]
Expand All @@ -81,6 +158,45 @@ consumers = [
{ queue = "key-migrations-canary", max_batch_size = 10, max_retries = 10, dead_letter_queue = "key-migrations-canary-dlq" },
{ queue = "key-migrations-canary-dlq", max_batch_size = 10, max_retries = 10 },
]

[[env.canary.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.canary.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.canary.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


[env.production]
vars = { ENVIRONMENT = "production", SYNC_RATELIMIT_ON_NO_DATA = "1" }
route = { pattern = "api.unkey.dev", custom_domain = true }
Expand All @@ -104,3 +220,42 @@ consumers = [

[env.production.observability]
enabled = true

[[env.production.unsafe.bindings]]
# The nameing scheme is important, because we're dynamically constructing
# these in the api code
#
# RL_{LIMIT}_{DURATION}s
#
# The namespace_id schema is somewhat made up though.
# I prefixed everything with 9900 -> 9900{limit}{duration}
name = "RL_10_60s"
type = "ratelimit"
namespace_id = "99001060"
simple = { limit = 10, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_30_60s"
type = "ratelimit"
namespace_id = "99003060"
simple = { limit = 30, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_200_60s"
type = "ratelimit"
namespace_id = "990020060"
simple = { limit = 200, period = 60}

[[env.production.unsafe.bindings]]
name = "RL_500_10s"
type = "ratelimit"
namespace_id = "990050010"
simple = { limit = 500, period = 10}

[[env.production.unsafe.bindings]]
name = "RL_200_10s"
type = "ratelimit"
namespace_id = "990020010"
simple = { limit = 200, period = 10}


2 changes: 1 addition & 1 deletion internal/metrics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const metricSchema = z.discriminatedUnion("metric", [
mode: z.enum(["sync", "async"]),
success: z.boolean().optional(),
error: z.boolean().optional(),
source: z.enum(["agent", "durable_object"]),
source: z.enum(["agent", "durable_object", "cloudflare"]),
}),
z.object({
metric: z.literal("metric.usagelimit"),
Expand Down
Loading
Loading