Skip to content

feat(services): php-fpm pools, one per installed PHP (T32) - #2

Merged
haiquang9994 merged 17 commits into
masterfrom
feat/t32-php-fpm-pools
Aug 19, 2026
Merged

feat(services): php-fpm pools, one per installed PHP (T32)#2
haiquang9994 merged 17 commits into
masterfrom
feat/t32-php-fpm-pools

Conversation

@haiquang9994

Copy link
Copy Markdown
Collaborator

Roadmap task T32 — one php-fpm pool per installed PHP, a socket or port per pool, and reload by
SIGUSR2. This is the first service whose binary does not come from a package: it lives inside a
runtime_installs directory, so services grew a second typed parent (runtime_install_id, with a
CHECK that exactly one of the two is set) rather than a fake packages row. It is also what the
other half of T28 was waiting for, and the first refusal runtime.uninstall has ever been able to
make.

What the task settled

  • One recipe, two spec shapes, no #[cfg]. Recipe::spec branches on cfg!(windows) — a value,
    not an attribute — so both arms compile everywhere and a unit test exercises the arm the machine is
    not. Which binary it is comes from the artifact's own provides map, so the index decides and no
    recipe writes a path down.
  • php-fpm does not exist on Windows, and php-cgi.exe was measured rather than assumed: it is
    already a process manager — php-fpm with pm = static, configured through the environment instead
    of a file — so this task writes no supervisor of its own.
  • One set of overrides on every system (max_children, max_requests, request_timeout,
    ready_timeout_ms, stop_grace_ms), rendered into a file or an environment as the platform
    requires. Deliberately no pm = dynamic, which Windows cannot express.
  • ReloadBehaviour::Signal: mixengine-platform gains CAN_SIGNAL and Supervised::signal,
    addressed at the leader where a stop is addressed at the group, and answering unsupported on
    Windows exactly as ask_to_stop does.
  • Nobody calls service.create for a pool. An idempotent hook runs after every install and at
    boot, so a PHP installed by an earlier build gets its pool with no data migration, and a home whose
    row was deleted by hand repairs itself.

How it is judged

Against a real PHP on all three systems, through the FastCGI protocol — a pool that listens and
cannot execute anything accepts a connection exactly like one that works. mixengine-testkit gained
a minimal responder client; crates/mixengine-cli/tests/php_fpm.rs installs a real artifact through
runtime.install, starts the pool the hook created, reads a body back, changes an override, and
asserts what each system actually does: Unix serves the new configuration from the same pid, Windows
says in daemon.log that it cannot and keeps the old one.

Fixes CI asked for

CAN_SIGNAL was on neither Unix re-export list neither Linux nor macOS compiled
php-fpm refuses an include= glob whose directory is missing, and --test opens error_log the recipe rendered a file a real php-fpm rejected
a reload was logged after the patience sleep, not when it was sent
the fixture PHP could not answer --test --fpm-config it took the whole declared set down with it
MIXENGINE_PHP_RUNTIME did not cross into the isolated namespace
the signal test raced the kernel and did not distinguish leader from group

Four of those six exist only on Unix and cannot be seen from Windows, which runs no validator at all.

Written down rather than fixed

  • macOS answers EPERM to kill(-pgid, ...) when every member of the group is already a zombie, and
    unix/process.rs forgives only ESRCH. No test reaches it and the runner does not either.
  • The daemon's log ring is fed by a task ordered against nothing, so a service's first lines can
    reach current.log and never the ring — T16c, against T16b's path, caught by a run of this
    branch rather than caused by it.

CI is green on all three operating systems: lint, test ×3, bench ×3.

`sys` is `linux/mod.rs` or `macos/mod.rs`, not `unix/` — each names what it
takes from there, and the new constant was written without being listed, so
`process::CAN_SIGNAL` resolved on Windows alone.
…(T32)

Two refusals, both measured against 8.3.33 and both invisible on Windows,
where there is no validator to run:

- `include=<etc>/pool.d/*.conf` is a hard error when the directory is
  missing — not the warning an empty one gets — and that directory cannot
  exist for the first `--test`, which judges a staged file while
  `include` names the installed path. Phase 4 brings the line and the
  directory together; the file says so where the line was.
- `--test` opens `error_log`, and the service log directory is otherwise
  created by the log sink, which is to say at the first start. Rendering
  creates it now, before the render is judged.
…nds (T32)

The signal is delivered by the time `signal` returns; the wait after it is
there so the next change does not land on a pool still cycling its workers.
Logging on the far side of it stamped daemon.log ten seconds late.
…k (T32)

`--version`'s sibling and there for the same reason. A declared set fails
whole when one row cannot be rendered, so a fixture PHP that refused
`--test --fpm-config` took every `service.*` call in the daemon suite down
with it on both Unix systems. The file is read rather than assumed.
…(T32)

The sudo fallback passes the environment explicitly, and the new variable
was not on the list — so that leg would have warned that no PHP was found
while one was sitting in the runner's temp directory.
…acing it (T32)

The fixture handles no SIGHUP and Rust installs none, so the default
disposition ends it — which the old test read as "it survived" only because
the check beat the kernel to it. It now waits for the leader's lock to free
(that is the delivery) and asserts the child's is still held (that is the
target). Keeping the child alive also keeps the group alive, so the stop
afterwards is an ordinary stop: macOS answers EPERM to a group whose every
member is a zombie, which is how this surfaced and which the roadmap now
records.
…s exist

A Windows CI run of this branch served a log tail out of `current.log` because the
daemon's ring was still empty, and the mechanism the code supports is a real gap
rather than only a slow runner: `Runner::relay` subscribes to a capture whose reader
threads are already broadcasting, so the lines printed in between reach the file and
never the ring. Written up as T16c against T16b's path, since T32 caught it rather
than caused it.

The CI job table in build-and-release.md describes six jobs and three of them exist;
say so where the table is, not only in ci.yml's opening comment.
@haiquang9994
haiquang9994 merged commit d206a78 into master Aug 19, 2026
@haiquang9994
haiquang9994 deleted the feat/t32-php-fpm-pools branch August 19, 2026 17:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant