Luther Monson, Staff Software Engineer at SUSE

Luther Monson

Go by way of PHP · Containers · VMs · Windows · Kubernetes

Composer Without PHP: Embedding vivacity in ePHPm

I embedded vivacity, a pure-Rust Composer-compatible installer, in ePHPm so one binary can install a PHP app's dependencies without PHP. This is what it took upstream and in ePHPm, and how it does against Composer on Windows.

10-Minute Read

Bar chart comparing Composer 2.10.3 and ephpm composer on a Laravel install on native Windows

The pitch for ePHPm is one binary in place of php-fpm, nginx and Redis. There was always a hole in it though. To deploy a PHP app you still need to run composer install. That drags PHP and composer.phar back onto the box or into the container build, just for that one step. ephpm composer install now runs a pure-Rust, Composer-compatible installer called vivacity inside the ePHPm binary. One binary serves the app and installs its dependencies. On Windows that works with no PHP installed at all.…

Clustered SQLite in ePHPm, Part 3: CDC Over the Cluster Channel

ePHPm ships Turso's change data capture stream between nodes over one authenticated, multiplexed TCP connection. This covers how that works and the two failover bugs that showed up when the e2e suite killed the primary.

15-Minute Read

Two ePHPm nodes connected by gossip on UDP 7946, the KV data plane on TCP 7947 and the cluster channel on TCP 7948 carrying cdc, snapshot and sql streams

By the end of Part 2 ePHPm v0.7.0 ran exactly one SQLite engine in process. That was Turso, for single-node and clustered mode alike. The sidecar was gone. What I skipped over is how a replica actually gets the primary’s writes, and what happens when the primary dies with data on it. That’s this post.

Calling Rust From PHP Without a Socket, Part 1: The Native Functions

How ePHPm exposes its in-process KV store and SQLite bridge to PHP as plain functions, and what it took to make that safe under ZTS.

14-Minute Read

Two paths from PHP to the ePHPm KV store: over a RESP socket, or through a native function and a function pointer table

ePHPm is a single Rust binary that embeds PHP. It carries its own KV store and its own SQLite-backed database in the same process. For a long time PHP code could only reach that KV store the same way it reaches Redis. It opened a TCP socket to the RESP listener ePHPm runs on localhost, encoded a command and waited for the reply. That works, and every PHP Redis client in existence already speaks it. But it is a strange thing to do when the hash map you’re talking to lives a few pointers…

P99 From 280 ms to 58 ms by Making the Queue Fair

ePHPm had the best throughput in a Laravel runtime comparison and the worst tail of the fast group. The cause was a bounded channel that let new requests cut in line.

9-Minute Read

Bar chart of P50, P75, P90 and P99 latency for FIFO versus barging admission in ePHPm worker mode

A third-party Laravel runtime comparison put ePHPm’s worker mode at the top on throughput. It was about 19% ahead of FrankenPHP on /api/static. It was also at the bottom of the fast group on tail latency, with a P99 of 157 ms against FrankenPHP’s 57 ms. Best throughput plus the fattest tail is a shape worth chasing, because fast execution can’t produce it. Making requests faster lowers the mean and the tail. Only waiting can pull them apart. Specifically, unfair waiting.

Stop Running PHP on tokio's spawn_blocking - Own Your Thread Pool

Why ePHPm moved PHP execution off tokio's generic blocking pool onto threads it owns. How the switch rolled out behind a knob and what it did to memory, throughput and overload.

10-Minute Read

Bar chart of server RSS at 32, 128 and 512 concurrent clients: spawn_blocking grows with concurrency, the owned pool stays flat

For most of its life, ePHPm ran every PHP request on tokio’s spawn_blocking pool. It works. It passes tests and it serves benchmarks fine. It’s the first thing anybody reaches for when they need to call blocking code from async Rust. It’s also quietly wrong for a language runtime. The problems only show up under overload or after a crash. Over the last three weeks it got replaced with a pool of OS threads that ePHPm owns. That pool shipped first behind a config flag, then as…

Clustered ACME in Production: Three Bugs That Took TLS Down

Moving wildcard TLS onto a live 3-node ePHPm cluster found a leader election that wasn't, a certificate only two nodes could see, and an HTTP port speaking TLS.

10-Minute Read

Diagram comparing a sharded KV write that leaves one of three nodes without the certificate to a broadcast write that reaches all three

ePHPm gets its own certificates from Let’s Encrypt. In a cluster only one node should be talking to the CA. The nodes elect an ACME leader over the shared KV store. The leader orders the cert, and everybody else installs it from KV. That all had unit tests and had worked on single nodes for months. Then I tried to move wildcard TLS termination for our preview environment off a separate proxy and onto the 3-node cluster itself. In about two days it found three separate bugs that each took…

Middleware in PHP: Compiling It With elephc, Then Not Needing To

Fitting PHP into ePHPm's native middleware ABI with an ahead-of-time compiler, and what that experiment said about where PHP middleware should actually run.

9-Minute Read

Timeline of an ePHPm request showing the native middleware lane before the body read and php: mounts inside the PHP request

ePHPm middleware runs before PHP. It’s a small versioned C ABI, loaded with dlopen, that gets to look at a request and reject it before PHP boots or the request body is read. That’s the right shape for a gateway. It’s the wrong shape for most PHP shops, who want to write their middleware in PHP. This is the story of compiling PHP into a native .so to fit that ABI and finding exactly where it broke. Then I built the lane that should have existed in the first place.

Security and Performance Hardening of a Rust Database Proxy

What a week of hardening litewire turned up. A dependency had a memory bug, one site could open its neighbor's database, a security rule lived only in a comment and every query paid for a needless 1 MB clear.

11-Minute Read

Bar chart of point SELECT throughput over the litewire MySQL frontend at three revisions: before the command filter, with the filter, and with the poll_read fix

litewire is the piece of ePHPm that lets PHP apps talk MySQL to a SQLite file. I wrote about how it works and about the MySQL quirks WordPress depends on. This one is the less visible half of August:

Rolling Your Own KV Store Instead of Embedding Redis

Why ePHPm has its own Redis-compatible cache built in instead of running Redis. It also covers a bug that hid in it for four months and how it compares to real Redis on the same machine.

11-Minute Read

Bar chart of redis-benchmark GET and SET throughput against Redis 7.4, ePHPm v0.5.2 and ePHPm v0.2.0

Every PHP deployment I’ve run eventually grew a Redis. Sessions, the WordPress object cache, Laravel’s cache and locks, rate limiting. ePHPm is supposed to be one binary that replaces nginx + php-fpm + Redis, so it needed a cache in the box. I didn’t want to write one. This post covers why I did anyway and how it’s built. It also covers the bug that sat in its eviction code from March until last month. Then I compare it against a stock Redis on the same machine with…

Requests That End Wrong: A 200 That Was Really a Crash

Three ePHPm bugs lived in how an embedded PHP request starts and ends. A bailout returned 200, $_POST came up empty and a SIGTERM corrupted the heap.

10-Minute Read

Diagram of a bare zend_bailout being absorbed by php_execute_script's own zend_try instead of reaching ePHPm's guard

The scariest response a server can send isn’t a 500. It’s a 200 that should have been a 500. A crash is loud. The pod restarts, an alert fires, somebody looks. A truncated page with a success status code is quiet. Your CDN caches it and the health check calls the box healthy. A client parses half a document as if it were the whole thing.

Recent Posts

Categories

About

Luther is a 20+ year software veteran specializing in backend APIs and architecting products. Employed at SUSE as the Team Lead for the Containers on Windows Team working on Rancher + Kubernetes