Skip to content

Commit b43c1a4

Browse files
authored
Merge pull request #20 from bakkot/prevent-excessive-requests
Allow bounding total requested data
2 parents ac96d7c + 7e4c7c3 commit b43c1a4

File tree

5 files changed

+45
-8
lines changed

5 files changed

+45
-8
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,13 @@ const config = {
6464
configUrl: "/foo/bar/config.json"
6565
}
6666

67+
68+
let maxBytesToRead = 10 * 1024 * 1024;
6769
const worker = await createDbWorker(
6870
[config],
69-
workerUrl.toString(), wasmUrl.toString()
71+
workerUrl.toString(),
72+
wasmUrl.toString(),
73+
maxBytesToRead // optional, defaults to Infinity
7074
);
7175
// you can also pass multiple config objects which can then be used as separate database schemas with `ATTACH virtualFilename as schemaname`, where virtualFilename is also set in the config object.
7276

@@ -75,6 +79,12 @@ const worker = await createDbWorker(
7579

7680
const result = await worker.db.exec(`select * from table where id = ?`, [123]);
7781

82+
// worker.worker.bytesRead is a Promise for the number of bytes read by the worker.
83+
// if a request would cause it to exceed maxBytesToRead, that request will throw a SQLite disk I/O error.
84+
console.log(await worker.worker.bytesRead);
85+
86+
// you can reset bytesRead by assigning to it:
87+
worker.worker.bytesRead = 0;
7888
```
7989

8090
## Cachebusting
@@ -186,4 +196,4 @@ cd sql.js
186196
yarn build
187197
cd ..
188198
yarn build
189-
```
199+
```

sql.js/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/db.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@ export interface WorkerHttpvfs {
3131
export async function createDbWorker(
3232
configs: SplitFileConfig[],
3333
workerUrl: string,
34-
wasmUrl: string
34+
wasmUrl: string,
35+
maxBytesToRead: number = Infinity
3536
): Promise<WorkerHttpvfs> {
3637
const worker: Worker = new Worker(workerUrl);
3738
const sqlite = Comlink.wrap<SqliteComlinkMod>(worker);
3839

3940
const db = ((await sqlite.SplitFileHttpDatabase(
4041
wasmUrl,
41-
configs
42+
configs,
43+
undefined,
44+
maxBytesToRead
4245
)) as unknown) as Comlink.Remote<LazyHttpDatabase>;
4346

4447
worker.addEventListener("message", handleAsyncRequestFromWorkerThread);

src/lazyFile.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type RangeMapper = (
88
toByte: number
99
) => { url: string; fromByte: number; toByte: number };
1010

11+
export type RequestLimiter = (bytes: number) => void;
12+
1113
export type LazyFileConfig = {
1214
/** function to map a read request to an url with read request */
1315
rangeMapper: RangeMapper;
@@ -21,6 +23,8 @@ export type LazyFileConfig = {
2123
maxReadSpeed?: number;
2224
/** if true, log all read pages into the `readPages` field for debugging */
2325
logPageReads?: boolean;
26+
/** if defined, this is called once per request and passed the number of bytes about to be requested **/
27+
requestLimiter?: RequestLimiter;
2428
};
2529
export type PageReadLog = {
2630
pageno: number;
@@ -46,6 +50,7 @@ export class LazyUint8Array {
4650
private readonly maxSpeed: number;
4751
private readonly maxReadHeads: number;
4852
private readonly logPageReads: boolean;
53+
private readonly requestLimiter: RequestLimiter;
4954

5055
constructor(config: LazyFileConfig) {
5156
this._chunkSize = config.requestChunkSize;
@@ -58,6 +63,7 @@ export class LazyUint8Array {
5863
if (config.fileLength) {
5964
this._length = config.fileLength;
6065
}
66+
this.requestLimiter = config.requestLimiter == null ? ((ignored) => {}) : config.requestLimiter;
6167
}
6268
/**
6369
* efficiently copy the range [start, start + length) from the http file into the
@@ -228,6 +234,7 @@ export class LazyUint8Array {
228234
absoluteFrom / 1024
229235
} KiB]`
230236
);
237+
this.requestLimiter(absoluteTo - absoluteFrom);
231238
this.totalFetchedBytes += absoluteTo - absoluteFrom;
232239
this.totalRequests++;
233240
if (absoluteFrom > absoluteTo)

src/sqlite.worker.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,33 @@ const mod = {
127127
db: null as null | LazyHttpDatabase,
128128
inited: false,
129129
sqljs: null as null | Promise<any>,
130+
bytesRead: 0,
130131
async SplitFileHttpDatabase(
131132
wasmUrl: string,
132133
configs: SplitFileConfig[],
133-
mainVirtualFilename?: string
134+
mainVirtualFilename?: string,
135+
maxBytesToRead: number = Infinity,
134136
): Promise<LazyHttpDatabase> {
135137
if (this.inited) throw Error(`sorry, only one db is supported right now`);
136138
this.inited = true;
137139
if (!this.sqljs) {
138140
this.sqljs = init(wasmUrl);
139141
}
140142
const sql = await this.sqljs;
143+
144+
this.bytesRead = 0;
145+
let requestLimiter = (bytes: number) => {
146+
if (this.bytesRead + bytes > maxBytesToRead) {
147+
this.bytesRead = 0;
148+
// I couldn't figure out how to get ERRNO_CODES included
149+
// so just hardcode the actual value
150+
// https://github.com/emscripten-core/emscripten/blob/565fb3651ed185078df1a13b8edbcb6b2192f29e/system/include/wasi/api.h#L146
151+
// https://github.com/emscripten-core/emscripten/blob/565fb3651ed185078df1a13b8edbcb6b2192f29e/system/lib/libc/musl/arch/emscripten/bits/errno.h#L13
152+
throw new sql.FS.ErrnoError(6 /* EAGAIN */);
153+
}
154+
this.bytesRead += bytes;
155+
};
156+
141157
const lazyFiles = new Map();
142158
const hydratedConfigs = await fetchConfigs(configs);
143159
let mainFileConfig;
@@ -182,7 +198,8 @@ const mod = {
182198
? config.databaseLengthBytes
183199
: undefined,
184200
logPageReads: true,
185-
maxReadHeads: 3
201+
maxReadHeads: 3,
202+
requestLimiter
186203
});
187204
lazyFiles.set(filename, lazyFile);
188205
}
@@ -197,7 +214,7 @@ const mod = {
197214
`Chunk size does not match page size: pragma page_size = ${pageSize} but chunkSize = ${mainFileConfig.requestChunkSize}`
198215
);
199216
}
200-
217+
201218
this.db.lazyFiles = lazyFiles;
202219
this.db.create_vtab(SeriesVtab);
203220
this.db.query = (...args) => toObjects(this.db!.exec(...args));

0 commit comments

Comments
 (0)