diff --git a/.gitignore b/.gitignore index 1a06409..dafc8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ anycable-go-darwin-* anycable-go-windows-* anycable-go-mrb-* anycable-thruster-* +.DS_Store diff --git a/README.md b/README.md index 2b52356..3c938ba 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ The repo behind [anycable.io/compare/nodejs-websocket](https://anycable.io/compa Setups under test: default Socket.io, Socket.io + Connection State Recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. +Sixth target, [socketioxide](https://github.com/totodore/socketioxide) (Rust Socket.io server), benchmarked head-to-head with AnyCable on the same Railway hardware. Results in [its own section below](#socketioxide-rust-socketio); deep dive in [`docs/socketioxide-comparison.md`](./docs/socketioxide-comparison.md). + Methodology, traps, and the bugs we caught in our own setup: [`docs/methodology.md`](./docs/methodology.md). Below: the numbers and how to rerun them. ## Headlines @@ -77,6 +79,37 @@ Three knobs that shape the numbers. Full reasoning in [`docs/methodology.md`](./ - **CSR runs with the in-memory adapter.** Simplest opt-in path. Redis Streams or MongoDB shift the tail by adding network RTT; structural picture holds. CSR is documented as incompatible with the Redis pub/sub adapter, so the "Redis adapter" most teams reach for first is the one CSR can't use. - **AnyCable's jitter-row RAM is the tradeoff for parallel replay.** Its history buffer is per-stream so `history` parallelises across streams; that costs more RAM during jittery runs. Page-level RAM-per-connection comes from the idle test, where the per-connection footprint is what's measured. +## socketioxide (Rust Socket.io) + +[socketioxide](https://github.com/totodore/socketioxide) speaks the Socket.io wire protocol in Rust. Its author asked us to benchmark it, so we ran it head-to-head with AnyCable on the same Railway hardware, AnyCable alongside as a same-window control. It answers a sharp question: which of Socket.io's problems are about the runtime language, and which are about the architecture? + +**Latency (steady network).** Comparable to AnyCable. Nothing a user feels. + +| | 1K p50 / p99 | 10K p50 / p99 | Delivery | +| --- | --- | --- | --- | +| socketioxide | 23 / 66 ms | 289 / 972 ms | 100% | +| AnyCable OSS | 16 / 46 ms | 232 / 731 ms | 100% | + +**Delivery under jitter.** At-most-once, no replay. In the Socket.io band to 1K, then it falls off a cliff. + +| Clients | socketioxide | AnyCable | +| --- | --- | --- | +| 200 (local) | 91.6% | 100% | +| 1,000 | 89.4% | 100% | +| 10,000 | **41% then 33%** (two runs) | 100% | + +**Avalanche (app deploy).** Every connection dies on the deploy (in-process WS goes down with the app), and recovery collapses at scale, tracking Node Socket.io almost exactly. + +| Clients | Reconnected | Recovery | +| --- | --- | --- | +| 5,000 | 100% | 2.9 s | +| 10,000 | 96% | 67 s | +| 20,000 | **0%** | never | + +**Idle capacity.** Held **600K+** idle connections at ~37 KB each (comparable to AnyCable's ~39 KB), roughly 5x past Node Socket.io's ~120K event-loop ceiling. We could not find its true ceiling: the load-generation fleet caps near ~12K connections per shard (ephemeral ports), so the harness ran out before socketioxide did. + +**The takeaway.** Rust fixes Socket.io's capacity ceiling, the single-event-loop wall that caps Node around 120K. It leaves two things untouched: at-most-once delivery (no replay protocol) and deploy fragility (the WS layer still dies with its app). Both live in the protocol and the topology, so swapping the language to Rust leaves them intact, and socketioxide collapses under jitter and deploy storms at scale the same way Node Socket.io does. AnyCable holds 100% on both because the WS layer is a separate process with replay. Full numbers and the deploy story: [`docs/socketioxide-comparison.md`](./docs/socketioxide-comparison.md). + ## Repository layout ``` diff --git a/backend/package-lock.json b/backend/package-lock.json index 3c1e4da..ab259a2 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2024,9 +2024,9 @@ } }, "node_modules/undici": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.2.0.tgz", - "integrity": "sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "dev": true, "license": "MIT", "engines": { diff --git a/backend/results/socketioxide-local-2026-06-23.json b/backend/results/socketioxide-local-2026-06-23.json new file mode 100644 index 0000000..2a9ee81 --- /dev/null +++ b/backend/results/socketioxide-local-2026-06-23.json @@ -0,0 +1,23 @@ +{ + "run": "socketioxide vs anycable-go, local head-to-head (control canary)", + "date": "2026-06-23T01:23:00Z", + "environment": { + "host": "local single machine (macOS, M-series)", + "scale": "small (200 clients) — NOT Railway 10K headline scale", + "note": "AnyCable run in the same window as a control; its expected shape (100% delivery, multi-second replay tail under jitter) confirms the environment was sound for the socketioxide measurement.", + "socketioxide": "0.18.4 (rustc 1.95.0, release build, features v4+tracing+state)", + "anycable_go": "1.6.14 (--broker=memory --presets=broker)", + "node": "v26.0.0", + "publish_path": "per-message HTTP POST to /_broadcast (symmetric for both)" + }, + "latency": { + "params": { "n": 200, "totalMessages": 100, "intervalMs": 200, "durationSec": 60, "jitter": "disabled" }, + "socketioxide": { "deliveryRatePct": 100, "lost": 0, "p50": 4, "p95": 12, "p99": 18, "max": 27 }, + "anycable": { "deliveryRatePct": 100, "lost": 0, "p50": 5, "p95": 12, "p99": 22, "max": 29 } + }, + "jitter": { + "params": { "n": 200, "totalMessages": 60, "intervalMs": 500, "durationSec": 90, "jitterIntervalSec": 15, "jitterDurationMs": 1000 }, + "socketioxide": { "deliveryRatePct": 91.61, "lost": 928, "received": 10993, "jitterEvents": 872, "p50": 4, "p95": 12, "p99": 16, "max": 26, "protocol": "at-most-once (no replay)" }, + "anycable": { "deliveryRatePct": 100, "lost": 0, "received": 12000, "jitterEvents": 931, "p50": 7, "p95": 3621, "p99": 5623, "max": 8455, "protocol": "replay (history on reconnect)" } + } +} diff --git a/backend/results/socketioxide-railway-2026-06-23.json b/backend/results/socketioxide-railway-2026-06-23.json new file mode 100644 index 0000000..0d041ab --- /dev/null +++ b/backend/results/socketioxide-railway-2026-06-23.json @@ -0,0 +1,95 @@ +{ + "run": "socketioxide vs anycable-go OSS, Railway phase 1 (10K)", + "date": "2026-06-23", + "infra": "Railway project gentle-commitment/production; bench-runner over internal network; anycable-go OSS as same-window canary (held expected shape on every test)", + "socketioxide": "0.18.4 (rust:1-slim release, [::]:3000)", + "anycable_go": "1.6.14 (broker=memory, presets=broker)", + "latency": { + "1k": { + "socketioxide": { + "deliv": 100, + "p50": 23, + "p99": 66 + }, + "anycable": { + "deliv": 100, + "p50": 16, + "p99": 46 + } + }, + "10k": { + "socketioxide": { + "deliv": 100, + "p50": 289, + "p99": 972, + "max": 3627 + }, + "anycable": { + "deliv": 100, + "p50": 232, + "p99": 731, + "max": 7798 + } + } + }, + "jitter_delivery_pct": { + "200_local": { + "socketioxide": 91.6, + "anycable": 100 + }, + "1k_railway": { + "socketioxide": 89.4, + "anycable": 100 + }, + "10k_railway": { + "socketioxide_run1": 40.6, + "socketioxide_run2": 32.7, + "anycable": 100 + } + }, + "finding": "socketioxide is at-most-once: in the band with default Socket.io / uWS up to 1K, then collapses under the 10K reconnect storm (41% then 33%, reproducible, no crash, 0 connect failures). AnyCable holds 100% (separate process + replay). Rust runtime does not rescue the in-process at-most-once architecture at scale.", + "idle_600k": { + "note": "harness-limited at ~600K (shards capped ~12K each); neither target saturated", + "socketioxide": { + "connected": 600091, + "peakMemGB": 21.4, + "ramKbPerConn": 37, + "peakCpuPct": 1.8 + }, + "anycable_go": { + "connected": 600084, + "peakMemGB": 22.1, + "ramKbPerConn": 39, + "peakCpuPct": 9.0 + }, + "finding": "socketioxide held 600K+ (5x past Node Socket.io ~120K event-loop ceiling); RAM/conn comparable to anycable. Rust runtime fixes the capacity wall, not the at-most-once delivery limit." + }, + "avalanche": { + "5k": { + "reconnectPct": 100, + "recoveryMs": 2922, + "neverBack": 0 + }, + "10k": { + "reconnectPct": 96, + "recoveryMs": 67271, + "neverBack": 411 + }, + "20k": { + "reconnectPct": 0, + "recoveryMs": 610836, + "neverBack": "all", + "note": "client side capped ~12K; 0% recovery unambiguous" + }, + "finding": "socketioxide tracks Node Socket.io avalanche cliff: 5K recovers, 10K ~67s/96%, 20K collapses to 0%. In-process WS dies on deploy regardless of language." + }, + "idle_1M_attempt": { + "per_shard_cap": 12002, + "cap_cause": "ephemeral-port exhaustion to single host:port from one source IP (not memory; nofile=122880)", + "fleet_grown_to": 85, + "theoretical_capacity": 1020000, + "result": "49/85 shards clean (588000, 0 failures); 36 shards errored/timed out under coordinator fan-out", + "conclusion": "harness-limited, not server-limited; socketioxide held every connection thrown (588K, 0 failures) with memory headroom. True ceiling unmeasured; needs more source IPs or a lighter idle client." + }, + "teardown": "all 87 services (85 shards + anycable-go + socketioxide-server) stopped; both targets downsized 32GB->0.5GB/1vCPU; verified offline" +} \ No newline at end of file diff --git a/backend/src/bench/tests-manifest.ts b/backend/src/bench/tests-manifest.ts index 18e2ded..ad89784 100644 --- a/backend/src/bench/tests-manifest.ts +++ b/backend/src/bench/tests-manifest.ts @@ -93,6 +93,14 @@ const TARGETS = { anycablePro: "ws://anycable-go-pro.railway.internal:8080/cable", anycableProBroadcast: "http://anycable-go-pro.railway.internal:8080/_broadcast", + // socketioxide is the Rust implementation of the Socket.io protocol. + // Wire-compatible with socket.io-client, so the existing bench-runner + // bench-jitter-socketio / bench-idle-socketio / bench-avalanche-socketio + // endpoints work with ?serverUrl=. No CSR variant yet — the library + // doesn't appear to ship Connection State Recovery as of 0.18.3. + // See docs/socketioxide-comparison.md for the open question to the + // library author. + socketioxide: "http://socketioxide-server.railway.internal:3000", }; // Common knobs reused across tests. Keep these explicit so the manifest @@ -202,6 +210,27 @@ export const tests: TestSpec[] = [ params: { n: 10000, ...LATENCY_10K, cableUrl: TARGETS.anycablePro, broadcastUrl: TARGETS.anycableProBroadcast }, baseline: { "latencyRawMs.p50": 234, "latencyRawMs.p99": 694, deliveryRatePct: 100 }, }, + // socketioxide: same Socket.io wire protocol, Rust implementation. Uses + // the bench-jitter-socketio endpoint with ?serverUrl=. No + // baselines yet — first run pending. See docs/socketioxide-comparison.md. + { + id: "latency-socketioxide-1k", + description: "Roundtrip latency, socketioxide (Rust), 1K subs", + category: "latency", + endpoint: "bench-jitter-socketio", + mode: "sync", + params: { n: 1000, ...LATENCY_1K, serverUrl: TARGETS.socketioxide }, + baseline: {}, + }, + { + id: "latency-socketioxide-10k", + description: "Roundtrip latency, socketioxide (Rust), 10K subs", + category: "latency", + endpoint: "bench-jitter-socketio", + mode: "sync", + params: { n: 10000, ...LATENCY_10K, serverUrl: TARGETS.socketioxide }, + baseline: {}, + }, // ------------------------------------------------------------------------- // Reliability (jitter under WiFi-drop pattern) @@ -265,6 +294,18 @@ export const tests: TestSpec[] = [ params: { n: 10000, ...JITTER_10K, cableUrl: TARGETS.anycablePro, broadcastUrl: TARGETS.anycableProBroadcast, samplesCap: 5000 }, baseline: { deliveryRatePct: 100, lostDeliveries: 0, "latencyRawMs.p95": 4100, "latencyRawMs.p99": 6200 }, }, + // socketioxide jitter row. Expected to land in the at-most-once band + // with default Socket.io and uWS, since socketioxide doesn't appear to + // ship CSR. Confirms the architectural claim across runtimes. + { + id: "jitter-socketioxide-10k", + description: "Reliability under WiFi jitter, socketioxide (Rust), 10K", + category: "jitter", + endpoint: "bench-jitter-socketio", + mode: "async", + params: { n: 10000, ...JITTER_10K, serverUrl: TARGETS.socketioxide, samplesCap: 5000 }, + baseline: {}, + }, // ------------------------------------------------------------------------- // Whispers (1K × 10 rooms, 100 peers/room) @@ -441,6 +482,20 @@ export const tests: TestSpec[] = [ baseline: { connected: 1000000, ramKbPerConnected: 5 }, driftThresholdPct: 60, }, + // socketioxide idle: same multi-shard fan-out, targets the Rust service. + { + id: "idle-socketioxide", + description: "Idle connections held, socketioxide (Rust), 1M target", + category: "idle", + endpoint: "bench-idle-socketio", + mode: "multi-shard", + numShards: 50, + perShardN: 20000, + params: { hold: 120, ramp: 200, stream: "idle-rebaseline", serverUrl: TARGETS.socketioxide }, + targetServiceId: "41f1ac22-2ea6-4d04-974e-4c148be426ff", + baseline: {}, + driftThresholdPct: 60, + }, // ------------------------------------------------------------------------- // Avalanche (in-process WS layer restart under N held connections). @@ -536,4 +591,42 @@ export const tests: TestSpec[] = [ baseline: { reconnectRatePct: 0 }, driftThresholdPct: 100, }, + // socketioxide avalanche escalation: mirror the Socket.io ladder (5K, 10K, + // 15K, 20K, 25K). Same redeploy mechanism, just pointed at the Rust service. + // The interesting question is whether Rust's event loop pushes the cliff out + // further than Node's, or whether the architectural problem (in-process WS + // dies with the app) holds the shape across languages. + { + id: "avalanche-socketioxide-5k", + description: "Avalanche: 5K socketioxide clients, server redeploy", + category: "avalanche", + endpoint: "bench-avalanche-socketio", + mode: "avalanche", + redeployServiceName: "socketioxide-server", + params: { n: 5000, ramp: 200, prearm: 90, recoveryWait: 180, stream: "avalanche-sox-5k", serverUrl: TARGETS.socketioxide }, + baseline: {}, + driftThresholdPct: 100, + }, + { + id: "avalanche-socketioxide-10k", + description: "Avalanche: 10K socketioxide clients, server redeploy", + category: "avalanche", + endpoint: "bench-avalanche-socketio", + mode: "avalanche", + redeployServiceName: "socketioxide-server", + params: { n: 10000, ramp: 200, prearm: 120, recoveryWait: 240, stream: "avalanche-sox-10k", serverUrl: TARGETS.socketioxide }, + baseline: {}, + driftThresholdPct: 100, + }, + { + id: "avalanche-socketioxide-20k", + description: "Avalanche: 20K socketioxide clients, server redeploy", + category: "avalanche", + endpoint: "bench-avalanche-socketio", + mode: "avalanche", + redeployServiceName: "socketioxide-server", + params: { n: 20000, ramp: 200, prearm: 240, recoveryWait: 600, stream: "avalanche-sox-20k", serverUrl: TARGETS.socketioxide }, + baseline: {}, + driftThresholdPct: 100, + }, ]; diff --git a/docs/socketioxide-comparison.md b/docs/socketioxide-comparison.md new file mode 100644 index 0000000..c291073 --- /dev/null +++ b/docs/socketioxide-comparison.md @@ -0,0 +1,331 @@ +# socketioxide in the comparison + +[socketioxide](https://github.com/totodore/socketioxide) is a Rust +implementation of the Socket.io v4+ protocol. The library's author asked us +to include it in the benchmark, and it's a good fit: same wire protocol as +the Node Socket.io server we already test, so the bench-runner's existing +`socket.io-client` driver works against it unchanged. The work is server-side. + +This page tracks what we added, what the open questions are, and what the +numbers look like once we have them. + +## Status + +- **Server scaffold:** `socketioxide/` in this repo. Cargo project plus + Dockerfile that mirrors the shape of `backend/src/socketio/server.ts`: + `/health`, `/stats`, `/_broadcast`, `/publish-local`. **Compiles and + runs** (release build against `socketioxide 0.18.4`); validated locally + against the bench-runner's socket.io-client driver (see Results). +- **Crate pinned to `socketioxide = 0.18`**, resolves to `0.18.4`. + Features: `v4` for the Socket.io v4 wire protocol the bench-runner + speaks, `tracing` for logs, `state` for the connection counter behind + `/stats`. Maintainer was actively pushing engineio hardening fixes on + the day this branch landed; bump the pin in tandem with the next tagged + release if those fixes have shipped by the time you deploy at scale. +- **Bench-runner endpoints:** none new. The Rust server speaks the + Socket.io wire protocol, so the existing `bench-jitter-socketio`, + `bench-idle-socketio`, and `bench-avalanche-socketio` endpoints all + accept it via `?serverUrl=...`. +- **Manifest entries:** added under each rubric in + `backend/src/bench/tests-manifest.ts`. Baselines are empty until the + first run lands. +- **Railway service:** one to deploy from this directory: + `socketioxide-server`. Same hardware tier as the other Socket.io + targets so the comparison stays apples-to-apples. The CSR variant is + deferred (see open questions). + +## Open question that gates the CSR comparison + +The Socket.io + CSR row of the page tests `connectionStateRecovery`: +on disconnect, the server stashes packet state and a per-socket pid; on +reconnect, the client sends pid + last-offset and the server replays. +Node Socket.io supports this since 4.6. + +**Does socketioxide ship CSR?** As of `0.18.3` it does not appear to. +The crate's `Cargo.toml` features list (`v4`, `msgpack`, `tracing`, +`extensions`, `state`, `__test_harness`) has no CSR-shaped flag; the +README and examples don't mention session resume; the `state` feature +is for application-level shared state, not session state. + +So we deferred the CSR variant. If the library author confirms CSR is +on the roadmap (or already present under a different name we missed), +we'll add `socketioxide-server-csr` as a second service and the +`-csr` manifest entries. Until then, socketioxide is tested only in +its at-most-once mode and reported next to default Socket.io / uWS. + +## Why this strengthens the comparison + +The page's architectural claim is that at-most-once delivery losses under +jitter and the in-process avalanche on deploy are properties of the +deployment shape, not of any particular implementation language. Adding a +Rust at-most-once Socket.io server lets us test that claim across runtimes. +Three outcomes are possible, all useful: + +- **Same shape, same numbers.** Rust loses ~16% under jitter, all + connections die on deploy. Confirms the claim: it was always about + topology, never the runtime. +- **Same shape, better numbers.** Rust event loop holds more connections + before the cliff (avalanche pushes out from 25K to 50K, say), but still + goes to zero at deploy. Refines the claim: implementation slows the + cliff, doesn't move it. +- **Different shape.** If socketioxide ships CSR on by default or stays up + through a deploy somehow, the comparison gets more interesting and the + page narrative gets richer. + +Either way, the result is a stronger page, not a weaker one. + +## Rubrics + manifest IDs + +| Rubric | socketioxide (at-most-once) | +|---|---| +| Latency 1K | `latency-socketioxide-1k` | +| Latency 10K | `latency-socketioxide-10k` | +| Jitter 10K | `jitter-socketioxide-10k` | +| Idle 1M target | `idle-socketioxide` | +| Avalanche 5K | `avalanche-socketioxide-5k` | +| Avalanche 10K | `avalanche-socketioxide-10k` | +| Avalanche 20K | `avalanche-socketioxide-20k` | + +Whispers and throughput entries can be added the same way once we have +the latency + jitter numbers and the server config holds up. + +## Open questions for the library author + +Tagged in the GitHub issue: + +1. **Connection State Recovery roadmap.** Does socketioxide support + server-side session resume (pid + last-offset replay) today? If yes, + how is it enabled (feature flag, builder config)? If no, is it on + the roadmap? Answer decides whether we add a `-csr` variant or skip + the CSR rubric for this server. +2. **Production-shaped publisher.** Our `/_broadcast` handler calls + `io.to(stream).emit(...).await` on every POST. That's the natural + shape but might not match the recommended pattern for socketioxide + at high broadcast rates. Open to a PR that swaps it. +3. **Default config knobs.** Anything we should tune for the + comparison to be fair? Compression, ping intervals, buffer sizes. + Defaults here match the Node Socket.io server's bench config; happy + to take a config patch. +4. **Engineio hardening.** Several `fix(engineio): ... malicious / + malformed packet` commits landed on 2026-06-20. Should we pin to a + commit past those rather than the `0.18.3` tag? Or wait for the + next release that bundles them? + +## Results + +### Local head-to-head, socketioxide vs AnyCable (2026-06-23) + +First real run. socketioxide `0.18.4` (release build) against `anycable-go` +1.6.14, both on one machine, 200 clients, per-message HTTP publishing for +both. AnyCable runs in the same window as a control: its expected shape +(100% delivery, multi-second replay tail under jitter) confirms the +environment was sound, so the socketioxide numbers aren't an artifact of a +bad local moment. This is small-scale local, not the Railway 10K headline +setup; treat it as "the harness works and the architectural shape holds", +not as a published page number. + +**Latency (jitter disabled), 200 clients, 100 messages at 5/sec:** + +| | Delivery | p50 | p95 | p99 | max | +|---|---|---|---|---|---| +| socketioxide | 100% | 4 ms | 12 ms | 18 ms | 27 ms | +| AnyCable (control) | 100% | 5 ms | 12 ms | 22 ms | 29 ms | + +Roundtrip latency is the same order on both. Nothing separates a Rust +Socket.io server from AnyCable when the network is steady. + +**Delivery under jitter, 200 clients, TCP force-close every ~15 s:** + +| | Delivery | Lost | p50 | p95 | p99 | max | +|---|---|---|---|---|---|---| +| socketioxide | **91.6%** | 928 | 4 ms | 12 ms | 16 ms | 26 ms | +| AnyCable (control) | **100%** | 0 | 7 ms | 3.6 s | 5.6 s | 8.5 s | + +This is the architectural result the page argues for, reproduced across a +fourth runtime. socketioxide is at-most-once: it has no replay, so the +broadcasts that land during a client's offline window are gone (~8% lost +here). The messages it *does* deliver are fast. AnyCable delivers 100% +because it replays the missed range on reconnect, which is what the +multi-second p95/p99 tail is: late-but-delivered messages. The Rust +implementation lands in the same at-most-once band as default Socket.io +and uWS. The delivery gap is about the protocol (replay vs none), not the +language. + +Raw numbers: `backend/results/socketioxide-local-2026-06-23.json`. + +### Railway run @ 10K, socketioxide vs AnyCable OSS (2026-06-23) + +Phase 1 on the real infra: `socketioxide-server` deployed to the same +Railway project as the page targets, `anycable-go` OSS woken alongside as +the same-window canary, both driven from the Railway-hosted bench-runner +over the internal network. AnyCable held its expected shape on every test +(latency in band, 100% delivery under jitter, 100% throughput), so the +window was healthy and the socketioxide numbers are not Railway noise. + +**Latency (jitter disabled):** + +| Scale | socketioxide p50 / p99 | AnyCable OSS p50 / p99 | Both delivery | +|---|---|---|---| +| 1K | 23 / 66 ms | 16 / 46 ms | 100% | +| 10K | 289 / 972 ms | 232 / 731 ms | 100% | + +Same order of magnitude. AnyCable is a touch faster at the tail; nothing +separates them in a way a user would feel. socketioxide delivers 100% when +the network is steady. + +**Delivery under jitter (TCP force-close every ~15 s, no replay protocol):** + +| Scale | socketioxide delivery | AnyCable OSS (canary) | +|---|---|---| +| 200 (local) | 91.6% | 100% | +| 1K (Railway) | 89.4% | 100% | +| 10K (Railway) | **40.6%**, then **32.7%** (two runs) | 100% | + +This is the headline finding. socketioxide sits in the at-most-once band +with default Socket.io (~85%) and uWS (~87%) up to 1K, then **collapses +under the 10K reconnect storm**: two independent runs landed at 41% and +33% delivery. The cliff is reproducible and is not a server crash (no +errors logged, 0 connect failures, 10K/10K clients connect every time) +and not Railway noise (AnyCable held 100% in the same windows). + +Two things compound at 10K. socketioxide is at-most-once, so anything that +lands during a client's offline window is gone. And the jitter path opens +a fresh connection per disruption (the standard Socket.io recovery, since +the protocol has no resume), so 10K clients churning ~7 reconnects each +is ~70K fresh handshakes against one in-process server. The Rust runtime +does not rescue the architecture: at scale, an in-process at-most-once WS +layer sheds most of its messages during a reconnect storm. AnyCable holds +100% because the WS layer is a separate process the disruption never +restarts, and replay recovers whatever the offline window missed. + +Caveat for fairness: AnyCable's jitter path reconnects the same cable +in place (its client library's built-in resume), which is lighter than +socketioxide's fresh-socket-per-event path. Part of the 10K gap is that +asymmetry, which is itself a property of having a resume protocol versus +not. The 1K row (89% socketioxide, same fresh-socket path) shows the +mechanism is sound at moderate scale; the 10K row shows where it breaks. + +Raw numbers: `backend/results/railway-phase1/` (per-test JSON) and +`backend/results/socketioxide-railway-2026-06-23.json` (summary). + +### Idle connection capacity @ ~600K (phase 2, 2026-06-23) + +50-shard fleet woken, both targets sized 32 GB / 32 vCPU, each ramped +toward 1M idle socket.io/cable connections. + +**The harness capped before either server did.** Both socketioxide and +anycable-go held ~600K and then the bench-runner shards ran out of +client-side capacity (~12K socket.io/cable clients per shard, well under +the ~50K port limit, so the shards' own event loops / memory were the +wall). The near-identical ceiling (600,091 vs 600,084) is the tell: it's +a property of the load generator, not the servers. Neither target +saturated; both sized 32 GB peaked at ~21-22 GB with ~10 GB headroom. + +| At ~600K held | Peak memory | RAM / conn | Peak CPU | +|---|---|---|---| +| socketioxide | 21.4 GB | ~37 KB | 1.8% | +| anycable-go OSS | 22.1 GB | ~39 KB | 9.0% | + +Two things worth stating. First, per-connection memory is comparable: +~37 KB (socketioxide) vs ~39 KB (anycable-go), both far below Node +Socket.io's ~52 KB. Second, and more telling: **socketioxide held 600K+ +idle connections, about 5x past Node Socket.io's ~120K ceiling.** Node +Socket.io caps there because handshakes serialise through one event loop +regardless of memory; socketioxide's multi-threaded tokio runtime clears +that wall. So the Rust implementation fixes Socket.io's idle-capacity +problem (a runtime/concurrency limit) even though it cannot fix the +at-most-once delivery problem (a protocol limit). The two ceilings have +different causes, and only one of them is about the language. + +Caveat: 600K is harness-limited, so this is a floor on each server's true +capacity, not the ceiling. The RAM/conn figures include the memory churn +of ~400K failed connection attempts hitting each target during ramp, so +treat them as approximate upper bounds. To find the real server ceilings +we would need larger bench-runner shards or more of them. + +**Pushing toward 1M (and why we stopped at the harness).** We traced the +600K cap to a hard per-shard limit of ~12,002 connections to a single +`host:port` from one source IP (ephemeral-port exhaustion, not memory: +the bench-runner containers report `nofile=122880`). 50 shards x 12K = +600K, exactly the cap. To go higher we grew the fleet to 85 shards +(~1.02M of theoretical capacity) and re-ran. The expanded fleet got +flaky: 49 of 85 shards delivered a clean 12,000 each (588,000 total, 0 +failures on those) while 36 shards errored or timed out under the +coordinator's fan-out. So the load generator, not socketioxide, remained +the wall, and we did not land a clean 1M. socketioxide itself never +showed stress: it accepted every connection the surviving shards threw +(588K, 0 failures) and stayed well under its memory limit. + +What this establishes: socketioxide comfortably holds **at least ~600K** +idle socket.io connections on a 32 GB box with headroom to spare, roughly +5x past Node Socket.io's ~120K event-loop ceiling, at per-connection +memory comparable to AnyCable. We could not measure its true ceiling +because reaching 1M needs a more capable load-generation fleet (more +source IPs, or a lighter idle client than socket.io-client). That is a +harness limitation, and the page does not claim a socketioxide idle +ceiling on the strength of it. + +Raw: `backend/results/railway-phase2/idle-*.json`. + +### Avalanche (deploy survival) @ 5K / 10K / 20K + +Each scale ramps N socket.io clients against `socketioxide-server`, then +a real `railway redeploy` swaps the container mid-test (same methodology +as the Socket.io avalanche ladder). socketioxide tracks Node Socket.io's +cliff almost exactly: + +| Clients | Reconnected | Recovery | Never back | +|---|---|---|---| +| 5,000 | 100% | 2.9 s | 0 | +| 10,000 | 96% | 67 s | 411 | +| 20,000 | **0%** | never (capped at 10 min) | all | + +On the deploy, every connection drops (in-process WS dies with the app, +the architectural fact). At 5K it recovers cleanly. By 10K the reconnect +storm against the freshly-restarted single instance stretches recovery to +over a minute with a few hundred clients never returning. By 20K it +collapses to 0% recovered, the same cliff Node Socket.io hits around 25K +on the page (Socket.io 10K was ~65 s / 96%, near-identical to +socketioxide's 67 s / 96%). The Rust runtime does not change the shape: +an in-process WS layer cannot survive its own app's deploy, and the +reconnect storm overwhelms the new instance at scale regardless of +language. AnyCable's avalanche row is 0 s by construction: the WS process +is never restarted by an app deploy. + +(The 20K row is muddied by the client side: one bench-runner caps near +~12K socket.io clients, so the 20K avalanche only fully ramped ~12K. The +0% recovery is unambiguous either way.) + +Raw: `backend/results/railway-phase2/avalanche-socketioxide-*.json`. + +### How phase 1 was deployed + +`socketioxide-server` is a net-new Railway service built from +`socketioxide/`. Three things the local build didn't catch surfaced on +Railway and are fixed in the Dockerfile / server: + +- Base image must be Rust 1.94+ (socketioxide 0.18.4 MSRV); `rust:1-slim`. +- Bind `[::]` not `0.0.0.0`: Railway's private network is IPv6, so an + IPv4-only bind is unreachable internally. +- Pin `PORT=3000` on the service so the listen port matches the manifest + target (Railway injects `PORT=8080` otherwise). + +### Reproduce the local run + +```bash +# Terminal 1 — socketioxide +cd socketioxide && cargo run --release # :3000 + +# Terminal 2 — anycable-go +anycable-go --port 8080 --broker=memory --presets=broker --public + +# Terminal 3 — jitter, both, same window +cd backend +SOCKETIO_URL=http://localhost:3000 NUM_CLIENTS=200 DURATION=90 \ + TOTAL_MESSAGES=60 INTERVAL_MS=500 JITTER_INTERVAL=15 JITTER_DURATION=1000 \ + npm run bench:jitter:socketio +ANYCABLE_URL=ws://localhost:8080/cable BROADCAST_URL=http://localhost:8090/_broadcast \ + NUM_CLIENTS=200 DURATION=90 \ + TOTAL_MESSAGES=60 INTERVAL_MS=500 JITTER_INTERVAL=15 JITTER_DURATION=1000 \ + npm run bench:jitter:anycable +``` diff --git a/socketioxide/.gitignore b/socketioxide/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/socketioxide/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/socketioxide/Cargo.lock b/socketioxide/Cargo.lock new file mode 100644 index 0000000..d7241df --- /dev/null +++ b/socketioxide/Cargo.lock @@ -0,0 +1,1278 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "engineioxide" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411a8df7da5a9d80d6b1b75bb42a31bd2119c8e3927265536ed5c22fc6911058" +dependencies = [ + "base64", + "bytes", + "engineioxide-core", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "memchr", + "pin-project-lite", + "serde", + "serde_json", + "smallvec", + "thiserror", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "engineioxide-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68868b34254632364403d72a823927096ef5a1241bd039b544e5e1fa675e49e" +dependencies = [ + "base64", + "bytes", + "rand 0.10.1", + "serde", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "matchit" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "socketioxide" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c067d6dd2b9745bcd16f3f2d00960f75fcce16e4abdf5db469854f45b37bda2" +dependencies = [ + "bytes", + "engineioxide", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "matchit 0.9.2", + "pin-project-lite", + "serde", + "socketioxide-core", + "socketioxide-parser-common", + "state", + "thiserror", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "socketioxide-bench-server" +version = "0.1.0" +dependencies = [ + "axum", + "serde", + "serde_json", + "socketioxide", + "tokio", + "tower", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "socketioxide-core" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8ae8f85d9fad2966a7e6a49b47821f6b99454ec54fe53784f5f4172e28f70a" +dependencies = [ + "arbitrary", + "bytes", + "engineioxide-core", + "futures-core", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "socketioxide-parser-common" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba9a856b0de7f665fe0d66fb7d60c74ad2e1810552c60a31f72e9aa6372cc02" +dependencies = [ + "bytes", + "itoa", + "serde", + "serde_json", + "socketioxide-core", +] + +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tungstenite" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "sha1", + "thiserror", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/socketioxide/Cargo.toml b/socketioxide/Cargo.toml new file mode 100644 index 0000000..d0ae99b --- /dev/null +++ b/socketioxide/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "socketioxide-bench-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Pinned to the current stable release as of 2026-06. The crate is on a +# steady ~monthly cadence; bump in tandem with engineioxide when a new +# semver-minor lands. v4 enables the Socket.io v4 wire protocol that +# our bench-runner's socket.io-client driver speaks. +socketioxide = { version = "0.18", features = ["v4", "tracing", "state"] } +axum = "0.7" +tokio = { version = "1", features = ["full"] } +tower = "0.5" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = "0.1" +tracing-subscriber = "0.3" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 diff --git a/socketioxide/Dockerfile b/socketioxide/Dockerfile new file mode 100644 index 0000000..2a589aa --- /dev/null +++ b/socketioxide/Dockerfile @@ -0,0 +1,29 @@ +# Build the socketioxide bench server. +# +# Same shape as backend/Dockerfile: builder stage produces a binary, +# runtime stage is a thin slim image. Build the image from this directory: +# +# docker build -t socketioxide-server . +# +# Or via railway.toml deploy from this directory (--path-as-root). + +# socketioxide 0.18.4 / engineioxide 0.17.5 set MSRV 1.94. Use the latest +# stable 1.x so the image clears it (built locally on 1.95). +FROM rust:1-slim-bookworm AS builder +WORKDIR /src +RUN apt-get update \ + && apt-get install -y --no-install-recommends pkg-config libssl-dev \ + && rm -rf /var/lib/apt/lists/* +# Copy the lock too so the image builds the exact dependency versions we +# resolved and tested locally (--locked fails the build if they drift). +COPY Cargo.toml Cargo.lock ./ +COPY src/ ./src/ +RUN cargo build --release --locked + +FROM debian:bookworm-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* +COPY --from=builder /src/target/release/socketioxide-bench-server /usr/local/bin/socketioxide-bench-server +EXPOSE 3000 +CMD ["socketioxide-bench-server"] diff --git a/socketioxide/railway.toml b/socketioxide/railway.toml new file mode 100644 index 0000000..5f9978b --- /dev/null +++ b/socketioxide/railway.toml @@ -0,0 +1,2 @@ +[build] +dockerfilePath = "Dockerfile" diff --git a/socketioxide/src/main.rs b/socketioxide/src/main.rs new file mode 100644 index 0000000..b64e172 --- /dev/null +++ b/socketioxide/src/main.rs @@ -0,0 +1,208 @@ +// Socketioxide bench server. Mirrors the shape of backend/src/socketio/server.ts +// so the bench-runner's existing socket.io-client driver can target it +// with `?serverUrl=` and no protocol-specific work needed. +// +// Endpoints: +// GET /health service liveness probe +// GET /stats { connections } +// POST /_broadcast { stream, data } -> io.to(stream).emit("message", data) +// POST /publish-local in-process publish loop, mirrors the Node server's +// /publish-local for the "publisher inside the WS +// process" diagnostic test. +// +// Env: +// PORT default 3000 +// +// CSR (Connection State Recovery) is intentionally not implemented here. +// socketioxide 0.18 doesn't ship server-side session resume (no CSR-shaped +// feature flag, no mention in README or examples). If the library adds it, +// we'll wire it the same way the Node server does. See +// docs/socketioxide-comparison.md for the open question to the library author. + +use std::env; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use axum::{ + extract::{Query, State as AxumState}, + http::StatusCode, + response::Json, + routing::{get, post}, + Router, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use socketioxide::{ + extract::{Data, SocketRef, State as IoState}, + SocketIo, +}; +use tokio::net::TcpListener; +use tracing::info; + +// Shared connection counter. Registered as socketioxide state via +// `.with_state(Arc)` (read in handlers through the State +// extractor) and cloned into the axum router state so GET /stats reads +// the same Arc. +type ConnCounter = Arc; + +// Router state for the HTTP endpoints: the io handle (to broadcast) plus +// the same counter Arc. +#[derive(Clone)] +struct HttpState { + io: SocketIo, + connections: ConnCounter, +} + +#[derive(Deserialize)] +struct BroadcastBody { + stream: String, + data: Value, +} + +#[derive(Serialize)] +struct Stats { + connections: u64, +} + +#[derive(Deserialize)] +struct PublishLocalQuery { + total: Option, + interval: Option, + stream: Option, + delay: Option, +} + +// --- socketioxide handlers --------------------------------------------------- + +async fn on_connect(s: SocketRef, connections: IoState) { + connections.fetch_add(1, Ordering::Relaxed); + + // Client emits `join` with the stream name (a string). + s.on("join", |s: SocketRef, Data::(room)| async move { + let _ = s.join(room); + }); + + // Whisper: client emits ("whisper", room, payload). socketioxide + // delivers multiple emit args as a tuple. Forward to everyone else + // in the room. Out of scope for the jitter/latency/idle/avalanche + // tests but kept for parity with the Node server. + s.on( + "whisper", + |s: SocketRef, Data::<(String, Value)>((room, payload))| async move { + let _ = s.to(room).emit("whisper", &payload).await; + }, + ); + + s.on_disconnect(on_disconnect); +} + +async fn on_disconnect(connections: IoState) { + connections.fetch_sub(1, Ordering::Relaxed); +} + +// --- HTTP handlers ----------------------------------------------------------- + +async fn health() -> Json { + Json(json!({ "status": "ok", "mode": "socketioxide" })) +} + +async fn stats(AxumState(state): AxumState) -> Json { + Json(Stats { + connections: state.connections.load(Ordering::Relaxed), + }) +} + +async fn broadcast( + AxumState(state): AxumState, + Json(body): Json, +) -> (StatusCode, Json) { + // The Node server accepts `data` as either a JSON object or a string + // containing JSON (the bench-runner sends the string form). Mirror that + // so the bench-runner's existing driver works unchanged. + let payload = match body.data { + Value::String(s) => serde_json::from_str::(&s).unwrap_or(Value::String(s)), + other => other, + }; + let _ = state.io.to(body.stream).emit("message", &payload).await; + (StatusCode::OK, Json(json!({ "ok": true }))) +} + +async fn publish_local( + AxumState(state): AxumState, + Query(q): Query, +) -> Json { + let total = q.total.unwrap_or(120); + let interval = q.interval.unwrap_or(500); + let stream = q.stream.unwrap_or_else(|| "benchmark".to_string()); + let delay = q.delay.unwrap_or(0); + + let io = state.io.clone(); + let stream_for_task = stream.clone(); + tokio::spawn(async move { + if delay > 0 { + tokio::time::sleep(Duration::from_secs(delay)).await; + } + for seq in 1..=total { + let sent_at = unix_millis(); + let msg = json!({ "seq": seq, "sentAt": sent_at, "text": format!("msg_{}", seq) }); + let _ = io.to(stream_for_task.clone()).emit("message", &msg).await; + tokio::time::sleep(Duration::from_millis(interval)).await; + } + }); + + Json(json!({ + "status": "publishing-local", + "total": total, + "interval": interval, + "stream": stream, + "delay": delay, + })) +} + +fn unix_millis() -> i64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0) +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let port: u16 = env::var("PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(3000); + + let connections: ConnCounter = Arc::new(AtomicU64::new(0)); + + let (layer, io) = SocketIo::builder() + .with_state(connections.clone()) + .build_layer(); + + io.ns("/", on_connect); + + let http_state = HttpState { + io: io.clone(), + connections: connections.clone(), + }; + + let app = Router::new() + .route("/health", get(health)) + .route("/stats", get(stats)) + .route("/_broadcast", post(broadcast)) + .route("/publish-local", post(publish_local)) + .layer(layer) + .with_state(http_state); + + // Bind IPv6 any (`[::]`), which is dual-stack on Linux and, crucially, + // is what Railway's private network (*.railway.internal) routes over. + // Binding 0.0.0.0 (IPv4-only) makes the service unreachable internally. + let addr = format!("[::]:{}", port); + let listener = TcpListener::bind(&addr).await.expect("bind"); + info!(addr = %addr, "socketioxide bench server listening"); + axum::serve(listener, app).await.expect("serve"); +}