beam.com 0.1.0-rc.4 → 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 +89 -14
- package/runtime/deno/worker.js +89 -14
- package/runtime/durable.js +17 -2
- package/runtime/page/runtime-id.js +1 -1
- package/runtime/page/worker.js +89 -14
- package/runtime/release.json +1 -1
- package/runtime/runtime-id.js +1 -1
- package/runtime/worker.js +89 -14
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,7 +1986,7 @@ 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
|
|
@@ -1974,14 +1994,20 @@ export class Vm {
|
|
|
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
1995
|
// With a body, the head goes in the event of the first part of the
|
|
1976
1996
|
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1977
|
-
if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1978
1998
|
else this.bridgeSend(c, bytes);
|
|
1979
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;
|
|
1980
2005
|
}
|
|
1981
2006
|
|
|
1982
2007
|
// Bytes to the app on the connection c.
|
|
1983
2008
|
bridgeSend(c, bytes) {
|
|
1984
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1985
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1986
2012
|
}
|
|
1987
2013
|
|
|
@@ -1989,9 +2015,12 @@ export class Vm {
|
|
|
1989
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1990
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1991
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1992
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1993
|
-
//
|
|
1994
|
-
//
|
|
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.
|
|
1995
2024
|
async bridgeUpload(c, body, chunked, max, head) {
|
|
1996
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1997
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
@@ -2046,7 +2075,7 @@ export class Vm {
|
|
|
2046
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2047
2076
|
part.push(piece);
|
|
2048
2077
|
size += piece.length;
|
|
2049
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2050
2079
|
}
|
|
2051
2080
|
if (ok && (done || head) && size) ok = await flush();
|
|
2052
2081
|
if (!ok) break;
|
|
@@ -2057,11 +2086,15 @@ export class Vm {
|
|
|
2057
2086
|
return;
|
|
2058
2087
|
}
|
|
2059
2088
|
}
|
|
2060
|
-
reader.cancel().catch(() => {});
|
|
2061
2089
|
} catch {
|
|
2062
2090
|
// The client stopped the upload.
|
|
2063
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2064
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2065
2098
|
}
|
|
2066
2099
|
|
|
2067
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2069,9 +2102,12 @@ export class Vm {
|
|
|
2069
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2070
2103
|
}
|
|
2071
2104
|
|
|
2072
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2073
|
-
|
|
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) {
|
|
2074
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2075
2111
|
this.bridgeRoom(c);
|
|
2076
2112
|
c.inbound?.flush();
|
|
2077
2113
|
}
|
|
@@ -2681,7 +2717,7 @@ export class Vm {
|
|
|
2681
2717
|
}
|
|
2682
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2683
2719
|
case 'tcp_read': {
|
|
2684
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2685
2721
|
break;
|
|
2686
2722
|
}
|
|
2687
2723
|
case 'tcp_close': {
|
|
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
|
|
|
2774
2810
|
// costs a turn of the VM.
|
|
2775
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2776
2812
|
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2777
2814
|
// The time that the head of a request waits for the first bytes of its
|
|
2778
2815
|
// body (bridgeUpload), in ms.
|
|
2779
2816
|
const HEAD_WAIT = 20;
|
|
2780
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
|
+
}
|
|
2781
2856
|
|
|
2782
2857
|
// The size bytes of the list parts, as one array.
|
|
2783
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,7 +1986,7 @@ 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
|
|
@@ -1974,14 +1994,20 @@ export class Vm {
|
|
|
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
1995
|
// With a body, the head goes in the event of the first part of the
|
|
1976
1996
|
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1977
|
-
if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1978
1998
|
else this.bridgeSend(c, bytes);
|
|
1979
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;
|
|
1980
2005
|
}
|
|
1981
2006
|
|
|
1982
2007
|
// Bytes to the app on the connection c.
|
|
1983
2008
|
bridgeSend(c, bytes) {
|
|
1984
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1985
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1986
2012
|
}
|
|
1987
2013
|
|
|
@@ -1989,9 +2015,12 @@ export class Vm {
|
|
|
1989
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1990
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1991
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1992
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1993
|
-
//
|
|
1994
|
-
//
|
|
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.
|
|
1995
2024
|
async bridgeUpload(c, body, chunked, max, head) {
|
|
1996
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1997
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
@@ -2046,7 +2075,7 @@ export class Vm {
|
|
|
2046
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2047
2076
|
part.push(piece);
|
|
2048
2077
|
size += piece.length;
|
|
2049
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2050
2079
|
}
|
|
2051
2080
|
if (ok && (done || head) && size) ok = await flush();
|
|
2052
2081
|
if (!ok) break;
|
|
@@ -2057,11 +2086,15 @@ export class Vm {
|
|
|
2057
2086
|
return;
|
|
2058
2087
|
}
|
|
2059
2088
|
}
|
|
2060
|
-
reader.cancel().catch(() => {});
|
|
2061
2089
|
} catch {
|
|
2062
2090
|
// The client stopped the upload.
|
|
2063
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2064
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2065
2098
|
}
|
|
2066
2099
|
|
|
2067
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2069,9 +2102,12 @@ export class Vm {
|
|
|
2069
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2070
2103
|
}
|
|
2071
2104
|
|
|
2072
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2073
|
-
|
|
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) {
|
|
2074
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2075
2111
|
this.bridgeRoom(c);
|
|
2076
2112
|
c.inbound?.flush();
|
|
2077
2113
|
}
|
|
@@ -2681,7 +2717,7 @@ export class Vm {
|
|
|
2681
2717
|
}
|
|
2682
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2683
2719
|
case 'tcp_read': {
|
|
2684
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2685
2721
|
break;
|
|
2686
2722
|
}
|
|
2687
2723
|
case 'tcp_close': {
|
|
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
|
|
|
2774
2810
|
// costs a turn of the VM.
|
|
2775
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2776
2812
|
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2777
2814
|
// The time that the head of a request waits for the first bytes of its
|
|
2778
2815
|
// body (bridgeUpload), in ms.
|
|
2779
2816
|
const HEAD_WAIT = 20;
|
|
2780
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
|
+
}
|
|
2781
2856
|
|
|
2782
2857
|
// The size bytes of the list parts, as one array.
|
|
2783
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,7 +1986,7 @@ 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
|
|
@@ -1974,14 +1994,20 @@ export class Vm {
|
|
|
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
1995
|
// With a body, the head goes in the event of the first part of the
|
|
1976
1996
|
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1977
|
-
if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1978
1998
|
else this.bridgeSend(c, bytes);
|
|
1979
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;
|
|
1980
2005
|
}
|
|
1981
2006
|
|
|
1982
2007
|
// Bytes to the app on the connection c.
|
|
1983
2008
|
bridgeSend(c, bytes) {
|
|
1984
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1985
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1986
2012
|
}
|
|
1987
2013
|
|
|
@@ -1989,9 +2015,12 @@ export class Vm {
|
|
|
1989
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1990
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1991
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1992
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1993
|
-
//
|
|
1994
|
-
//
|
|
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.
|
|
1995
2024
|
async bridgeUpload(c, body, chunked, max, head) {
|
|
1996
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1997
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
@@ -2046,7 +2075,7 @@ export class Vm {
|
|
|
2046
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2047
2076
|
part.push(piece);
|
|
2048
2077
|
size += piece.length;
|
|
2049
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2050
2079
|
}
|
|
2051
2080
|
if (ok && (done || head) && size) ok = await flush();
|
|
2052
2081
|
if (!ok) break;
|
|
@@ -2057,11 +2086,15 @@ export class Vm {
|
|
|
2057
2086
|
return;
|
|
2058
2087
|
}
|
|
2059
2088
|
}
|
|
2060
|
-
reader.cancel().catch(() => {});
|
|
2061
2089
|
} catch {
|
|
2062
2090
|
// The client stopped the upload.
|
|
2063
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2064
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2065
2098
|
}
|
|
2066
2099
|
|
|
2067
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2069,9 +2102,12 @@ export class Vm {
|
|
|
2069
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2070
2103
|
}
|
|
2071
2104
|
|
|
2072
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2073
|
-
|
|
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) {
|
|
2074
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2075
2111
|
this.bridgeRoom(c);
|
|
2076
2112
|
c.inbound?.flush();
|
|
2077
2113
|
}
|
|
@@ -2681,7 +2717,7 @@ export class Vm {
|
|
|
2681
2717
|
}
|
|
2682
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2683
2719
|
case 'tcp_read': {
|
|
2684
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2685
2721
|
break;
|
|
2686
2722
|
}
|
|
2687
2723
|
case 'tcp_close': {
|
|
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
|
|
|
2774
2810
|
// costs a turn of the VM.
|
|
2775
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2776
2812
|
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2777
2814
|
// The time that the head of a request waits for the first bytes of its
|
|
2778
2815
|
// body (bridgeUpload), in ms.
|
|
2779
2816
|
const HEAD_WAIT = 20;
|
|
2780
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
|
+
}
|
|
2781
2856
|
|
|
2782
2857
|
// The size bytes of the list parts, as one array.
|
|
2783
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,7 +1986,7 @@ 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
|
|
@@ -1974,14 +1994,20 @@ export class Vm {
|
|
|
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
1995
|
// With a body, the head goes in the event of the first part of the
|
|
1976
1996
|
// body (bridgeUpload): a small body is one event, as before the parts.
|
|
1977
|
-
if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1997
|
+
if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
|
|
1978
1998
|
else this.bridgeSend(c, bytes);
|
|
1979
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;
|
|
1980
2005
|
}
|
|
1981
2006
|
|
|
1982
2007
|
// Bytes to the app on the connection c.
|
|
1983
2008
|
bridgeSend(c, bytes) {
|
|
1984
2009
|
c.sent += bytes.length;
|
|
2010
|
+
if (c.need) c.need = Math.max(0, c.need - bytes.length);
|
|
1985
2011
|
this.event({ t: 'tcp_data', id: c.id }, bytes);
|
|
1986
2012
|
}
|
|
1987
2013
|
|
|
@@ -1989,9 +2015,12 @@ export class Vm {
|
|
|
1989
2015
|
// (or the rest of the body): one event for each part, not for each small
|
|
1990
2016
|
// chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
|
|
1991
2017
|
// unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
|
|
1992
|
-
// the memory of the VM, and a slow app slows the client.
|
|
1993
|
-
//
|
|
1994
|
-
//
|
|
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.
|
|
1995
2024
|
async bridgeUpload(c, body, chunked, max, head) {
|
|
1996
2025
|
// workerd has readAtLeast. The option min of the standard BYOB read is
|
|
1997
2026
|
// not safe: when the stream closes with fewer bytes than min, the read
|
|
@@ -2046,7 +2075,7 @@ export class Vm {
|
|
|
2046
2075
|
const piece = value.subarray(at, at + UPLOAD_READ);
|
|
2047
2076
|
part.push(piece);
|
|
2048
2077
|
size += piece.length;
|
|
2049
|
-
if (size >= UPLOAD_PART) ok = await flush();
|
|
2078
|
+
if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
|
|
2050
2079
|
}
|
|
2051
2080
|
if (ok && (done || head) && size) ok = await flush();
|
|
2052
2081
|
if (!ok) break;
|
|
@@ -2057,11 +2086,15 @@ export class Vm {
|
|
|
2057
2086
|
return;
|
|
2058
2087
|
}
|
|
2059
2088
|
}
|
|
2060
|
-
reader.cancel().catch(() => {});
|
|
2061
2089
|
} catch {
|
|
2062
2090
|
// The client stopped the upload.
|
|
2063
2091
|
this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
|
|
2092
|
+
return;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(alone);
|
|
2064
2095
|
}
|
|
2096
|
+
// The app ended the connection, or the body is above BEAM_MAX_BODY.
|
|
2097
|
+
await drain(() => read(UPLOAD_PART), () => reader.cancel());
|
|
2065
2098
|
}
|
|
2066
2099
|
|
|
2067
2100
|
// The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
|
|
@@ -2069,9 +2102,12 @@ export class Vm {
|
|
|
2069
2102
|
if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
|
|
2070
2103
|
}
|
|
2071
2104
|
|
|
2072
|
-
// The app read n bytes of the connection c (tcp_read).
|
|
2073
|
-
|
|
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) {
|
|
2074
2109
|
c.read += n;
|
|
2110
|
+
if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
|
|
2075
2111
|
this.bridgeRoom(c);
|
|
2076
2112
|
c.inbound?.flush();
|
|
2077
2113
|
}
|
|
@@ -2681,7 +2717,7 @@ export class Vm {
|
|
|
2681
2717
|
}
|
|
2682
2718
|
// wasm_tcp: the app read n bytes of a socket.
|
|
2683
2719
|
case 'tcp_read': {
|
|
2684
|
-
this.tcps.get(msg.id)?.ack?.(msg.n);
|
|
2720
|
+
this.tcps.get(msg.id)?.ack?.(msg.n, msg);
|
|
2685
2721
|
break;
|
|
2686
2722
|
}
|
|
2687
2723
|
case 'tcp_close': {
|
|
@@ -2774,10 +2810,49 @@ const LATE = Symbol('late');
|
|
|
2774
2810
|
// costs a turn of the VM.
|
|
2775
2811
|
const UPLOAD_WINDOW = 256 * 1024;
|
|
2776
2812
|
const UPLOAD_PART = 32 * 1024;
|
|
2813
|
+
const UPLOAD_LARGE = 1024 * 1024;
|
|
2777
2814
|
// The time that the head of a request waits for the first bytes of its
|
|
2778
2815
|
// body (bridgeUpload), in ms.
|
|
2779
2816
|
const HEAD_WAIT = 20;
|
|
2780
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
|
+
}
|
|
2781
2856
|
|
|
2782
2857
|
// The size bytes of the list parts, as one array.
|
|
2783
2858
|
function join(parts, size) {
|