Push by subscription: hold no upstream connection per tab
A signed-in tab held two sockets: the browser's, and one from ihasmail to
Stalwart carrying that tab's push stream. The upstream one was most of what a
tab cost, and the only reason Stalwart's connection limit applied to ihasmail
at all.
RFC 8620 section 7.2 defines the other push transport: a PushSubscription,
where the server POSTs StateChange objects to a URL the client registers.
Stalwart 0.16.20 implements it. ihasmail now registers one subscription per
account at sign-in, and when Stalwart POSTs a change, fans it out to that
account's open tabs over the browser-facing streams it already holds. A tab
opens on the relay as before and is moved to fan-out the moment its account
verifies -- the upstream request is ended, the browser stream is untouched,
and nothing keeps a reference to what was torn down. After that there is no
upstream connection at all. The shapes are the RFC's; nothing here is taken
from any other client.
Measured at a 256 MiB cap over a private plain-HTTP route, against a real
Stalwart with 6,144 accounts verifying during the ramp and no failures:
tabs client Stalwart system KiB/tab
raw relay (before) 5,000 48.2 46.4 94.6
push by subscription 6,144 33.3 4.8 38.0
a direct-to-server client 12,389 4.8 53.8 58.6
Descriptors per tab: one, the browser's. Stalwart pays 4.8 KiB per tab and
holds no connection for it, so its per-listener connection limit no longer
applies to ihasmail. What remains per tab on the client is Node's cost for a
held HTTP/1.1 connection.
PUSH_URL is the https origin Stalwart can reach ihasmail at. The RFC requires
https and Stalwart enforces it, so Stalwart must trust that certificate: a
public TLS front already does; a private segment needs an internal CA in
Stalwart's trust store. An account whose subscription cannot be verified
stays on the relay, so nothing breaks -- only the saving needs the
certificate. PUSH_MODE=relay disables the subscription path entirely.
/api/push/:token accepts only a JSON body under 64 KiB for a known 32-byte
token, answers 200 or 404, and echoes nothing. /api/health reports how many
accounts are verified, pending or failed and how many tabs are on each path.
This commit is contained in:
@@ -309,6 +309,16 @@ export const config = {
|
||||
apiRateLimit: int("API_RATE_LIMIT", 1200),
|
||||
/* Whether JMAP responses are gzipped. Measured: see the bake-off rerun. */
|
||||
compressJmap: process.env.COMPRESS_JMAP !== "0",
|
||||
/*
|
||||
* How push reaches the browser. "relay" holds one upstream stream per tab
|
||||
* (today's behaviour). "subscribe" registers one JMAP PushSubscription per
|
||||
* account and fans Stalwart's POSTs out to that account's tabs, holding no
|
||||
* upstream connection at all -- see push.ts. It needs PUSH_URL: the https
|
||||
* origin Stalwart can reach ihasmail at, with a certificate it trusts.
|
||||
* An account that cannot be verified stays on the relay.
|
||||
*/
|
||||
pushMode: (process.env.PUSH_MODE === "relay" ? "relay" : "subscribe") as "relay" | "subscribe",
|
||||
pushUrl: process.env.PUSH_URL || "",
|
||||
/* See relayPushRaw(): pipe the push stream socket-to-socket instead of through fetch(). */
|
||||
rawPushRelay: process.env.RAW_PUSH_RELAY !== "0",
|
||||
/* See absoluteUpstream(): follow Stalwart's advertised origin instead of pinning to ours. */
|
||||
|
||||
Reference in New Issue
Block a user