JSONB or a normal table

Audit snapshots, product attributes, and a document with two hot keys. Three cases from one Postgres project, one question for each: column or JSONB. I answer it by looking at queries, not at data. Audit snapshots. When an order changes, we store the whole previous state. Nobody queries inside it. It is read as a blob, whole, rarely, by a human during an incident. Perfect JSONB. Normalizing it would mean a dozen tables for data nobody joins. ...

July 16, 2022 · 2 min · Murat Useinov

Argument resolvers and the HTTP border

Forty lines of $request->get() before the first line of real logic. That was the controller nobody wanted to touch, and the Symfony 6.1 upgrade was my excuse to open it. An action that reads ten request fields by hand does two jobs. It translates HTTP into data, and it runs the use case. The first job is boring and repeated in every action, which is exactly why it should not be written by hand ten times. Symfony has argument resolvers for this since 3.1. People just do not use them for their own types. ...

June 13, 2022 · 2 min · Murat Useinov

Redis 7 functions, same old rule

FCALL update_if_newer 1 user:42 17. One round trip, a name instead of a SHA, and it survives a restart. Redis 7 came out last week, and Functions are the feature I care about. Everything EVAL scripts should have been. The problem they solve is real. Take compare-and-update: set a value only if the stored version is older. From PHP it is three round trips and a race between them. WATCH/MULTI works but is ugly and retries under contention. A Lua script does it atomically in one trip, and that is why we all wrote Lua scripts. But scripts are anonymous blobs. The application carries the source, sends it on every deploy, and debugging starts with “which SHA is this”. Functions fix the operational part: the library lives in Redis, has a name, has a version. ...

May 5, 2022 · 2 min · Murat Useinov

Retry is a strategy, not a loop

A failed job that retries every second, one hundred times, against a payment provider that is already down. That is a small DDoS with a queue in front of it. If the provider needs five minutes to recover, hammering it for the first two achieves nothing except log volume. Symfony Messenger has everything needed. You just have to configure it on purpose: framework: messenger: transports: async: dsn: '%env(MESSENGER_TRANSPORT_DSN)%' retry_strategy: max_retries: 5 delay: 2000 multiplier: 4 failure_transport: failed Two seconds, eight, thirty two, and so on. Exponential backoff gives the external service room to breathe. I also add jitter in a custom retry strategy, because a hundred messages that failed together will otherwise retry together, in one synchronized wave. Same failure, five times. ...

April 27, 2022 · 2 min · Murat Useinov

Backfill in batches, not in one UPDATE

Sixty million rows in orders, a new nullable column total_cents from expand-and-contract, and a Friday afternoon. The question is how to fill it. The naive answer is one statement. UPDATE orders SET total_cents = ROUND(total * 100). On a test database it works. On production it holds row locks on the whole table for the whole run, writes one giant chunk of WAL, and the replica falls minutes behind. On MySQL the binlog gets the same present. One project I worked on learned this on that Friday. Since then I backfill only in batches. ...

March 17, 2022 · 2 min · Murat Useinov

Laravel 9 and the boring major

One evening. That was the Laravel 9 upgrade on a mid-size project last week. Most of the diff was composer.json and a few config files. Laravel 9 is out this month. Symfony 6 components under the hood, PHP 8.0 minimum, Flysystem 3. And from now on, one major per year. The upgrade guide is short, and the “high impact” section is shorter. This is a policy: small majors, often. I want to defend this policy, because I remember the other kind. A project that sat on an old major for three years, because “we will upgrade when there is time”. There is never time. Then the version goes EOL, a security fix forces the jump, and you pay three years of drift in one horrible month. Packages you depend on drop support one by one, and suddenly it is not a framework upgrade anymore. It is archaeology. ...

February 25, 2022 · 2 min · Murat Useinov

Order status is an enum now

One row in orders had status 'canceled', with one l. Nobody knows how long it sat there. The enum found it on the first day. PHP 8.1 enums are six weeks old, enough time in one project to say something practical. The first candidate was obvious: order status. For years it was a class with string constants, Order::STATUS_PAID, Order::STATUS_SHIPPED. Constants look safe, but nothing stops a function from receiving 'payed'. The parameter type was string, and string accepts everything. ...

January 8, 2022 · 2 min · Murat Useinov

Symfony 6.0 is boring, take the compliment

The deprecation counter in the profiler, three digits. That is where one project stands this week, and that number is the whole Symfony 6 migration. Symfony shipped 5.4 and 6.0 together at the end of November, same code, same day. 4.4 and 5.0 worked exactly like this two years ago, so the story is rehearsed. 6.0 is 5.4 with the deprecated code deleted. You never migrate to a major. You migrate to the last minor, and the major is a formality. ...

December 9, 2021 · 2 min · Murat Useinov

PHP 8.1: enums, readonly and a word about Fibers

'payed'. One letter, half a day of debugging, a few years ago. The string walked straight through OrderStatus::isValid(), because someone had added it to the constants list “for compatibility”. Every project I touched has that class: a bag of string constants, an isValid() helper, and a prayer. The type system never knew these strings were special. PHP 8.1 came out yesterday. Point release on paper, and it brings the feature I wanted since forever. Enums. ...

November 26, 2021 · 2 min · Murat Useinov

Generics we already have

A template calls getEmail() on an Order at three in the morning. The method returned array, the docblock said “array of User”, and eight months later someone put an Order in it. Everything was fine until it was not. PHP still has no generics and will not get them soon. Meanwhile PHPStan and Psalm shipped them anyway, in docblocks. Checked at analysis, not at runtime. For everyday backend work that turns out to be most of the value. ...

October 9, 2021 · 2 min · Murat Useinov