intertwingly

It’s just data

Mastodon on Spinel


The posts in this series so far have described machinery — an oracle, an IDE, a flow analyzer — and gestured at a destination. This post states the destination plainly, because I've learned that goals you state plainly are the ones you can't quietly shrink.

The goal has a two-part success condition: Mastodon — a real, large, heavily-deployed Rails application — passes its own test suite when compiled by Spinel, and the build step earns the status of a supported configuration. That second half deserves precision. Mastodon is deployed widely, and it isn't important that all Mastodon deployments run on the same runtime. But "supported configuration" is a deliberately higher bar than where JRuby and TruffleRuby sit today — "it might work, but is unsupported" — which is precisely why no operator bets production on them. Supported means checkable artifacts: a CI job in the matrix, a documented deployment option, issues that get triaged rather than closed as "unsupported runtime." It's a bar someone else decides you've cleared, so the deadline — end of 2026 — binds what's mine to control: the suite passing, the evidence assembled, the ask credibly made. Behind the headline sits the project's real question, unchanged since the beginning: how much of Rails transpiles, honestly accounted, with a per-target ledger of what doesn't rather than a demo reel of what does.

The main path runs in three stages: analyze (Roundhouse ingests and type-checks Mastodon, driving diagnostics toward zero errors — the IDE post was this stage made visible), cruby (the transpiled output runs and passes tests on stock MRI, binding real gems like Sidekiq where reimplementation adds nothing), and spinel (the whole reachable graph compiles ahead-of-time and runs). Parallel lanes de-risk each stage before the goal depends on it: lobste.rs proves the emit path at a tenth the size, and — the reframing that unlocked the rest of this post — Mastodon itself turns out to be a composable set of subsystems rather than a monolith to be swallowed whole. Its production topology is three services: the Rails web tier, a fleet of Sidekiq worker processes, and a streaming server written in Node.js. That topology exists largely to work around MRI's concurrency constraints — the GVL shapes the worker fleet; per-connection thread cost is the entire reason the streaming server is a separate program in a different language, one that pays for its existence by duplicating Mastodon's filtering logic in JavaScript. Spinel's substrate removes those constraints, so the services can be replaced piecewise — with PostgreSQL and Redis retained as the durable, load-bearing infrastructure they genuinely are, and Sidekiq's API and Redis wire format honored as a specification, so replaced and unreplaced pieces interoperate during the transition.

That yields a ladder in which every rung is independently shippable and every rung's substrate feeds the next. First, a hand-ported replacement for the streaming server — a drop-in binary any operator can evaluate on its own merits, no transpiler knowledge required. Then the ActivityPub delivery workers: a Spinel binary draining an unmodified Mastodon's queues over the Sidekiq wire format. Then the transpiler takes over where hand-porting stops paying — the distribution tier, the job graph, ultimately the full application — converging with the main path's third stage from the other direction. Each rung either produces a production artifact that demonstrates value to strangers on its own terms, or surfaces a structural blocker early enough to fix upstream. A goal with a deadline needs both properties.

Already done

This is not a plan waiting to start. As of today:

A fix is merged into Mastodon. mastodon#39738 — an N+1 query fix on the admin collections page, found during the flow-analysis validation work — was merged upstream about eight hours after filing. One data point, but the right kind: tooling-led findings, submitted as ordinary contributions, get reviewed and accepted.

spinel-redis exists and is published. A Redis client in pure Spinel Ruby — RESP protocol, typed per-command methods, pub/sub with the same nested block API as the redis-rb gem. Its conformance suite is redis-rb's own examples, and its oracle is the real gem: the same test flows run under CRuby against redis-rb and must produce byte-identical output. That oracle earned its keep on day one, catching that redis-rb 5 removed the []/[]= sugar its own bundled example still uses. The package is also the first PR ever opened against Spinel's new package index.

spinel-pg exists and is published. A PostgreSQL wire-protocol client, including SCRAM-SHA-256 authentication implemented in pure Spinel Ruby — SHA-256, HMAC, and PBKDF2 written from scratch and pinned to the RFC test vectors, byte-exact against a real server on the first compile.

spinel-mastodon-streaming runs. The first ladder rung, live: health endpoint, public timelines over SSE fed from real Redis pub/sub, the full Mastodon WebSocket client protocol (upgrade, subscribe/unsubscribe frames, the legacy query form, Mastodon's double-encoded event envelopes), and real OAuth token authentication against Mastodon's actual database schema via spinel-pg — token precedence rules matching the Node implementation, 401 before upgrade, presence keys with the same TTL semantics the Rails side expects.

The first number is measured. At 300 authenticated streaming connections: the Spinel binary went from 2.1MB resident at idle to 15.8MB; the Node server it replaces went from 193MB to 202MB. That's 93× at rest and 12.8× at 300 connections. And the honest part, because these posts publish the numbers that don't flatter: the marginal cost per connection is currently 45KB against Node's 28KB, which means the curves would cross around eleven thousand connections. The "order of magnitude at any scale" I expected does not hold yet; per-connection residency is now a named optimization target with a versioned benchmark script to keep it honest.

The substrate got hardened by the attempt. The streaming work found four byte-versus-character bugs in the runtime's WebSocket codec — the kind that only fire when a client's randomly-chosen mask bytes land wrong, which is to say: in production, eventually, unreproducibly. It also filed three Spinel compiler defects with minimal reproductions — including a left-to-right evaluation-order violation in && chains, the sort of thing that would have been a nightmare to diagnose inside a transpiled Mastodon. All three were fixed upstream within a day. Two more issues — binary-safe string search, binary-key crypto parameters — are filed and open. This is the ladder doing exactly what it's for: every one of these was found on a two-thousand-line subsystem with a standalone payoff, not discovered a year from now load-bearing under the whole application.

What's next

The streaming binary grows toward full conformance — hashtag, list, and direct streams; the authorization matrix; a record/replay harness against the Node implementation — and toward the per-connection memory target. Behind it, the delivery workers wait on one piece of infrastructure (TLS bindings, for which working donor code already exists), and the main analyze-stage work continues as before. Progress is checkable in the repos linked above; the numbers will keep appearing here, whichever way they point.


Roundhouse is open source: dual-licensed MIT / Apache-2.0. Issues and discussion welcome.