Skip to content

Commit 6aa3833

Browse files
authored
chore(rivetkit): enforce limits on fs driver kv api to match engine (#4273)
1 parent cd347ac commit 6aa3833

4 files changed

Lines changed: 89 additions & 1 deletion

File tree

engine/packages/pegboard/src/actor_kv/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ mod entry;
1313
mod utils;
1414

1515
const VERSION: &str = env!("CARGO_PKG_VERSION");
16+
17+
// Keep the KV validation limits below in sync with
18+
// rivetkit-typescript/packages/rivetkit/src/drivers/file-system/kv-limits.ts.
1619
const MAX_KEY_SIZE: usize = 2 * 1024;
1720
const MAX_VALUE_SIZE: usize = 128 * 1024;
1821
const MAX_KEYS: usize = 128;

engine/packages/pegboard/src/actor_kv/utils.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ pub fn validate_entries(
8383
for value in values {
8484
ensure!(
8585
value.len() <= MAX_VALUE_SIZE,
86-
"value is too large (max 128 KiB)"
86+
"value is too large (max {} KiB)",
87+
MAX_VALUE_SIZE / 1024
8788
);
8889
}
8990

rivetkit-typescript/packages/rivetkit/src/drivers/file-system/global-state.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ import {
3737
type SqliteRuntime,
3838
type SqliteRuntimeDatabase,
3939
} from "./sqlite-runtime";
40+
import {
41+
estimateKvSize,
42+
validateKvEntries,
43+
validateKvKey,
44+
validateKvKeys,
45+
} from "./kv-limits";
4046

4147
// Actor handler to track running instances
4248

@@ -1322,7 +1328,10 @@ export class FileSystemGlobalState {
13221328
}
13231329
throw new Error(`Actor ${actorId} state not loaded`);
13241330
}
1331+
13251332
const db = this.#getOrCreateActorKvDatabase(actorId);
1333+
const totalSize = estimateKvSize(db);
1334+
validateKvEntries(entries, totalSize);
13261335
this.#putKvEntriesInDb(db, entries);
13271336
});
13281337
}
@@ -1344,6 +1353,8 @@ export class FileSystemGlobalState {
13441353
}
13451354
}
13461355

1356+
validateKvKeys(keys);
1357+
13471358
const db = this.#getOrCreateActorKvDatabase(actorId);
13481359
const results: (Uint8Array | null)[] = [];
13491360
for (const key of keys) {
@@ -1372,9 +1383,11 @@ export class FileSystemGlobalState {
13721383
}
13731384
throw new Error(`Actor ${actorId} state not loaded`);
13741385
}
1386+
13751387
if (keys.length === 0) {
13761388
return;
13771389
}
1390+
validateKvKeys(keys);
13781391

13791392
const db = this.#getOrCreateActorKvDatabase(actorId);
13801393
db.exec("BEGIN");
@@ -1410,6 +1423,7 @@ export class FileSystemGlobalState {
14101423
throw new Error(`Actor ${actorId} state not loaded`);
14111424
}
14121425
}
1426+
validateKvKey(prefix, "prefix key");
14131427

14141428
const db = this.#getOrCreateActorKvDatabase(actorId);
14151429
const upperBound = computePrefixUpperBound(prefix);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { SqliteRuntimeDatabase } from "./sqlite-runtime";
2+
3+
// Keep these limits in sync with engine/packages/pegboard/src/actor_kv/mod.rs.
4+
const KV_MAX_KEY_SIZE = 2 * 1024;
5+
const KV_MAX_VALUE_SIZE = 128 * 1024;
6+
const KV_MAX_KEYS = 128;
7+
const KV_MAX_PUT_PAYLOAD_SIZE = 976 * 1024;
8+
const KV_MAX_STORAGE_SIZE = 1024 * 1024 * 1024;
9+
const KV_KEY_WRAPPER_OVERHEAD_SIZE = 2;
10+
11+
export function estimateKvSize(db: SqliteRuntimeDatabase): number {
12+
const row = db.get<{ total: number | bigint | null }>(
13+
"SELECT COALESCE(SUM(LENGTH(key) + LENGTH(value)), 0) AS total FROM kv",
14+
);
15+
return row ? Number(row.total ?? 0) : 0;
16+
}
17+
18+
export function validateKvKey(
19+
key: Uint8Array,
20+
keyLabel: "key" | "prefix key" = "key",
21+
): void {
22+
if (key.byteLength + KV_KEY_WRAPPER_OVERHEAD_SIZE > KV_MAX_KEY_SIZE) {
23+
throw new Error(`${keyLabel} is too long (max 2048 bytes)`);
24+
}
25+
}
26+
27+
export function validateKvKeys(keys: Uint8Array[]): void {
28+
if (keys.length > KV_MAX_KEYS) {
29+
throw new Error("a maximum of 128 keys is allowed");
30+
}
31+
32+
for (const key of keys) {
33+
validateKvKey(key);
34+
}
35+
}
36+
37+
export function validateKvEntries(
38+
entries: [Uint8Array, Uint8Array][],
39+
totalSize: number,
40+
): void {
41+
if (entries.length > KV_MAX_KEYS) {
42+
throw new Error("A maximum of 128 key-value entries is allowed");
43+
}
44+
45+
let payloadSize = 0;
46+
for (const [key, value] of entries) {
47+
payloadSize +=
48+
key.byteLength + KV_KEY_WRAPPER_OVERHEAD_SIZE + value.byteLength;
49+
}
50+
51+
if (payloadSize > KV_MAX_PUT_PAYLOAD_SIZE) {
52+
throw new Error("total payload is too large (max 976 KiB)");
53+
}
54+
55+
const storageRemaining = Math.max(0, KV_MAX_STORAGE_SIZE - totalSize);
56+
if (payloadSize > storageRemaining) {
57+
throw new Error(
58+
`not enough space left in storage (${storageRemaining} bytes remaining, current payload is ${payloadSize} bytes)`,
59+
);
60+
}
61+
62+
for (const [key, value] of entries) {
63+
validateKvKey(key);
64+
if (value.byteLength > KV_MAX_VALUE_SIZE) {
65+
throw new Error(
66+
`value is too large (max ${KV_MAX_VALUE_SIZE / 1024} KiB)`,
67+
);
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)