N+1 hides on your laptop

Ten rows in the dev database, on the same machine, 0.1 ms per query. Ten extra queries is one millisecond. The page feels instant, the code ships. Production has a thousand rows and the database one network hop away. Round trip is about a millisecond even in a good datacenter. A thousand queries is a second of pure network waiting. Not slow SQL. Each query is fast. The plural is slow. ...

July 5, 2019 · 2 min · Murat Useinov

One use case, one class

Four hundred lines in a checkout action, half of them inside one try. That is one way a project dies. The other is OrderManager: thirty methods, six injected services, and nobody can say what the class is for, because it is for everything about orders. Both diseases have the same cure and it is embarrassingly simple. One use case, one class, one public method. final class PlaceOrder { public function __construct( OrderRepository $orders, PaymentGateway $payments, EventDispatcher $events ) { ... } public function handle(PlaceOrderCommand $command): OrderId { // the whole story, top to bottom } } The shape answers three questions by itself. What comes in: one command object, a thing you can log, validate, put in a queue. What it needs: the constructor lists dependencies of this use case, not of orders in general, so when PlaceOrder suddenly needs the mailer you see it and can ask why. What comes out: one result. Reading handle() from top to bottom tells the whole story of placing an order. No chapter hidden in a base class or a trait. ...

June 26, 2019 · 2 min · Murat Useinov

Messenger retries and the failure transport

A log full of the same stack trace, every second, all night. That is what a queue looks like when a handler throws on a malformed payload and the retry policy is “forever”. Symfony 4.3 is out this week, and Messenger in it finally has an answer for that night. Retry is good for transient errors. Network blinked, deadlock, remote API returned 503. Wait, try again, it passes. But some errors are permanent. Malformed payload, entity deleted, a bug in the handler. Retrying those forever means a worker grinding the same poison message until morning. Infinite retry is not persistence, it is denial. ...

May 30, 2019 · 2 min · Murat Useinov

What Redis does when memory ends

Users logged out at random. Not all, not always. Only sometimes, only in the afternoon. Afternoon is when traffic peaks. Traffic peaks fill the cache. The cache lived in the same Redis as the sessions, the instance hit maxmemory, and the eviction policy was allkeys-lru. Redis did exactly what we asked: threw away the least recently used keys, and some of them were sessions of people who went for lunch. ...

April 2, 2019 · 2 min · Murat Useinov

Transactional DDL is not portable

Statement three of a migration fails. Column name typo. On PostgreSQL the first two ALTERs roll back with it, the schema returns to the exact state before the migration, you fix the typo and run again. On MySQL the first two ALTERs are already permanent. Same up() method, same php artisan migrate, same green output. The migration tool gives one abstraction over two very different databases. The abstraction covers syntax. It does not cover what happens on failure. ...

March 18, 2019 · 2 min · Murat Useinov

Eloquent observers and hidden control flow

Three thousand welcome emails. A colleague ran a user import on one project last month: loop over a CSV, $user->save(), go home. Next morning we found the script had also warmed the search index three thousand times and invalidated cache after every row. Nobody wrote that in the import script. The observers did. Laravel 5.8 came out yesterday, and reading the changelog brought that evening back, so here is the note. ...

February 27, 2019 · 2 min · Murat Useinov

One payment is enough

Charge the card, send the receipt, ack the job. The worker lost its Redis connection between step one and step three. The queue delivered the job again. The customer paid twice. Support learned some new words from him. Retries are not an edge case. Laravel retries failed jobs by design, and you want that, because networks blink. So every job with a side effect must answer one question: what happens if this runs twice. “It will not run twice” is not an answer. It will. ...

January 21, 2019 · 2 min · Murat Useinov

PHP 7.3 and the value of small releases

A trailing comma after the last argument of a function call. PHP 7.3 came out at the start of the month, and this is the feature I noticed first, because I meet the missing comma in every second diff: $this->logger->info( 'order created', ['order_id' => $order->id], ); A tiny thing that kills a whole class of noisy diffs, the ones where adding an argument touches the previous line too. Arrays got this years ago. Calls only now. ...

December 27, 2018 · 2 min · Murat Useinov

Deprecations are a to-do list, not noise

SYMFONY_DEPRECATIONS_HELPER='max[total]=20' ./bin/phpunit. Twenty is the number of deprecation warnings we had on the day we started counting. The rule is that it only goes down. Symfony 4.2 came out yesterday, on the six month schedule. With this cadence deprecation warnings stop being an event and become weather. Every minor release brings a new batch, teams train themselves not to see them, and that is a mistake, because a deprecation is the next major upgrade delivered early, in small pieces, with instructions. ...

November 30, 2018 · 2 min · Murat Useinov

PostgreSQL 11: partitions grow up, JIT arrives

jit = on, restart, run the API test suite. Same numbers as before. That was my first evening with PostgreSQL 11, released last week, and it was the correct result. JIT is the loud feature of this release and the misunderstood one. Postgres can now compile expression evaluation into machine code through LLVM. People read “compilation” and expect their endpoints to get faster. They will not. A primary key lookup takes a fraction of a millisecond. There is nothing in it worth compiling, and the compilation itself costs more than the whole query. JIT is for the other kind of query: an aggregate chewing through millions of rows, where the same expression runs so many times that generating machine code for it pays back. ...

October 26, 2018 · 2 min · Murat Useinov