Context
A green concurrency test only shows that the test did not reach the race. In a one-row lost-update experiment the window was a few milliseconds wide, and it moved between my two machines. pg-interleave is a proxy that sits between your code and Postgres so a test can control the order in which statements arrive, instead of hoping the timing lines up.
It is alpha software under the MIT license, published to npm as pg-interleave (latest 0.7.1). The server is written in Rust on tokio and the client is a TypeScript SDK. You point the database URL at the proxy's port, and the test drives it over a small HTTP API.
What it does
- Statement barriers. Hold one statement in the proxy, let another connection run a whole transaction, then release. The lost update becomes a deterministic test with no
sleep. - Torn commits. With
drop_connection_on_committhe commit finishes on the server and the client's socket is then cut. A generic retry helper tries again, and a transaction without an idempotency key inserts twice. A mocked connection cannot show that, because a mocked drop leaves no committed row behind. - Schedule fuzzer. Random delays before queries, including
BEGINandCOMMIT. Each connection gets its own stream derived from a seed with SplitMix64. Client-side timing still varies by a few milliseconds, so a seed gives a close result, not an identical one.
Testing it on software I did not write
I wrote eight tests for pg-boss v12.38.1, a job queue on top of Postgres: an injected 40P01 deadlock in the fetch statement, a torn commit on job completion, a held completion statement, a handler that outlives its expiry, and bursts of concurrent send() calls. None of them found a bug in pg-boss. The expiry test showed pg-boss's documented at-least-once behavior.
The bug in my own tool
My first deadlock test passed, and it should not have. pg-boss writes its fetch statement across several lines, my pattern was a single-line regex, and in a regex . does not cross a newline. The rule never matched, the proxy forwarded the query untouched, and the worker behaved normally. A reviewer found it by running my pattern against the real statement.
Two changes came out of it: the SDK now compiles a RegExp with the dotAll flag and keeps i and m, and every rule handle has assertFired(n), which throws if the rule did not fire exactly n times.
The 44 ms trap
An early version of my README claimed that the fuzzer finds 100% of lost updates. That number was an artifact. The proxy added about 44 ms to every query, which is consistent with Nagle's algorithm interacting with delayed ACKs. With queries that slow, the two transactions overlapped whether or not the fuzzer was on, so the comparison meant nothing. Setting TCP_NODELAY on both sockets dropped the overhead to about 0.2 ms per query, and the plain baseline at a 5 ms offset fell to between 0 and 5 out of 100. I corrected the README and wrote the story up.
Limitations
- Plaintext only. The proxy answers
NtoSSLRequest, sosslmode=requiredoes not work. - Flush pipelines. Drivers that rely on explicit
Flushframes beforeSynccan stall the proxy. - Data-modifying CTEs. The AST selector classifies
WITH u AS (UPDATE ...) SELECT ...as aSELECT. Use a regex for those. - PgBouncer in transaction mode. Backend PIDs change between transactions, so route rules with SQL comment tags.
- Shared state and no auth. Rules live in the proxy process and the control plane has no authentication. Run test files serially and keep it on
127.0.0.1. - One toy benchmark. The race window was a different width on my two machines, and I have not measured the fuzzer on a large real application.
When not to use it
If you can write the scenario as plain sequential awaits on two clients, you do not need a proxy. PostgreSQL's own isolationtester is a better fit when you are testing the database engine. Toxiproxy is the right layer for latency and dropped connections. pg-interleave is for the case in between: application code you do not want to modify, where you want to stop one specific statement.