PHP 8.0: ignore the JIT, take the rest

JIT on, JIT off, the same Symfony endpoint. Difference within noise. Same for a Laravel endpoint. PHP 8.0 came out yesterday and this was the first thing I checked, because every headline is about the JIT. Not a scandal. A web request spends its time in I/O, in MySQL, in framework code full of method calls the JIT cannot do much with. JIT is for tight numeric loops, and your controller has none. If you compute fractals in PHP, congratulations. The rest of us can leave it off. ...

November 27, 2020 · 2 min · Murat Useinov

Stop building running totals in PHP

Two hundred thousand rows over the wire to compute forty numbers. That was our monthly report on MySQL 5.7: fetch all transactions for the period, loop in PHP, accumulate a running balance, compare each row with the previous one, rank customers by volume. Classic 5.7 shape, because the database could not say “previous row” or “rank within group”. We are finally moving that project to MySQL 8. Two years after GA, which by database standards is reckless haste. The first win had nothing to do with performance. We deleted PHP. ...

October 6, 2020 · 2 min · Murat Useinov

Laravel 8: the interesting parts are not on the marketing page

->refundedTwice(). That is the line from Laravel 8 I care about, and it is nowhere on the release page. Jetstream gets the screenshots. Class-based factories and the queue changes get my attention, because both are about production. Factories used to be closures registered through a global function. Now they are classes with states: Order::factory() ->paid() ->has(OrderItem::factory()->count(3)) ->create(); Test data quality decides test quality. Most integration suites on our project test the happy path fifty times, because the default factory returns a fresh, valid, boring record. The bugs live somewhere else. An order refunded twice. A user registered before the migration added the column. A subscription that expired in the middle of renewal. A factory state gives such a monster a name, and once the name exists, people write tests with it. Named ugly data is the cheapest test improvement I know. ...

September 11, 2020 · 2 min · Murat Useinov

Average response time explains nothing

Average response time: 180 ms. Ticket from support: “the API is slow”. Both true. The average is a diplomat. It offends nobody and tells you nothing. Latency is a distribution. Our 180 ms hides a p50 of 90 ms and a p99 above four seconds. One request in a hundred is terrible, and with thirty requests per page load, most users hit that unlucky one regularly. The people complaining are not imagining things. They live in the tail, and the average never visits there. ...

August 4, 2020 · 2 min · Murat Useinov

PHP 8 will retire some of our workarounds

@param int|string $id. I grepped one service this morning: dozens of those, and every one is a wish. The docblock says what the signature could not. First alpha of PHP 8 came out last week, and native union types turn the wish into a contract, checked at runtime and by tooling. Half of my phpdoc can go. Everyone writes about the JIT. I keep thinking about the less shiny thing: how many of our daily patterns exist only to compensate for a missing language feature, and are about to become legacy. ...

July 3, 2020 · 2 min · Murat Useinov

Types are for reading, not for the compiler

OrderRepository in a constructor. Click. I am there. That is how I explore a big Symfony project now, and I noticed my main reason for types has changed. Not bug catching anymore. Navigation. Symfony 5.1 is out, PHP 7.4 is everywhere I work, and the IDE knows every caller and every implementation. Compare with the array-passing style we all wrote for years: public function register($data) { // what is in $data? read three call sites to find out } versus ...

June 27, 2020 · 2 min · Murat Useinov

A deadlock is the database doing its job

Error 1213 in the logs, and the chat lights up: the database is broken. It is not. Two transactions locked rows in opposite order, A waits for B, B waits for A, and InnoDB did the only sane thing: picked a victim and killed it. The cycle is gone. This is a feature. The classic shape is a money transfer. One request moves funds from account 1 to account 2, another from 2 to 1, both update the first account and then the second. Opposite order, instant cycle under load. Tests never show it, because tests do not run two of these in the same millisecond. ...

May 2, 2020 · 2 min · Murat Useinov

Waiting for three APIs, one at a time

Three external calls on one page: prices, stock, delivery estimate. Each answers in about 300 ms. The page waits a full second, because we call them one after another. PHP is synchronous, what can you do. Turns out, something. Symfony HttpClient is lazy. request() sends and returns immediately. The waiting happens when you read the response. So start all three, read later: $prices = $client->request('GET', $pricesUrl); $stock = $client->request('GET', $stockUrl); $delivery = $client->request('GET', $deliveryUrl); $data = [ 'prices' => $prices->toArray(), 'stock' => $stock->toArray(), 'delivery' => $delivery->toArray(), ]; Under the hood it is curl multi. Three requests fly at once, wall time is the slowest one instead of the sum. Our second became 350 ms. No swoole, no reactphp, no async rewrite. Same boring controller. ...

April 21, 2020 · 2 min · Murat Useinov

Sanctum and choosing how much auth you need

Airlock lived for about a week. Laravel 7 shipped it this month, a trademark scare followed, and it became Sanctum. Fast rename, same idea. And the idea is good, because it names a problem people solve badly. The problem: your own SPA needs to talk to your own API. For years the reflex was OAuth2. Install Passport, stand up an authorization server, issue JWTs to a frontend on the same domain as the backend. All that machinery to authenticate first-party code against itself. OAuth is a delegation protocol, it lets a third party act for a user. When there is no third party, you are running a passport office for your own family. ...

March 26, 2020 · 2 min · Murat Useinov

A Redis lock is a promise you cannot fully keep

Two workers, one order, processed twice. Every project gets this day. Someone writes SETNX, calls it a distributed lock, closes the ticket. I want to slow down here, because the ticket is not closed. The small bugs first. A lock needs a TTL, or a crashed worker holds it forever. A lock needs an owner token, or worker A releases the lock of worker B: $token = bin2hex(random_bytes(16)); $ok = $redis->set('lock:order:'.$orderId, $token, ['nx', 'ex' => 30]); And the release must be atomic: compare the token and delete in one Lua script. Check in PHP, delete in a second command, and there is a gap. Something will land in that gap. ...

February 26, 2020 · 2 min · Murat Useinov