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

Cursor pagination is part of your API contract

Someone inserts a row while the client walks the pages, and the whole window shifts. Page two shows an item the client already saw on page one. Or an item falls between pages and the client never sees it. For a feed this is annoying. For an export or a sync endpoint this is a data loss bug that nobody can reproduce. I used to think keyset pagination is a performance trick. Deep OFFSET is slow, keyset is fast, end of story. Now I think the performance part is the boring half. Offset pagination over a changing dataset lies to the client. That is the interesting half. ...

January 8, 2020 · 2 min · Murat Useinov

Static state outlives the job

One static array, from one of our projects: class Settings { private static $cache = []; public static function get($tenantId, $key) { if (!isset(self::$cache[$tenantId])) { self::$cache[$tenantId] = self::load($tenantId); } return self::$cache[$tenantId][$key] ?? null; } } A reasonable per-request cache. Under PHP-FPM it died with the process. In a queue worker it lives forever. PHP had one great architectural feature nobody put in the manual: the process died after every request. Leak memory, cache nonsense in a static, forget to close things. Did not matter. The dying process forgave everything. ...

December 22, 2019 · 2 min · Murat Useinov

PHP 7.4: typed properties first, preloading later

Two lines of a class, and half of my docblocks can be deleted: class Money { private int $amount; private string $currency; } PHP 7.4 came out yesterday, and this is the part I start using on Monday. Those docblocks existed only to say @var int. Now the language says it, and unlike the docblock, it checks. Assign a string, get a TypeError at the assignment, not a strange bug three layers later. There is one new state to learn. A typed property without a default is uninitialized, and reading it before the first write throws. This is the feature. “Object exists but is not filled yet” finally fails loudly instead of pretending to be null. ...

November 29, 2019 · 2 min · Murat Useinov

PostgreSQL 12 inlines your CTE

grep -rn "WITH " src/ was the first thing I ran after upgrading to PostgreSQL 12 this month. I was looking for a trick that had stopped working. For years WITH was an optimization fence. The planner materialized the CTE first and only then ran the outer query. Everyone used this both ways. As a bug: you wrap a subquery into a CTE for readability, the planner stops pushing conditions inside, a fast query becomes a scan of half a table. As a feature: you write a CTE on purpose, to pin evaluation order and stop the planner from being creative. Half of the CTE advice on the internet is really advice about the fence. ...

October 18, 2019 · 2 min · Murat Useinov

Cleaning up before Symfony 5

Symfony 5.0 is 4.4 minus everything deprecated. That one sentence is the whole upgrade plan. Both land in November, and if the application runs on 4.4 with zero deprecation warnings, the major is a version bump. Two months is enough to get there in small chunks, with no freeze and no heroic branch that lives for six weeks. The order that works for me. First, get to 4.3 and make the deprecation report visible. The PHPUnit bridge prints the summary after the test run. In dev, the profiler collects the same warnings per request. You cannot clean what you do not see. The first report will be depressing. Fine. It is a todo list, not a verdict. ...

September 25, 2019 · 2 min · Murat Useinov

200 OK is not health

Route /health, returns ok, everyone happy. That is the first version of every health endpoint on the project we are moving to Kubernetes. Then somebody pastes the same path into livenessProbe and readinessProbe, and a thirty second database hiccup becomes a long evening. The two probes ask different questions. Readiness asks: should this pod get traffic right now. Here it is correct to check dependencies. Database unreachable, cache cold, migrations still running: answer no. Kubernetes takes the pod out of the Service, traffic goes to the others, the pod returns when the world improves. Failing readiness is cheap and reversible. A polite “not now”. ...

August 7, 2019 · 2 min · Murat Useinov