beam.com 0.1.0-rc.3 → 0.1.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/runtime/cloudflare/durable.js +17 -2
- package/runtime/cloudflare/index.js +2 -2
- package/runtime/cloudflare/worker.js +112 -20
- package/runtime/deno/worker.js +112 -20
- package/runtime/durable.js +17 -2
- package/runtime/page/runtime-id.js +1 -1
- package/runtime/page/worker.js +112 -20
- package/runtime/release.json +1 -1
- package/runtime/runtime-id.js +1 -1
- package/runtime/worker.js +112 -20
package/README.md
CHANGED
|
@@ -219,7 +219,7 @@ jobs:
|
|
|
219
219
|
|
|
220
220
|
Then turn on Pages one time: Settings, Pages, Source "GitHub Actions". The
|
|
221
221
|
`GITHUB_TOKEN` cannot do this step. Until the release `v0.1.0`, use the
|
|
222
|
-
release candidate `@v0.1.0-rc.
|
|
222
|
+
release candidate `@v0.1.0-rc.5` in place of `@v0.1.0`. With `@main`, the
|
|
223
223
|
workflow uses the `edge` build of `beam.com`.
|
|
224
224
|
|
|
225
225
|
The workflow ([`pages-app.yml`](.github/workflows/pages-app.yml)) builds
|
|
@@ -286,7 +286,7 @@ Each GitHub release from 0.1.0-rc.2 also holds the package, the same
|
|
|
286
286
|
file as on npm. npm installs it with no registry:
|
|
287
287
|
|
|
288
288
|
```sh
|
|
289
|
-
npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.
|
|
289
|
+
npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.5/beam.com-0.1.0-rc.5.tgz
|
|
290
290
|
```
|
|
291
291
|
|
|
292
292
|
- `BEAM_COM`: the path of a `beam.com` to use. Then nothing is
|
package/package.json
CHANGED
|
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
|
|
|
36
36
|
import { Vm } from './worker.js';
|
|
37
37
|
|
|
38
38
|
const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
|
|
39
|
+
|
|
40
|
+
// The request to the object stub. workerd pipes the body from the client
|
|
41
|
+
// to the object, and that pipe reads on after the response has gone when
|
|
42
|
+
// the object stops its read early. The read then fails with no handler:
|
|
43
|
+
// "Uncaught TypeError: Can't read from request stream after response has
|
|
44
|
+
// been sent". Here the pipe is one of this Worker, with a handler. A
|
|
45
|
+
// FixedLengthStream (workerd) keeps the length of the body.
|
|
46
|
+
export function forward(stub, request) {
|
|
47
|
+
if (!request.body) return stub.fetch(request);
|
|
48
|
+
const length = Number(request.headers.get('content-length') ?? NaN);
|
|
49
|
+
const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
|
|
50
|
+
? new FixedLengthStream(length) : new TransformStream();
|
|
51
|
+
request.body.pipeTo(pass.writable).catch(() => {});
|
|
52
|
+
return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
|
|
53
|
+
}
|
|
39
54
|
const REGISTRY = '.registry';
|
|
40
55
|
const EXPIRES = 'beam:expires';
|
|
41
56
|
|
|
@@ -363,7 +378,7 @@ export default {
|
|
|
363
378
|
// The app can trust the header: a client cannot give it.
|
|
364
379
|
request = new Request(inner, request);
|
|
365
380
|
request.headers.set('x-beam-tenant', m[1]);
|
|
366
|
-
const response = await env.BEAM.get(env.BEAM.idFromName(m[1]))
|
|
381
|
+
const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
|
|
367
382
|
if (!response.headers.has('service-worker-allowed')) return response;
|
|
368
383
|
const out = new Response(response.body, response);
|
|
369
384
|
out.headers.delete('service-worker-allowed');
|
|
@@ -386,6 +401,6 @@ export default {
|
|
|
386
401
|
request = new Request(request);
|
|
387
402
|
if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
|
|
388
403
|
else request.headers.delete('x-beam-tenant');
|
|
389
|
-
return env.BEAM.get(env.BEAM.idFromName(name))
|
|
404
|
+
return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
|
|
390
405
|
},
|
|
391
406
|
};
|
|
@@ -28,7 +28,7 @@ import { env as globalEnv } from 'cloudflare:workers';
|
|
|
28
28
|
import { release, use } from './release.js';
|
|
29
29
|
import { useSnapshot } from './snapshot.js';
|
|
30
30
|
import plain, { parseSnapshot, staticResponse, Vm } from './worker.js';
|
|
31
|
-
import front from './durable.js';
|
|
31
|
+
import front, { forward } from './durable.js';
|
|
32
32
|
|
|
33
33
|
export { Beam } from './durable.js';
|
|
34
34
|
|
|
@@ -113,7 +113,7 @@ export function serve(app, { binding = 'BEAM', name, nifs = null, snapshot = nul
|
|
|
113
113
|
const objects = env[binding];
|
|
114
114
|
if (!objects && vm) return vm.fetch(request, ctx);
|
|
115
115
|
if (!objects) return plain.fetch(request, env, ctx);
|
|
116
|
-
if (typeof name === 'function') return objects.getByName(name(request))
|
|
116
|
+
if (typeof name === 'function') return forward(objects.getByName(name(request)), request);
|
|
117
117
|
return front.fetch(request, frontEnv(env), ctx);
|
|
118
118
|
},
|
|
119
119
|
scheduled(controller, env, ctx) {
|
|
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
|
|
|
285
285
|
// BEAM_ERL_FLAGS as one text, with one space between the flags.
|
|
286
286
|
export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
287
287
|
|
|
288
|
+
// The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
|
|
289
|
+
// flag wins). A binary or a heap of 512 KB or more got its own carrier,
|
|
290
|
+
// and in WebAssembly these carriers made the memory of the VM grow far
|
|
291
|
+
// above the memory that Erlang used (14 MB): 16 clients that sent bodies
|
|
292
|
+
// of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
|
|
293
|
+
// was about 200 MB. With -Mea min, the allocators of ERTS are off, and
|
|
294
|
+
// these flags do not apply.
|
|
295
|
+
export function allocFlags(flags) {
|
|
296
|
+
return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
// BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
|
|
289
300
|
// "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
|
|
290
301
|
// hosts, as "*:443"). The host resolves a name, so the VM cannot reach
|
|
@@ -1340,7 +1351,7 @@ export class Vm {
|
|
|
1340
1351
|
// Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
|
|
1341
1352
|
// not supported).
|
|
1342
1353
|
const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
|
|
1343
|
-
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1354
|
+
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1344
1355
|
'-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
|
|
1345
1356
|
'-home', '/', ...args, '-noshell');
|
|
1346
1357
|
// Distributed Erlang over wasm_tcp, with no epmd (all nodes on
|
|
@@ -1664,12 +1675,20 @@ export class Vm {
|
|
|
1664
1675
|
if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
|
|
1665
1676
|
let finished = () => {};
|
|
1666
1677
|
const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
|
|
1678
|
+
let response;
|
|
1667
1679
|
try {
|
|
1668
|
-
|
|
1680
|
+
response = await this.request(request, h, finished);
|
|
1669
1681
|
} catch (e) {
|
|
1670
1682
|
finished();
|
|
1671
1683
|
throw e;
|
|
1672
1684
|
}
|
|
1685
|
+
// A response before the VM read the body (a limit of the host, a VM
|
|
1686
|
+
// that stopped): the body goes to no reader (see drain).
|
|
1687
|
+
if (request.body && !request.body.locked) {
|
|
1688
|
+
const reader = request.body.getReader();
|
|
1689
|
+
await drain(() => reader.read(), () => reader.cancel());
|
|
1690
|
+
}
|
|
1691
|
+
return response;
|
|
1673
1692
|
}
|
|
1674
1693
|
|
|
1675
1694
|
async request(request, h, finished) {
|
|
@@ -1953,8 +1972,9 @@ export class Vm {
|
|
|
1953
1972
|
finished();
|
|
1954
1973
|
return this.stopped();
|
|
1955
1974
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1975
|
+
let c;
|
|
1976
|
+
const response = await new Promise((resolve) => {
|
|
1977
|
+
c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
|
|
1958
1978
|
if (h) h.sockets++;
|
|
1959
1979
|
this.conns.add(c);
|
|
1960
1980
|
c.timeout = timeout;
|
|
@@ -1966,20 +1986,28 @@ export class Vm {
|
|
|
1966
1986
|
this.tcps.set(id, {
|
|
1967
1987
|
send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
|
|
1968
1988
|
close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
|
|
1969
|
-
ack: (n) => this.bridgeRead(c, n), h,
|
|
1989
|
+
ack: (n, msg) => this.bridgeRead(c, n, msg), h,
|
|
1970
1990
|
});
|
|
1971
1991
|
// ack: wasm_tcp tells the host what the app read (tcp_read): the
|
|
1972
1992
|
// body of the request, and the messages of a WebSocket. sent: the
|
|
1973
1993
|
// host tells wasm_tcp what the client took (tcp_sent).
|
|
1974
1994
|
this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
|
|
1975
|
-
|
|
1976
|
-
|
|
1995
|
+
// With a body, the head goes in the event of the first part of the
|
|
1996
|
+
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1998
|
+
else this.bridgeSend(c, bytes);
|
|
1977
1999
|
});
|
|
2000
|
+
// A response before the end of the body: the rest of the body goes to
|
|
2001
|
+
// the app while it reads, else bridgeUpload reads and drops it (see
|
|
2002
|
+
// drain). The response waits for that, DRAIN_TIME ms at most.
|
|
2003
|
+
if (c.upload) await within(c.upload, DRAIN_TIME);
|
|
2004
|
+
return response;
|
|
1978
2005
|
}
|
|
1979
2006
|
|
|
1980
2007
|
// Bytes to the app on the connection c.
|
|
1981
2008
|
bridgeSend(c, bytes) {
|
|
1982
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1983
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1984
2012
|
}
|
|
1985
2013
|
|
|
@@ -1987,8 +2015,13 @@ export class Vm {
|
|
|
1987
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1988
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1989
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1990
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1991
|
-
|
|
2018
|
+
// the memory of the VM, and a slow app slows the client. While a recv of
|
|
2019
|
+
// the app waits for more bytes (c.need, as Bandit reads a body), a part
|
|
2020
|
+
// has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
|
|
2021
|
+
// each event costs a turn of the VM. The head of the request goes with
|
|
2022
|
+
// the bytes of the first read of the client, with no wait for
|
|
2023
|
+
// UPLOAD_PART bytes.
|
|
2024
|
+
async bridgeUpload(c, body, chunked, max, head) {
|
|
1992
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1993
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1994
2027
|
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
@@ -1997,7 +2030,7 @@ export class Vm {
|
|
|
1997
2030
|
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
1998
2031
|
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
1999
2032
|
const reader = byob ?? body.getReader();
|
|
2000
|
-
const read = byob ? () => byob.readAtLeast(
|
|
2033
|
+
const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2001
2034
|
let total = 0;
|
|
2002
2035
|
let part = [];
|
|
2003
2036
|
let size = 0;
|
|
@@ -2011,13 +2044,22 @@ export class Vm {
|
|
|
2011
2044
|
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2012
2045
|
part = [];
|
|
2013
2046
|
size = 0;
|
|
2014
|
-
|
|
2047
|
+
out(chunked ? chunk(bytes) : bytes);
|
|
2015
2048
|
return true;
|
|
2016
2049
|
};
|
|
2050
|
+
// Bytes to the app, after the head when it did not go yet.
|
|
2051
|
+
const out = (bytes) => {
|
|
2052
|
+
clearTimeout(alone);
|
|
2053
|
+
this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
|
|
2054
|
+
head = null;
|
|
2055
|
+
};
|
|
2056
|
+
// A client that sends no body yet: the head goes alone after HEAD_WAIT
|
|
2057
|
+
// ms, so the app sees the request (and its own time limits run).
|
|
2058
|
+
const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
|
|
2017
2059
|
try {
|
|
2018
2060
|
for (;;) {
|
|
2019
2061
|
// A read can end the stream with its last bytes (done and a value).
|
|
2020
|
-
const { value, done } = await read();
|
|
2062
|
+
const { value, done } = await read(head ? 1 : UPLOAD_PART);
|
|
2021
2063
|
if (c.done || this.dead) break;
|
|
2022
2064
|
if (value?.length) {
|
|
2023
2065
|
total += value.length;
|
|
@@ -2033,21 +2075,26 @@ export class Vm {
|
|
|
2033
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2034
2076
|
part.push(piece);
|
|
2035
2077
|
size += piece.length;
|
|
2036
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2037
2079
|
}
|
|
2038
|
-
if (ok && done && size) ok = await flush();
|
|
2080
|
+
if (ok && (done || head) && size) ok = await flush();
|
|
2039
2081
|
if (!ok) break;
|
|
2040
2082
|
if (done) {
|
|
2041
|
-
if (chunked)
|
|
2083
|
+
if (chunked) out(CHUNKS_END);
|
|
2084
|
+
else if (head) out(new Uint8Array(0));
|
|
2042
2085
|
this.bridgeClock(c, c.timeout * 1000);
|
|
2043
2086
|
return;
|
|
2044
2087
|
}
|
|
2045
2088
|
}
|
|
2046
|
-
reader.cancel().catch(() => {});
|
|
2047
2089
|
} catch {
|
|
2048
2090
|
// The client stopped the upload.
|
|
2049
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2050
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2051
2098
|
}
|
|
2052
2099
|
|
|
2053
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2055,9 +2102,12 @@ export class Vm {
|
|
|
2055
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2056
2103
|
}
|
|
2057
2104
|
|
|
2058
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2059
|
-
|
|
2105
|
+
// The app read n bytes of the connection c (tcp_read). msg.want and
|
|
2106
|
+
// msg.got give the bytes that a recv of the app still waits for, beyond
|
|
2107
|
+
// the bytes on their way to it (c.need).
|
|
2108
|
+
bridgeRead(c, n, msg) {
|
|
2060
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2061
2111
|
this.bridgeRoom(c);
|
|
2062
2112
|
c.inbound?.flush();
|
|
2063
2113
|
}
|
|
@@ -2667,7 +2717,7 @@ export class Vm {
|
|
|
2667
2717
|
}
|
|
2668
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2669
2719
|
case 'tcp_read': {
|
|
2670
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2671
2721
|
break;
|
|
2672
2722
|
}
|
|
2673
2723
|
case 'tcp_close': {
|
|
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
|
|
|
2759
2809
|
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2760
2810
|
// costs a turn of the VM.
|
|
2761
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2762
|
-
const UPLOAD_PART =
|
|
2812
|
+
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2814
|
+
// The time that the head of a request waits for the first bytes of its
|
|
2815
|
+
// body (bridgeUpload), in ms.
|
|
2816
|
+
const HEAD_WAIT = 20;
|
|
2763
2817
|
const UPLOAD_READ = 64 * 1024;
|
|
2818
|
+
// The rest of a request body that the app did not read (drain): the
|
|
2819
|
+
// bytes that the host reads at most before the response goes, the ms with
|
|
2820
|
+
// no new bytes, and the ms in all.
|
|
2821
|
+
const DRAIN_BYTES = 64 * 1024 * 1024;
|
|
2822
|
+
const DRAIN_IDLE = 5000;
|
|
2823
|
+
const DRAIN_TIME = 60000;
|
|
2824
|
+
|
|
2825
|
+
// The rest of a request body, read and dropped before the response goes,
|
|
2826
|
+
// when the app answered before it read the whole body (a 413, for
|
|
2827
|
+
// example). workerd cannot read a request body after the response has
|
|
2828
|
+
// gone, and it then closes the connection with the bytes that were not
|
|
2829
|
+
// read. wrangler dev uses that connection again, and its next request
|
|
2830
|
+
// fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
|
|
2831
|
+
// with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
|
|
2832
|
+
// read() gives { value, done }. limits replaces the bounds (a test).
|
|
2833
|
+
export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
|
|
2834
|
+
const end = Date.now() + time;
|
|
2835
|
+
let n = 0;
|
|
2836
|
+
try {
|
|
2837
|
+
for (;;) {
|
|
2838
|
+
const r = await within(read(), Math.min(idle, end - Date.now()));
|
|
2839
|
+
if (r === LATE) break;
|
|
2840
|
+
if (r.done) return;
|
|
2841
|
+
n += r.value?.byteLength ?? 0;
|
|
2842
|
+
if (n > bytes || Date.now() >= end) break;
|
|
2843
|
+
}
|
|
2844
|
+
} catch {
|
|
2845
|
+
return; // the client stopped the body
|
|
2846
|
+
}
|
|
2847
|
+
try { await cancel(); } catch {}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// The promise p, or LATE after ms ms.
|
|
2851
|
+
function within(p, ms) {
|
|
2852
|
+
let timer;
|
|
2853
|
+
const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
|
|
2854
|
+
return Promise.race([p, late]).finally(() => clearTimeout(timer));
|
|
2855
|
+
}
|
|
2764
2856
|
|
|
2765
2857
|
// The size bytes of the list parts, as one array.
|
|
2766
2858
|
function join(parts, size) {
|
package/runtime/deno/worker.js
CHANGED
|
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
|
|
|
285
285
|
// BEAM_ERL_FLAGS as one text, with one space between the flags.
|
|
286
286
|
export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
287
287
|
|
|
288
|
+
// The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
|
|
289
|
+
// flag wins). A binary or a heap of 512 KB or more got its own carrier,
|
|
290
|
+
// and in WebAssembly these carriers made the memory of the VM grow far
|
|
291
|
+
// above the memory that Erlang used (14 MB): 16 clients that sent bodies
|
|
292
|
+
// of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
|
|
293
|
+
// was about 200 MB. With -Mea min, the allocators of ERTS are off, and
|
|
294
|
+
// these flags do not apply.
|
|
295
|
+
export function allocFlags(flags) {
|
|
296
|
+
return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
// BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
|
|
289
300
|
// "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
|
|
290
301
|
// hosts, as "*:443"). The host resolves a name, so the VM cannot reach
|
|
@@ -1340,7 +1351,7 @@ export class Vm {
|
|
|
1340
1351
|
// Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
|
|
1341
1352
|
// not supported).
|
|
1342
1353
|
const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
|
|
1343
|
-
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1354
|
+
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1344
1355
|
'-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
|
|
1345
1356
|
'-home', '/', ...args, '-noshell');
|
|
1346
1357
|
// Distributed Erlang over wasm_tcp, with no epmd (all nodes on
|
|
@@ -1664,12 +1675,20 @@ export class Vm {
|
|
|
1664
1675
|
if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
|
|
1665
1676
|
let finished = () => {};
|
|
1666
1677
|
const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
|
|
1678
|
+
let response;
|
|
1667
1679
|
try {
|
|
1668
|
-
|
|
1680
|
+
response = await this.request(request, h, finished);
|
|
1669
1681
|
} catch (e) {
|
|
1670
1682
|
finished();
|
|
1671
1683
|
throw e;
|
|
1672
1684
|
}
|
|
1685
|
+
// A response before the VM read the body (a limit of the host, a VM
|
|
1686
|
+
// that stopped): the body goes to no reader (see drain).
|
|
1687
|
+
if (request.body && !request.body.locked) {
|
|
1688
|
+
const reader = request.body.getReader();
|
|
1689
|
+
await drain(() => reader.read(), () => reader.cancel());
|
|
1690
|
+
}
|
|
1691
|
+
return response;
|
|
1673
1692
|
}
|
|
1674
1693
|
|
|
1675
1694
|
async request(request, h, finished) {
|
|
@@ -1953,8 +1972,9 @@ export class Vm {
|
|
|
1953
1972
|
finished();
|
|
1954
1973
|
return this.stopped();
|
|
1955
1974
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1975
|
+
let c;
|
|
1976
|
+
const response = await new Promise((resolve) => {
|
|
1977
|
+
c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
|
|
1958
1978
|
if (h) h.sockets++;
|
|
1959
1979
|
this.conns.add(c);
|
|
1960
1980
|
c.timeout = timeout;
|
|
@@ -1966,20 +1986,28 @@ export class Vm {
|
|
|
1966
1986
|
this.tcps.set(id, {
|
|
1967
1987
|
send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
|
|
1968
1988
|
close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
|
|
1969
|
-
ack: (n) => this.bridgeRead(c, n), h,
|
|
1989
|
+
ack: (n, msg) => this.bridgeRead(c, n, msg), h,
|
|
1970
1990
|
});
|
|
1971
1991
|
// ack: wasm_tcp tells the host what the app read (tcp_read): the
|
|
1972
1992
|
// body of the request, and the messages of a WebSocket. sent: the
|
|
1973
1993
|
// host tells wasm_tcp what the client took (tcp_sent).
|
|
1974
1994
|
this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
|
|
1975
|
-
|
|
1976
|
-
|
|
1995
|
+
// With a body, the head goes in the event of the first part of the
|
|
1996
|
+
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1998
|
+
else this.bridgeSend(c, bytes);
|
|
1977
1999
|
});
|
|
2000
|
+
// A response before the end of the body: the rest of the body goes to
|
|
2001
|
+
// the app while it reads, else bridgeUpload reads and drops it (see
|
|
2002
|
+
// drain). The response waits for that, DRAIN_TIME ms at most.
|
|
2003
|
+
if (c.upload) await within(c.upload, DRAIN_TIME);
|
|
2004
|
+
return response;
|
|
1978
2005
|
}
|
|
1979
2006
|
|
|
1980
2007
|
// Bytes to the app on the connection c.
|
|
1981
2008
|
bridgeSend(c, bytes) {
|
|
1982
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1983
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1984
2012
|
}
|
|
1985
2013
|
|
|
@@ -1987,8 +2015,13 @@ export class Vm {
|
|
|
1987
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1988
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1989
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1990
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1991
|
-
|
|
2018
|
+
// the memory of the VM, and a slow app slows the client. While a recv of
|
|
2019
|
+
// the app waits for more bytes (c.need, as Bandit reads a body), a part
|
|
2020
|
+
// has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
|
|
2021
|
+
// each event costs a turn of the VM. The head of the request goes with
|
|
2022
|
+
// the bytes of the first read of the client, with no wait for
|
|
2023
|
+
// UPLOAD_PART bytes.
|
|
2024
|
+
async bridgeUpload(c, body, chunked, max, head) {
|
|
1992
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1993
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1994
2027
|
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
@@ -1997,7 +2030,7 @@ export class Vm {
|
|
|
1997
2030
|
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
1998
2031
|
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
1999
2032
|
const reader = byob ?? body.getReader();
|
|
2000
|
-
const read = byob ? () => byob.readAtLeast(
|
|
2033
|
+
const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2001
2034
|
let total = 0;
|
|
2002
2035
|
let part = [];
|
|
2003
2036
|
let size = 0;
|
|
@@ -2011,13 +2044,22 @@ export class Vm {
|
|
|
2011
2044
|
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2012
2045
|
part = [];
|
|
2013
2046
|
size = 0;
|
|
2014
|
-
|
|
2047
|
+
out(chunked ? chunk(bytes) : bytes);
|
|
2015
2048
|
return true;
|
|
2016
2049
|
};
|
|
2050
|
+
// Bytes to the app, after the head when it did not go yet.
|
|
2051
|
+
const out = (bytes) => {
|
|
2052
|
+
clearTimeout(alone);
|
|
2053
|
+
this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
|
|
2054
|
+
head = null;
|
|
2055
|
+
};
|
|
2056
|
+
// A client that sends no body yet: the head goes alone after HEAD_WAIT
|
|
2057
|
+
// ms, so the app sees the request (and its own time limits run).
|
|
2058
|
+
const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
|
|
2017
2059
|
try {
|
|
2018
2060
|
for (;;) {
|
|
2019
2061
|
// A read can end the stream with its last bytes (done and a value).
|
|
2020
|
-
const { value, done } = await read();
|
|
2062
|
+
const { value, done } = await read(head ? 1 : UPLOAD_PART);
|
|
2021
2063
|
if (c.done || this.dead) break;
|
|
2022
2064
|
if (value?.length) {
|
|
2023
2065
|
total += value.length;
|
|
@@ -2033,21 +2075,26 @@ export class Vm {
|
|
|
2033
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2034
2076
|
part.push(piece);
|
|
2035
2077
|
size += piece.length;
|
|
2036
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2037
2079
|
}
|
|
2038
|
-
if (ok && done && size) ok = await flush();
|
|
2080
|
+
if (ok && (done || head) && size) ok = await flush();
|
|
2039
2081
|
if (!ok) break;
|
|
2040
2082
|
if (done) {
|
|
2041
|
-
if (chunked)
|
|
2083
|
+
if (chunked) out(CHUNKS_END);
|
|
2084
|
+
else if (head) out(new Uint8Array(0));
|
|
2042
2085
|
this.bridgeClock(c, c.timeout * 1000);
|
|
2043
2086
|
return;
|
|
2044
2087
|
}
|
|
2045
2088
|
}
|
|
2046
|
-
reader.cancel().catch(() => {});
|
|
2047
2089
|
} catch {
|
|
2048
2090
|
// The client stopped the upload.
|
|
2049
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2050
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2051
2098
|
}
|
|
2052
2099
|
|
|
2053
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2055,9 +2102,12 @@ export class Vm {
|
|
|
2055
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2056
2103
|
}
|
|
2057
2104
|
|
|
2058
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2059
|
-
|
|
2105
|
+
// The app read n bytes of the connection c (tcp_read). msg.want and
|
|
2106
|
+
// msg.got give the bytes that a recv of the app still waits for, beyond
|
|
2107
|
+
// the bytes on their way to it (c.need).
|
|
2108
|
+
bridgeRead(c, n, msg) {
|
|
2060
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2061
2111
|
this.bridgeRoom(c);
|
|
2062
2112
|
c.inbound?.flush();
|
|
2063
2113
|
}
|
|
@@ -2667,7 +2717,7 @@ export class Vm {
|
|
|
2667
2717
|
}
|
|
2668
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2669
2719
|
case 'tcp_read': {
|
|
2670
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2671
2721
|
break;
|
|
2672
2722
|
}
|
|
2673
2723
|
case 'tcp_close': {
|
|
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
|
|
|
2759
2809
|
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2760
2810
|
// costs a turn of the VM.
|
|
2761
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2762
|
-
const UPLOAD_PART =
|
|
2812
|
+
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2814
|
+
// The time that the head of a request waits for the first bytes of its
|
|
2815
|
+
// body (bridgeUpload), in ms.
|
|
2816
|
+
const HEAD_WAIT = 20;
|
|
2763
2817
|
const UPLOAD_READ = 64 * 1024;
|
|
2818
|
+
// The rest of a request body that the app did not read (drain): the
|
|
2819
|
+
// bytes that the host reads at most before the response goes, the ms with
|
|
2820
|
+
// no new bytes, and the ms in all.
|
|
2821
|
+
const DRAIN_BYTES = 64 * 1024 * 1024;
|
|
2822
|
+
const DRAIN_IDLE = 5000;
|
|
2823
|
+
const DRAIN_TIME = 60000;
|
|
2824
|
+
|
|
2825
|
+
// The rest of a request body, read and dropped before the response goes,
|
|
2826
|
+
// when the app answered before it read the whole body (a 413, for
|
|
2827
|
+
// example). workerd cannot read a request body after the response has
|
|
2828
|
+
// gone, and it then closes the connection with the bytes that were not
|
|
2829
|
+
// read. wrangler dev uses that connection again, and its next request
|
|
2830
|
+
// fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
|
|
2831
|
+
// with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
|
|
2832
|
+
// read() gives { value, done }. limits replaces the bounds (a test).
|
|
2833
|
+
export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
|
|
2834
|
+
const end = Date.now() + time;
|
|
2835
|
+
let n = 0;
|
|
2836
|
+
try {
|
|
2837
|
+
for (;;) {
|
|
2838
|
+
const r = await within(read(), Math.min(idle, end - Date.now()));
|
|
2839
|
+
if (r === LATE) break;
|
|
2840
|
+
if (r.done) return;
|
|
2841
|
+
n += r.value?.byteLength ?? 0;
|
|
2842
|
+
if (n > bytes || Date.now() >= end) break;
|
|
2843
|
+
}
|
|
2844
|
+
} catch {
|
|
2845
|
+
return; // the client stopped the body
|
|
2846
|
+
}
|
|
2847
|
+
try { await cancel(); } catch {}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// The promise p, or LATE after ms ms.
|
|
2851
|
+
function within(p, ms) {
|
|
2852
|
+
let timer;
|
|
2853
|
+
const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
|
|
2854
|
+
return Promise.race([p, late]).finally(() => clearTimeout(timer));
|
|
2855
|
+
}
|
|
2764
2856
|
|
|
2765
2857
|
// The size bytes of the list parts, as one array.
|
|
2766
2858
|
function join(parts, size) {
|
package/runtime/durable.js
CHANGED
|
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
|
|
|
36
36
|
import { Vm } from './worker.js';
|
|
37
37
|
|
|
38
38
|
const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
|
|
39
|
+
|
|
40
|
+
// The request to the object stub. workerd pipes the body from the client
|
|
41
|
+
// to the object, and that pipe reads on after the response has gone when
|
|
42
|
+
// the object stops its read early. The read then fails with no handler:
|
|
43
|
+
// "Uncaught TypeError: Can't read from request stream after response has
|
|
44
|
+
// been sent". Here the pipe is one of this Worker, with a handler. A
|
|
45
|
+
// FixedLengthStream (workerd) keeps the length of the body.
|
|
46
|
+
export function forward(stub, request) {
|
|
47
|
+
if (!request.body) return stub.fetch(request);
|
|
48
|
+
const length = Number(request.headers.get('content-length') ?? NaN);
|
|
49
|
+
const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
|
|
50
|
+
? new FixedLengthStream(length) : new TransformStream();
|
|
51
|
+
request.body.pipeTo(pass.writable).catch(() => {});
|
|
52
|
+
return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
|
|
53
|
+
}
|
|
39
54
|
const REGISTRY = '.registry';
|
|
40
55
|
const EXPIRES = 'beam:expires';
|
|
41
56
|
|
|
@@ -363,7 +378,7 @@ export default {
|
|
|
363
378
|
// The app can trust the header: a client cannot give it.
|
|
364
379
|
request = new Request(inner, request);
|
|
365
380
|
request.headers.set('x-beam-tenant', m[1]);
|
|
366
|
-
const response = await env.BEAM.get(env.BEAM.idFromName(m[1]))
|
|
381
|
+
const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
|
|
367
382
|
if (!response.headers.has('service-worker-allowed')) return response;
|
|
368
383
|
const out = new Response(response.body, response);
|
|
369
384
|
out.headers.delete('service-worker-allowed');
|
|
@@ -386,6 +401,6 @@ export default {
|
|
|
386
401
|
request = new Request(request);
|
|
387
402
|
if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
|
|
388
403
|
else request.headers.delete('x-beam-tenant');
|
|
389
|
-
return env.BEAM.get(env.BEAM.idFromName(name))
|
|
404
|
+
return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
|
|
390
405
|
},
|
|
391
406
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
|
|
2
2
|
// the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
|
|
3
|
-
export default '
|
|
3
|
+
export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
|
package/runtime/page/worker.js
CHANGED
|
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
|
|
|
285
285
|
// BEAM_ERL_FLAGS as one text, with one space between the flags.
|
|
286
286
|
export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
287
287
|
|
|
288
|
+
// The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
|
|
289
|
+
// flag wins). A binary or a heap of 512 KB or more got its own carrier,
|
|
290
|
+
// and in WebAssembly these carriers made the memory of the VM grow far
|
|
291
|
+
// above the memory that Erlang used (14 MB): 16 clients that sent bodies
|
|
292
|
+
// of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
|
|
293
|
+
// was about 200 MB. With -Mea min, the allocators of ERTS are off, and
|
|
294
|
+
// these flags do not apply.
|
|
295
|
+
export function allocFlags(flags) {
|
|
296
|
+
return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
// BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
|
|
289
300
|
// "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
|
|
290
301
|
// hosts, as "*:443"). The host resolves a name, so the VM cannot reach
|
|
@@ -1340,7 +1351,7 @@ export class Vm {
|
|
|
1340
1351
|
// Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
|
|
1341
1352
|
// not supported).
|
|
1342
1353
|
const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
|
|
1343
|
-
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1354
|
+
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1344
1355
|
'-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
|
|
1345
1356
|
'-home', '/', ...args, '-noshell');
|
|
1346
1357
|
// Distributed Erlang over wasm_tcp, with no epmd (all nodes on
|
|
@@ -1664,12 +1675,20 @@ export class Vm {
|
|
|
1664
1675
|
if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
|
|
1665
1676
|
let finished = () => {};
|
|
1666
1677
|
const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
|
|
1678
|
+
let response;
|
|
1667
1679
|
try {
|
|
1668
|
-
|
|
1680
|
+
response = await this.request(request, h, finished);
|
|
1669
1681
|
} catch (e) {
|
|
1670
1682
|
finished();
|
|
1671
1683
|
throw e;
|
|
1672
1684
|
}
|
|
1685
|
+
// A response before the VM read the body (a limit of the host, a VM
|
|
1686
|
+
// that stopped): the body goes to no reader (see drain).
|
|
1687
|
+
if (request.body && !request.body.locked) {
|
|
1688
|
+
const reader = request.body.getReader();
|
|
1689
|
+
await drain(() => reader.read(), () => reader.cancel());
|
|
1690
|
+
}
|
|
1691
|
+
return response;
|
|
1673
1692
|
}
|
|
1674
1693
|
|
|
1675
1694
|
async request(request, h, finished) {
|
|
@@ -1953,8 +1972,9 @@ export class Vm {
|
|
|
1953
1972
|
finished();
|
|
1954
1973
|
return this.stopped();
|
|
1955
1974
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1975
|
+
let c;
|
|
1976
|
+
const response = await new Promise((resolve) => {
|
|
1977
|
+
c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
|
|
1958
1978
|
if (h) h.sockets++;
|
|
1959
1979
|
this.conns.add(c);
|
|
1960
1980
|
c.timeout = timeout;
|
|
@@ -1966,20 +1986,28 @@ export class Vm {
|
|
|
1966
1986
|
this.tcps.set(id, {
|
|
1967
1987
|
send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
|
|
1968
1988
|
close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
|
|
1969
|
-
ack: (n) => this.bridgeRead(c, n), h,
|
|
1989
|
+
ack: (n, msg) => this.bridgeRead(c, n, msg), h,
|
|
1970
1990
|
});
|
|
1971
1991
|
// ack: wasm_tcp tells the host what the app read (tcp_read): the
|
|
1972
1992
|
// body of the request, and the messages of a WebSocket. sent: the
|
|
1973
1993
|
// host tells wasm_tcp what the client took (tcp_sent).
|
|
1974
1994
|
this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
|
|
1975
|
-
|
|
1976
|
-
|
|
1995
|
+
// With a body, the head goes in the event of the first part of the
|
|
1996
|
+
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1998
|
+
else this.bridgeSend(c, bytes);
|
|
1977
1999
|
});
|
|
2000
|
+
// A response before the end of the body: the rest of the body goes to
|
|
2001
|
+
// the app while it reads, else bridgeUpload reads and drops it (see
|
|
2002
|
+
// drain). The response waits for that, DRAIN_TIME ms at most.
|
|
2003
|
+
if (c.upload) await within(c.upload, DRAIN_TIME);
|
|
2004
|
+
return response;
|
|
1978
2005
|
}
|
|
1979
2006
|
|
|
1980
2007
|
// Bytes to the app on the connection c.
|
|
1981
2008
|
bridgeSend(c, bytes) {
|
|
1982
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1983
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1984
2012
|
}
|
|
1985
2013
|
|
|
@@ -1987,8 +2015,13 @@ export class Vm {
|
|
|
1987
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1988
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1989
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1990
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1991
|
-
|
|
2018
|
+
// the memory of the VM, and a slow app slows the client. While a recv of
|
|
2019
|
+
// the app waits for more bytes (c.need, as Bandit reads a body), a part
|
|
2020
|
+
// has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
|
|
2021
|
+
// each event costs a turn of the VM. The head of the request goes with
|
|
2022
|
+
// the bytes of the first read of the client, with no wait for
|
|
2023
|
+
// UPLOAD_PART bytes.
|
|
2024
|
+
async bridgeUpload(c, body, chunked, max, head) {
|
|
1992
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1993
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1994
2027
|
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
@@ -1997,7 +2030,7 @@ export class Vm {
|
|
|
1997
2030
|
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
1998
2031
|
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
1999
2032
|
const reader = byob ?? body.getReader();
|
|
2000
|
-
const read = byob ? () => byob.readAtLeast(
|
|
2033
|
+
const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2001
2034
|
let total = 0;
|
|
2002
2035
|
let part = [];
|
|
2003
2036
|
let size = 0;
|
|
@@ -2011,13 +2044,22 @@ export class Vm {
|
|
|
2011
2044
|
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2012
2045
|
part = [];
|
|
2013
2046
|
size = 0;
|
|
2014
|
-
|
|
2047
|
+
out(chunked ? chunk(bytes) : bytes);
|
|
2015
2048
|
return true;
|
|
2016
2049
|
};
|
|
2050
|
+
// Bytes to the app, after the head when it did not go yet.
|
|
2051
|
+
const out = (bytes) => {
|
|
2052
|
+
clearTimeout(alone);
|
|
2053
|
+
this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
|
|
2054
|
+
head = null;
|
|
2055
|
+
};
|
|
2056
|
+
// A client that sends no body yet: the head goes alone after HEAD_WAIT
|
|
2057
|
+
// ms, so the app sees the request (and its own time limits run).
|
|
2058
|
+
const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
|
|
2017
2059
|
try {
|
|
2018
2060
|
for (;;) {
|
|
2019
2061
|
// A read can end the stream with its last bytes (done and a value).
|
|
2020
|
-
const { value, done } = await read();
|
|
2062
|
+
const { value, done } = await read(head ? 1 : UPLOAD_PART);
|
|
2021
2063
|
if (c.done || this.dead) break;
|
|
2022
2064
|
if (value?.length) {
|
|
2023
2065
|
total += value.length;
|
|
@@ -2033,21 +2075,26 @@ export class Vm {
|
|
|
2033
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2034
2076
|
part.push(piece);
|
|
2035
2077
|
size += piece.length;
|
|
2036
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2037
2079
|
}
|
|
2038
|
-
if (ok && done && size) ok = await flush();
|
|
2080
|
+
if (ok && (done || head) && size) ok = await flush();
|
|
2039
2081
|
if (!ok) break;
|
|
2040
2082
|
if (done) {
|
|
2041
|
-
if (chunked)
|
|
2083
|
+
if (chunked) out(CHUNKS_END);
|
|
2084
|
+
else if (head) out(new Uint8Array(0));
|
|
2042
2085
|
this.bridgeClock(c, c.timeout * 1000);
|
|
2043
2086
|
return;
|
|
2044
2087
|
}
|
|
2045
2088
|
}
|
|
2046
|
-
reader.cancel().catch(() => {});
|
|
2047
2089
|
} catch {
|
|
2048
2090
|
// The client stopped the upload.
|
|
2049
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2050
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2051
2098
|
}
|
|
2052
2099
|
|
|
2053
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2055,9 +2102,12 @@ export class Vm {
|
|
|
2055
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2056
2103
|
}
|
|
2057
2104
|
|
|
2058
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2059
|
-
|
|
2105
|
+
// The app read n bytes of the connection c (tcp_read). msg.want and
|
|
2106
|
+
// msg.got give the bytes that a recv of the app still waits for, beyond
|
|
2107
|
+
// the bytes on their way to it (c.need).
|
|
2108
|
+
bridgeRead(c, n, msg) {
|
|
2060
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2061
2111
|
this.bridgeRoom(c);
|
|
2062
2112
|
c.inbound?.flush();
|
|
2063
2113
|
}
|
|
@@ -2667,7 +2717,7 @@ export class Vm {
|
|
|
2667
2717
|
}
|
|
2668
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2669
2719
|
case 'tcp_read': {
|
|
2670
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2671
2721
|
break;
|
|
2672
2722
|
}
|
|
2673
2723
|
case 'tcp_close': {
|
|
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
|
|
|
2759
2809
|
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2760
2810
|
// costs a turn of the VM.
|
|
2761
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2762
|
-
const UPLOAD_PART =
|
|
2812
|
+
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2814
|
+
// The time that the head of a request waits for the first bytes of its
|
|
2815
|
+
// body (bridgeUpload), in ms.
|
|
2816
|
+
const HEAD_WAIT = 20;
|
|
2763
2817
|
const UPLOAD_READ = 64 * 1024;
|
|
2818
|
+
// The rest of a request body that the app did not read (drain): the
|
|
2819
|
+
// bytes that the host reads at most before the response goes, the ms with
|
|
2820
|
+
// no new bytes, and the ms in all.
|
|
2821
|
+
const DRAIN_BYTES = 64 * 1024 * 1024;
|
|
2822
|
+
const DRAIN_IDLE = 5000;
|
|
2823
|
+
const DRAIN_TIME = 60000;
|
|
2824
|
+
|
|
2825
|
+
// The rest of a request body, read and dropped before the response goes,
|
|
2826
|
+
// when the app answered before it read the whole body (a 413, for
|
|
2827
|
+
// example). workerd cannot read a request body after the response has
|
|
2828
|
+
// gone, and it then closes the connection with the bytes that were not
|
|
2829
|
+
// read. wrangler dev uses that connection again, and its next request
|
|
2830
|
+
// fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
|
|
2831
|
+
// with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
|
|
2832
|
+
// read() gives { value, done }. limits replaces the bounds (a test).
|
|
2833
|
+
export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
|
|
2834
|
+
const end = Date.now() + time;
|
|
2835
|
+
let n = 0;
|
|
2836
|
+
try {
|
|
2837
|
+
for (;;) {
|
|
2838
|
+
const r = await within(read(), Math.min(idle, end - Date.now()));
|
|
2839
|
+
if (r === LATE) break;
|
|
2840
|
+
if (r.done) return;
|
|
2841
|
+
n += r.value?.byteLength ?? 0;
|
|
2842
|
+
if (n > bytes || Date.now() >= end) break;
|
|
2843
|
+
}
|
|
2844
|
+
} catch {
|
|
2845
|
+
return; // the client stopped the body
|
|
2846
|
+
}
|
|
2847
|
+
try { await cancel(); } catch {}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// The promise p, or LATE after ms ms.
|
|
2851
|
+
function within(p, ms) {
|
|
2852
|
+
let timer;
|
|
2853
|
+
const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
|
|
2854
|
+
return Promise.race([p, late]).finally(() => clearTimeout(timer));
|
|
2855
|
+
}
|
|
2764
2856
|
|
|
2765
2857
|
// The size bytes of the list parts, as one array.
|
|
2766
2858
|
function join(parts, size) {
|
package/runtime/release.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version": "0.1.0-rc.
|
|
1
|
+
{"version": "0.1.0-rc.5", "sha256": "ac36bab20b0cf8a6bcd9b9e02bae1e9d356855a75f4664ac65c58ff6fed8c024"}
|
package/runtime/runtime-id.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
|
|
2
2
|
// the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
|
|
3
|
-
export default '
|
|
3
|
+
export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
|
package/runtime/worker.js
CHANGED
|
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
|
|
|
285
285
|
// BEAM_ERL_FLAGS as one text, with one space between the flags.
|
|
286
286
|
export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
287
287
|
|
|
288
|
+
// The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
|
|
289
|
+
// flag wins). A binary or a heap of 512 KB or more got its own carrier,
|
|
290
|
+
// and in WebAssembly these carriers made the memory of the VM grow far
|
|
291
|
+
// above the memory that Erlang used (14 MB): 16 clients that sent bodies
|
|
292
|
+
// of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
|
|
293
|
+
// was about 200 MB. With -Mea min, the allocators of ERTS are off, and
|
|
294
|
+
// these flags do not apply.
|
|
295
|
+
export function allocFlags(flags) {
|
|
296
|
+
return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
// BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
|
|
289
300
|
// "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
|
|
290
301
|
// hosts, as "*:443"). The host resolves a name, so the VM cannot reach
|
|
@@ -1340,7 +1351,7 @@ export class Vm {
|
|
|
1340
1351
|
// Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
|
|
1341
1352
|
// not supported).
|
|
1342
1353
|
const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
|
|
1343
|
-
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1354
|
+
m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
|
|
1344
1355
|
'-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
|
|
1345
1356
|
'-home', '/', ...args, '-noshell');
|
|
1346
1357
|
// Distributed Erlang over wasm_tcp, with no epmd (all nodes on
|
|
@@ -1664,12 +1675,20 @@ export class Vm {
|
|
|
1664
1675
|
if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
|
|
1665
1676
|
let finished = () => {};
|
|
1666
1677
|
const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
|
|
1678
|
+
let response;
|
|
1667
1679
|
try {
|
|
1668
|
-
|
|
1680
|
+
response = await this.request(request, h, finished);
|
|
1669
1681
|
} catch (e) {
|
|
1670
1682
|
finished();
|
|
1671
1683
|
throw e;
|
|
1672
1684
|
}
|
|
1685
|
+
// A response before the VM read the body (a limit of the host, a VM
|
|
1686
|
+
// that stopped): the body goes to no reader (see drain).
|
|
1687
|
+
if (request.body && !request.body.locked) {
|
|
1688
|
+
const reader = request.body.getReader();
|
|
1689
|
+
await drain(() => reader.read(), () => reader.cancel());
|
|
1690
|
+
}
|
|
1691
|
+
return response;
|
|
1673
1692
|
}
|
|
1674
1693
|
|
|
1675
1694
|
async request(request, h, finished) {
|
|
@@ -1953,8 +1972,9 @@ export class Vm {
|
|
|
1953
1972
|
finished();
|
|
1954
1973
|
return this.stopped();
|
|
1955
1974
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1975
|
+
let c;
|
|
1976
|
+
const response = await new Promise((resolve) => {
|
|
1977
|
+
c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
|
|
1958
1978
|
if (h) h.sockets++;
|
|
1959
1979
|
this.conns.add(c);
|
|
1960
1980
|
c.timeout = timeout;
|
|
@@ -1966,20 +1986,28 @@ export class Vm {
|
|
|
1966
1986
|
this.tcps.set(id, {
|
|
1967
1987
|
send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
|
|
1968
1988
|
close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
|
|
1969
|
-
ack: (n) => this.bridgeRead(c, n), h,
|
|
1989
|
+
ack: (n, msg) => this.bridgeRead(c, n, msg), h,
|
|
1970
1990
|
});
|
|
1971
1991
|
// ack: wasm_tcp tells the host what the app read (tcp_read): the
|
|
1972
1992
|
// body of the request, and the messages of a WebSocket. sent: the
|
|
1973
1993
|
// host tells wasm_tcp what the client took (tcp_sent).
|
|
1974
1994
|
this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
|
|
1975
|
-
|
|
1976
|
-
|
|
1995
|
+
// With a body, the head goes in the event of the first part of the
|
|
1996
|
+
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1998
|
+
else this.bridgeSend(c, bytes);
|
|
1977
1999
|
});
|
|
2000
|
+
// A response before the end of the body: the rest of the body goes to
|
|
2001
|
+
// the app while it reads, else bridgeUpload reads and drops it (see
|
|
2002
|
+
// drain). The response waits for that, DRAIN_TIME ms at most.
|
|
2003
|
+
if (c.upload) await within(c.upload, DRAIN_TIME);
|
|
2004
|
+
return response;
|
|
1978
2005
|
}
|
|
1979
2006
|
|
|
1980
2007
|
// Bytes to the app on the connection c.
|
|
1981
2008
|
bridgeSend(c, bytes) {
|
|
1982
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1983
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1984
2012
|
}
|
|
1985
2013
|
|
|
@@ -1987,8 +2015,13 @@ export class Vm {
|
|
|
1987
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1988
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1989
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1990
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1991
|
-
|
|
2018
|
+
// the memory of the VM, and a slow app slows the client. While a recv of
|
|
2019
|
+
// the app waits for more bytes (c.need, as Bandit reads a body), a part
|
|
2020
|
+
// has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
|
|
2021
|
+
// each event costs a turn of the VM. The head of the request goes with
|
|
2022
|
+
// the bytes of the first read of the client, with no wait for
|
|
2023
|
+
// UPLOAD_PART bytes.
|
|
2024
|
+
async bridgeUpload(c, body, chunked, max, head) {
|
|
1992
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1993
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
1994
2027
|
// errors the stream (Node.js 26, Chromium). So the other hosts use the
|
|
@@ -1997,7 +2030,7 @@ export class Vm {
|
|
|
1997
2030
|
try { byob = body.getReader({ mode: 'byob' }); } catch {}
|
|
1998
2031
|
if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
|
|
1999
2032
|
const reader = byob ?? body.getReader();
|
|
2000
|
-
const read = byob ? () => byob.readAtLeast(
|
|
2033
|
+
const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
|
|
2001
2034
|
let total = 0;
|
|
2002
2035
|
let part = [];
|
|
2003
2036
|
let size = 0;
|
|
@@ -2011,13 +2044,22 @@ export class Vm {
|
|
|
2011
2044
|
const bytes = part.length === 1 ? part[0] : join(part, size);
|
|
2012
2045
|
part = [];
|
|
2013
2046
|
size = 0;
|
|
2014
|
-
|
|
2047
|
+
out(chunked ? chunk(bytes) : bytes);
|
|
2015
2048
|
return true;
|
|
2016
2049
|
};
|
|
2050
|
+
// Bytes to the app, after the head when it did not go yet.
|
|
2051
|
+
const out = (bytes) => {
|
|
2052
|
+
clearTimeout(alone);
|
|
2053
|
+
this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
|
|
2054
|
+
head = null;
|
|
2055
|
+
};
|
|
2056
|
+
// A client that sends no body yet: the head goes alone after HEAD_WAIT
|
|
2057
|
+
// ms, so the app sees the request (and its own time limits run).
|
|
2058
|
+
const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
|
|
2017
2059
|
try {
|
|
2018
2060
|
for (;;) {
|
|
2019
2061
|
// A read can end the stream with its last bytes (done and a value).
|
|
2020
|
-
const { value, done } = await read();
|
|
2062
|
+
const { value, done } = await read(head ? 1 : UPLOAD_PART);
|
|
2021
2063
|
if (c.done || this.dead) break;
|
|
2022
2064
|
if (value?.length) {
|
|
2023
2065
|
total += value.length;
|
|
@@ -2033,21 +2075,26 @@ export class Vm {
|
|
|
2033
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2034
2076
|
part.push(piece);
|
|
2035
2077
|
size += piece.length;
|
|
2036
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2037
2079
|
}
|
|
2038
|
-
if (ok && done && size) ok = await flush();
|
|
2080
|
+
if (ok && (done || head) && size) ok = await flush();
|
|
2039
2081
|
if (!ok) break;
|
|
2040
2082
|
if (done) {
|
|
2041
|
-
if (chunked)
|
|
2083
|
+
if (chunked) out(CHUNKS_END);
|
|
2084
|
+
else if (head) out(new Uint8Array(0));
|
|
2042
2085
|
this.bridgeClock(c, c.timeout * 1000);
|
|
2043
2086
|
return;
|
|
2044
2087
|
}
|
|
2045
2088
|
}
|
|
2046
|
-
reader.cancel().catch(() => {});
|
|
2047
2089
|
} catch {
|
|
2048
2090
|
// The client stopped the upload.
|
|
2049
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2050
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2051
2098
|
}
|
|
2052
2099
|
|
|
2053
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2055,9 +2102,12 @@ export class Vm {
|
|
|
2055
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2056
2103
|
}
|
|
2057
2104
|
|
|
2058
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2059
|
-
|
|
2105
|
+
// The app read n bytes of the connection c (tcp_read). msg.want and
|
|
2106
|
+
// msg.got give the bytes that a recv of the app still waits for, beyond
|
|
2107
|
+
// the bytes on their way to it (c.need).
|
|
2108
|
+
bridgeRead(c, n, msg) {
|
|
2060
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2061
2111
|
this.bridgeRoom(c);
|
|
2062
2112
|
c.inbound?.flush();
|
|
2063
2113
|
}
|
|
@@ -2667,7 +2717,7 @@ export class Vm {
|
|
|
2667
2717
|
}
|
|
2668
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2669
2719
|
case 'tcp_read': {
|
|
2670
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2671
2721
|
break;
|
|
2672
2722
|
}
|
|
2673
2723
|
case 'tcp_close': {
|
|
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
|
|
|
2759
2809
|
// read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
|
|
2760
2810
|
// costs a turn of the VM.
|
|
2761
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2762
|
-
const UPLOAD_PART =
|
|
2812
|
+
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2814
|
+
// The time that the head of a request waits for the first bytes of its
|
|
2815
|
+
// body (bridgeUpload), in ms.
|
|
2816
|
+
const HEAD_WAIT = 20;
|
|
2763
2817
|
const UPLOAD_READ = 64 * 1024;
|
|
2818
|
+
// The rest of a request body that the app did not read (drain): the
|
|
2819
|
+
// bytes that the host reads at most before the response goes, the ms with
|
|
2820
|
+
// no new bytes, and the ms in all.
|
|
2821
|
+
const DRAIN_BYTES = 64 * 1024 * 1024;
|
|
2822
|
+
const DRAIN_IDLE = 5000;
|
|
2823
|
+
const DRAIN_TIME = 60000;
|
|
2824
|
+
|
|
2825
|
+
// The rest of a request body, read and dropped before the response goes,
|
|
2826
|
+
// when the app answered before it read the whole body (a 413, for
|
|
2827
|
+
// example). workerd cannot read a request body after the response has
|
|
2828
|
+
// gone, and it then closes the connection with the bytes that were not
|
|
2829
|
+
// read. wrangler dev uses that connection again, and its next request
|
|
2830
|
+
// fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
|
|
2831
|
+
// with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
|
|
2832
|
+
// read() gives { value, done }. limits replaces the bounds (a test).
|
|
2833
|
+
export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
|
|
2834
|
+
const end = Date.now() + time;
|
|
2835
|
+
let n = 0;
|
|
2836
|
+
try {
|
|
2837
|
+
for (;;) {
|
|
2838
|
+
const r = await within(read(), Math.min(idle, end - Date.now()));
|
|
2839
|
+
if (r === LATE) break;
|
|
2840
|
+
if (r.done) return;
|
|
2841
|
+
n += r.value?.byteLength ?? 0;
|
|
2842
|
+
if (n > bytes || Date.now() >= end) break;
|
|
2843
|
+
}
|
|
2844
|
+
} catch {
|
|
2845
|
+
return; // the client stopped the body
|
|
2846
|
+
}
|
|
2847
|
+
try { await cancel(); } catch {}
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// The promise p, or LATE after ms ms.
|
|
2851
|
+
function within(p, ms) {
|
|
2852
|
+
let timer;
|
|
2853
|
+
const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
|
|
2854
|
+
return Promise.race([p, late]).finally(() => clearTimeout(timer));
|
|
2855
|
+
}
|
|
2764
2856
|
|
|
2765
2857
|
// The size bytes of the list parts, as one array.
|
|
2766
2858
|
function join(parts, size) {
|