mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(convex): reduce hot rate-limit and catalog reads
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
const MAX_TRENDING_LIMIT = 200;
|
||||
const KEEP_LEADERBOARD_ENTRIES = 3;
|
||||
const DAILY_STATS_PAGE_SIZE = 5_000;
|
||||
const DAILY_STATS_PAGE_SIZE = 1_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action → Query → Mutation pattern (avoids 32K document-read limit)
|
||||
|
||||
@@ -173,6 +173,35 @@ describe("applyRateLimit headers", () => {
|
||||
expect(headers.get("Retry-After")).toBeNull();
|
||||
});
|
||||
|
||||
it("converts shard write conflicts into a rate-limit response", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(2_500_000);
|
||||
const ctx = {
|
||||
runQuery: vi.fn().mockResolvedValue({
|
||||
allowed: true,
|
||||
remaining: 19,
|
||||
limit: 20,
|
||||
resetAt: 2_530_000,
|
||||
}),
|
||||
runMutation: vi
|
||||
.fn()
|
||||
.mockRejectedValue(
|
||||
new Error(
|
||||
'Document in table "rateLimitShards" changed while this mutation was being run',
|
||||
),
|
||||
),
|
||||
} as unknown as Parameters<typeof applyRateLimit>[0];
|
||||
const request = new Request("https://example.com", {
|
||||
headers: { "cf-connecting-ip": "203.0.113.1" },
|
||||
});
|
||||
|
||||
const result = await applyRateLimit(ctx, request, "download");
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.response.status).toBe(429);
|
||||
expect(result.response.headers.get("Retry-After")).toBe("30");
|
||||
});
|
||||
|
||||
it("allows authenticated users when user bucket is healthy and shared ip bucket is exhausted", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(3_000_000);
|
||||
const ctx = makeRateLimitCtx({
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getOptionalApiTokenUserId } from "./apiTokenAuth";
|
||||
import { corsHeaders, mergeHeaders } from "./httpHeaders";
|
||||
|
||||
const RATE_LIMIT_WINDOW_MS = 60_000;
|
||||
const RATE_LIMIT_SHARDS = 16;
|
||||
const RATE_LIMIT_SHARDS = 64;
|
||||
export const RATE_LIMITS = {
|
||||
read: { ip: 600, key: 2400 },
|
||||
write: { ip: 45, key: 180 },
|
||||
@@ -235,7 +235,7 @@ function shouldTrustForwardedIps() {
|
||||
function isRateLimitWriteConflict(error: unknown) {
|
||||
if (!(error instanceof Error)) return false;
|
||||
return (
|
||||
error.message.includes("rateLimits") &&
|
||||
(error.message.includes("rateLimits") || error.message.includes("rateLimitShards")) &&
|
||||
error.message.includes("changed while this mutation was being run")
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -554,7 +554,7 @@ export const backfillLlmEval: ReturnType<typeof internalAction> = internalAction
|
||||
return { error: "OPENAI_API_KEY not configured" };
|
||||
}
|
||||
|
||||
const requestedBatchSize = Math.max(1, Math.floor(args.batchSize ?? 25));
|
||||
const requestedBatchSize = Math.max(1, Math.min(Math.floor(args.batchSize ?? 25), 50));
|
||||
const maxToSchedule =
|
||||
args.maxToSchedule === undefined ? undefined : Math.max(0, Math.floor(args.maxToSchedule));
|
||||
const cursor = args.cursor ?? 0;
|
||||
|
||||
@@ -22,14 +22,12 @@ const consumeHandler = (
|
||||
)._handler;
|
||||
|
||||
describe("rate limit sharding", () => {
|
||||
it("sums all shards when checking status", async () => {
|
||||
it("sums shard rows without reading the legacy rateLimits table", async () => {
|
||||
const ctx = {
|
||||
db: {
|
||||
query: vi.fn((table: string) => ({
|
||||
query: vi.fn(() => ({
|
||||
withIndex: vi.fn(() => ({
|
||||
collect: vi.fn(async () =>
|
||||
table === "rateLimits" ? [{ count: 3 }] : [{ count: 4 }, { count: 5 }],
|
||||
),
|
||||
collect: vi.fn(async () => [{ count: 4 }, { count: 5 }]),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
@@ -42,7 +40,9 @@ describe("rate limit sharding", () => {
|
||||
});
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.remaining).toBe(8);
|
||||
expect(result.remaining).toBe(11);
|
||||
expect(ctx.db.query).toHaveBeenCalledTimes(1);
|
||||
expect(ctx.db.query).toHaveBeenCalledWith("rateLimitShards");
|
||||
});
|
||||
|
||||
it("writes only the selected shard when consuming", async () => {
|
||||
@@ -55,7 +55,7 @@ describe("rate limit sharding", () => {
|
||||
})),
|
||||
})),
|
||||
});
|
||||
return { unique: vi.fn(async () => null) };
|
||||
return { first: vi.fn(async () => null) };
|
||||
});
|
||||
const ctx = {
|
||||
db: {
|
||||
|
||||
@@ -19,16 +19,12 @@ export const getRateLimitStatusInternal = internalQuery({
|
||||
return { allowed: false, remaining: 0, limit: args.limit, resetAt };
|
||||
}
|
||||
|
||||
const legacyRows = await ctx.db
|
||||
.query("rateLimits")
|
||||
.withIndex("by_key_window", (q) => q.eq("key", args.key).eq("windowStart", windowStart))
|
||||
.collect();
|
||||
const shardRows = await ctx.db
|
||||
.query("rateLimitShards")
|
||||
.withIndex("by_key_window", (q) => q.eq("key", args.key).eq("windowStart", windowStart))
|
||||
.collect();
|
||||
|
||||
const count = [...legacyRows, ...shardRows].reduce((sum, row) => sum + row.count, 0);
|
||||
const count = shardRows.reduce((sum, row) => sum + row.count, 0);
|
||||
const allowed = count < args.limit;
|
||||
return {
|
||||
allowed,
|
||||
@@ -61,7 +57,7 @@ export const consumeRateLimitInternal = internalMutation({
|
||||
.withIndex("by_key_window_shard", (q) =>
|
||||
q.eq("key", args.key).eq("windowStart", windowStart).eq("shard", shard),
|
||||
)
|
||||
.unique();
|
||||
.first();
|
||||
|
||||
if (!existing) {
|
||||
await ctx.db.insert("rateLimitShards", {
|
||||
|
||||
+3
-3
@@ -124,8 +124,8 @@ const MAX_LIST_LIMIT = 50;
|
||||
const MAX_PUBLIC_LIST_LIMIT = 200;
|
||||
const MAX_LIST_BULK_LIMIT = 200;
|
||||
const MAX_LIST_TAKE = 1000;
|
||||
const MAX_SKILL_CATALOG_SCAN_DOCUMENTS = 2_000;
|
||||
const MAX_SKILL_CATALOG_SCAN_PAGES = 20;
|
||||
const MAX_SKILL_CATALOG_SCAN_DOCUMENTS = 500;
|
||||
const MAX_SKILL_CATALOG_SCAN_PAGES = 6;
|
||||
const MAX_SKILL_CATALOG_SEARCH_PAGE_SIZE = 200;
|
||||
const HARD_DELETE_BATCH_SIZE = 100;
|
||||
const HARD_DELETE_VERSION_BATCH_SIZE = 10;
|
||||
@@ -3906,7 +3906,7 @@ export const getActiveSkillBatchForLlmBackfillInternal = internalQuery({
|
||||
batchSize: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const batchSize = args.batchSize ?? 10;
|
||||
const batchSize = clampInt(args.batchSize ?? 10, 1, 50);
|
||||
const cursor = args.cursor ?? 0;
|
||||
|
||||
// Use built-in by_creation_time index for stable cursor-based pagination
|
||||
|
||||
Reference in New Issue
Block a user