How it fails safe
How it fails safe
A change reaches a host through a sequence designed so that every failure has a path back. None of these is decoration; each exists because the obvious approach takes a host down, and each is tested against a real master.
Only pm.* keys, in one file
The tool writes one file, zz-fpm-tune.conf, into the directory your master
already includes. It contains only pm.* settings; your pool configuration
(listen, user, everything else) is not touched. PHP-FPM merges a section
defined across several included files, so repeating each section header with just
these keys overrides them and leaves the rest as you wrote it.
Deleting that file returns everything to what you configured. The next run writes a fresh one from what it can see; it does not put the old overrides back.
Validated against a sandbox first
Before anything reaches the live directory, the change is rendered and checked
with php-fpm -t against a copy of the pool directory. A configuration php-fpm
would reject never reaches the directory it globs, not even for the length of a
fork. --dry-run stops there: it renders and validates and writes nothing.
One atomic write
The change set is indivisible. A growth and the reduction that funds it reach the host together, in a single rename, or not at all. A file php-fpm might read mid-write is never on disk: a reader sees the old file or the new one, never half of either.
A reload, never a restart
The master is reloaded with SIGUSR2, php-fpm's graceful reload: it re-reads the config and cycles workers without stopping the service, so an in-flight request is not killed and the pool is never fully down. A daemonized master comes back under a new pid (php-fpm's own default), which is followed rather than mistaken for a death.
One honest caveat, and it is php-fpm's, not this tool's. On reload php-fpm recreates
a pool's listen socket, so under concurrent load a request arriving in the
sub-second window before the new socket is accepting can fail to connect and get a
502. It is rare, and this tool keeps it rare on purpose: a resize only reloads when
the change clears the hysteresis threshold, so a
pool is not reloaded for every small drift. If even a resize-time blip is
unacceptable, a TCP pool (listen = 127.0.0.1:9000) with SO_REUSEPORT hands the
socket over without the recreate window, where a unix-socket pool cannot. Sizing is
identical either way.
Rolled back if the master does not survive
Validation forks a separate process with no sockets to bind; a live master can
still fail to initialise on a configuration that validated. If the master does
not come back from the reload, the previous file is restored and the master
reloaded onto it. If even the restore cannot be written (a full or read-only
directory), you are told exactly which state the host is in, because the operator's
next move depends on it: a rejected file that was never signalled is not yet
dangerous, but a master that is down and cannot be put back needs a systemctl start now.
Recoverable across a crash
What is about to be written is recorded first (path, phase, and a hash of the intended content), and the record is made durable before the master is signalled. An interrupted run is finished or undone on the next start. The rollback itself is rehearsed against a sandbox before it is performed, because a configuration can be broken by something that is not this tool, and reverting a change that would not have fixed it makes things worse. See Recovering a host.
One writer at a time
A lock on the pool directory (not just the state file) stops two processes
writing the same file. It is keyed on the directory itself, at a fixed path, so a
second run pointed at its own state file and backups is still refused. A crashed
process cannot leave a stale lock: it is an flock the kernel drops on exit.