The Other End of the Wire

Every network program the language had ever run was a client — it dialed out, spoke, hung up. The fifth capability tool was the mirror image: a key-value server speaking the Redis wire protocol, waiting for connections instead of making them. The missing primitive took an afternoon, as usual. What the server actually delivered was the first honest stress test of the language's concurrency — and the discovery that sharing a mutable map across threads compiled cleanly and silently lost writes.

meredogfoodserverconcurrencylanguage-design

This part is about what happens when a program refuses to end. Everything built so far — the compilers, the games, the inspectors, the filters — runs, finishes, and hands its memory back to the operating system with the process. A server does not have that luxury. It waits, serves, and keeps waiting, and anything the language quietly wastes or quietly corrupts stops being invisible and starts being a bill. Over the next episodes that bill arrives several times, each one exposing something the language had been getting away with. The first delivery came from an unexpected direction: not memory, but the type system’s promises about threads.

One primitive short of a server

The tool was a small key-value server speaking the Redis wire protocol, so a stock client could talk to it. The measured gap looked familiar by now: the runtime had a complete TCP client vocabulary — connect, read, write, close, all on plain file descriptors — grown long ago for the database clients, plus one HTTP-specific server helper that read a request, wrote a response, and hung up. Nothing could accept a raw connection and keep it open. The fix followed the well-worn pattern: two new primitives, listen and accept, mirroring connect against the same descriptors-and-buffers FFI, a few dozen lines each. As with every capability episode before it, the named gap was the shallow part. A single-connection echo server worked the same afternoon; the protocol handling — parse a command frame, apply it, encode a reply, repeat without hanging up — was ordinary code in a language that has long been comfortable with ordinary code.

The compile that should have failed

Then came concurrency. Serving many clients means many threads reaching for one store, and the obvious first draft did the obvious thing: spawn a thread per connection and let them all touch the shared mutable map. The compiler accepted it. The language has a Send/Sync-style discipline precisely so that this kind of program is either safe or rejected — region-bound values are supposed to be thread-local, and the design documents said as much. But under a stress test — eight clients, each writing a key and immediately reading it back — about two percent of the reads came back empty. A SET acknowledged, a GET returning nothing, on a store whose runtime is a lock-free array being grown and indexed by racing threads. The map had crossed the thread boundary with the checker’s blessing and then behaved exactly like the data race it was. The why turned out to be a single optimistic default: the classifier judged a container by its type arguments, the container’s region marker is an unresolved type variable, and unresolved meant “assume fine.” That bug’s fix — and a second, larger hole it exposed — gets its own episode later in this part, because the honest sequence matters: first the server worked around it, then the language repaired it.

Sharing by communicating

The workaround was not a lock. The language’s channels are its one genuinely thread-safe primitive, so the server was restructured as an actor: one owner thread holds the map and is the only thread that ever touches it; each connection thread parses commands and sends them — with a private reply channel — through a shared request channel. The store never crosses a thread boundary at all, so there is nothing to race on and nothing to lock. Under the same stress test the actor version lost zero writes out of thousands. This was also the first real load ever placed on the channel machinery, which had until now only appeared in tests and toys — every command in flight is two channel hops, and the numbers held. The pattern earned a name in the codebase’s vocabulary: share by communicating, never by handing a mutable structure across spawn. When the type system later learned to reject the naive version, this server — unchanged — became the regression test for the blessed pattern.

The bill arrives

The actor server worked, survived hundreds of sequential connections thanks to a recycled buffer pool, and answered a real Redis client correctly. Then the stress test ran a little longer, and at around eight thousand commands the process died — not from a bug in the server’s logic, but from region OOM: the language’s program-lifetime memory region, where every reply string and parsed command quietly lands, had a fixed capacity and no notion of giving anything back. A short-lived tool never notices this. A server is nothing but the long run. That failure — what it revealed about growth versus reclamation, and the two-stage repair that ended with this same server holding flat memory under hundreds of thousands of commands — is the story the rest of this part tells.

← Back to Mere: Building a Language