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.…

A Lock Is Only as Good as Its Release

SETNX gets all the attention, but most of the bugs in ePHPm's KV locks were on the release side. This is how blind deletes, a missing compare-and-delete and a cluster where two nodes could both win got fixed.

9-Minute Read

Timeline: request A's lock expires, B acquires it, A's blind DEL frees B's lock and C gets in

Every “build a lock on Redis” tutorial spends its time on acquisition: SET key value NX PX 30000, one winner, done. The release gets one line, DEL key. That line is where most of the lock bugs I’ve had in ePHPm lived. Over about three months the same mistake showed up in three places. First the native session handler, then two of the Composer wrapper packages, and finally the cluster. Each time it wore a slightly different costume.

Step Debugging a Worker That Never Ends

A persistent PHP worker never finishes its request, so Xdebug never sees your trigger. Here's how ePHPm routes the debugged request to its own fresh worker instead of trying to reset anything.

12-Minute Read

Diagram of the ePHPm router sending untriggered requests to the warm worker pool and Xdebug-triggered requests to a one-request debug lane

Persistent workers are the big performance win in modern PHP. You boot Laravel or Symfony once, keep the container in memory, and serve thousands of requests out of the same process. ePHPm has had a worker mode for a while. Like most worker runtimes it had a quiet hole in it. You couldn’t step debug a request with Xdebug. I wanted that fixed without patching Xdebug and without giving up the warm pool. The answer ended up being to stop fighting the constraint and route around it. The debug…

WebSockets in ePHPm, From Zero to a Working Chat

How to run WebSockets in ePHPm with a single PHP file. It covers turning them on, sharing the login session, pushing messages from normal HTTP requests, serving over TLS and the limits to know about.

14-Minute Read

Diagram: browsers hold sockets to ePHPm's Rust session tasks and registry; each connect, message and disconnect runs websocket.php once on the PHP thread pool, and ordinary HTTP requests push through ephpm_ws_broadcast

I covered the design of ePHPm’s native WebSockets in a few paragraphs of the odds and ends post. Rust owns the socket and PHP runs once per event. An idle connection never costs you a PHP thread. That was the why. This post is the how. We’ll build a small chat app with login, per-user push notifications from ordinary HTTP requests and a browser client. Then we’ll serve it over TLS. Last come the limits, the sharp edges and how to debug it when the browser only tells you 1006.

The Bugs Were Our Own Linker Wraps: Making a PHP Timeout Actually Time Out

max_execution_time did nothing in ePHPm because we had wrapped zend_set_timeout to a no-op. The SDK already had per-thread timers. We just had to stop switching them off.

8-Minute Read

Side by side: a process-wide SIGPROF timer that can land on the wrong thread, versus a per-thread POSIX timer that delivers SIGRTMIN to the PHP thread that armed it

In the stack guard post the villain was a -Wl,--wrap linker flag that turned off PHP’s C-stack overflow guard. It wasn’t the only one. Until August, max_execution_time did nothing in ePHPm on Linux, for the same reason. We had wrapped the function that arms it to a no-op. A runaway script ran until the HTTP layer’s request timeout gave up on it and sent a 504. The PHP thread kept burning CPU after that.

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…

Every Way a Tenant Escaped: A Cross-Tenant Breakout Field Guide

Eight ways one site broke out of its sandbox on a multi-tenant PHP server, all found by attacking it on purpose, all fixed.

10-Minute Read

Table of tenant isolation boundaries in ePHPm, how each was crossed, and the PR that fixed it

ePHPm can serve a directory full of sites out of one process. Point sites_dir at a folder and every subdirectory is a virtual host. The Host header decides which one you get. That’s how the PR preview environment runs. Every pull request gets a subdomain, and the code behind it is whatever somebody pushed. One process holding many tenants means ePHPm enforces every per-tenant boundary itself, in-process. There’s no container wall between site A and site B unless you add one.

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