FINAL DECISIONS — new.apertia.cz server installment

Host 21.davidstrejc.cz (94.130.65.137, Hetzner, 2× Micron 3500 NVMe RAID1, 64 GB RAM) · audited 2026-08-04 · read-only audit, every claim re-verified against repo (file:line) and live host. This page is the decision record; nothing was changed on the server except publishing this page.

Verdict: No PHP memory leak. The box suffers from four compounding, fixable problems: a scheduler retry storm (process churn), JIT enabled for every PHP process on the host (segfaults + ~3.6 GB residency), thermal throttling (hardware), and a shared-search query shape that defeats every index built for it. Fix order is D1 → D2/D3 → D4/D5 → D7. No hardware purchase, no RAM upgrade, no PostgreSQL tuning knobs.

Corrections to the original audit (verified 2026-08-04)

Audit saidVerified reality
RAID md3 mismatch_cnt=2560 (implied maybe benign)md3 is the root filesystem (920 GB /, holds PostgreSQL). Swap is separate md1. The benign swap-mismatch explanation does not apply — D5 is mandatory.
"JIT wrapper host-only" (drift)/usr/local/bin/php8.5-octane is a no-op passthrough (exec /usr/bin/php8.5 "$@"). JIT comes from system php.ini: opcache.jit=1205, 256 M buffer, opcache.enable_cli=Onevery PHP process on the box (all vhosts, all CLI spawns) maps opcache 768 M + JIT 256 M = exactly the 1 GiB /dev/zero region every segfault dies in.
"Several PHP 8.5 segfaults Aug 1–3"166 segfaults since Aug 1 (kernel journal), all at the same offset in the 1 GiB opcache+JIT mapping, mostly short-lived CLI processes — i.e. the scheduler storm is feeding the crash mill.
Candidate commits d57ff2a21a, 85276da811Exist, author Adam Kalíšek 2026-08-03, branch codex/accounting-remediation-implementation-20260803NOT merged to main. They add exactly the missing occurrence/attempt state (+migration, typed ScheduledJobExecutionException, runtime service, tests).
Outbox 4.8 GB (retention gap)Nightly purge already exists (erp:outbox:purge --days=1 --vacuum). Live: heap 334 MB, indexes+toast 4.4 GB — this is index bloat from daily delete churn, not missing retention. ~460 k processed rows/day.
Slow search → "repair query shape"Sharper: trgm + f_unaccent expression indexes already exist, but the runtime emits f_unaccent(COALESCE(col::text,'')) which matches neither index expression, and ORs search_vector @@ with the full ILIKE fallback — the planner can never index-serve it (the repo's own 2026-07-16 migration says so). Fix layer: FilterTextSearch compiler, not new indexes.
Worker RSS 426–456 MiB (PSS unconfirmed)PSS confirmed via smaps_rollup: ~390–400 MB per Octane worker. RSS ≈ PSS — the footprint is real, not shared-page double counting.

Decisions

P0D1 — Kill the active storm today: fix the recipient data.
P0D2 — Scheduler gets occurrence/attempt state via Adam's branch, review + rebase, no blind cherry-pick.
P0D3 — JIT OFF host-wide. A/B on standby only to document the delta, not to decide it.
P0D4 — Thermal: hardware inspection ticket at Hetzner, before any software turbo-capping.
P0D5 — RAID scrub with backup proof first. md3 is the root fs under PostgreSQL — treat it seriously.
P1D6 — One committed runtime contract. The host stops being the only place the truth lives.
P1D7 — Search: fix the compiler to emit what the indexes can serve. No new indexes.
P1D8 — One leads schema truth. Module owns Lead; the tenant-tree copy dies.
P2D9 — Outbox: reclaim the 4.4 GB of index/toast bloat; retention already works.
P2D10 — Hygiene watchlist, re-measured after D1–D3 land.

Execution log — 2026-08-04 evening (non-harmful actions executed)

DONEStorm stopped at 18:10 CEST. The failing job is Send invoice payment reminders (dunning), Failed 944× today alone — every minute since midnight, single candidate invoice 202600538 ("The invoice has no valid billing recipient"). BAEST Machinery Holding has no email anywhere: Espo legacy has zero address rows, no contacts with email, `email` and `invoice_email` both NULL — D1's "populate from authoritative source" is impossible until accounting supplies the address. Interim surgical fix executed instead: autoDunningDisabled=true on invoice 202600538 via EntityManager (audited write, reversible). Next run: Success, Candidates: 0, occurrence closed, no further catch-up runs. Re-enable the flag the day accounting fills in BAEST's billing email.
DONED3 first half: JIT off for all new CLI processes. /etc/php/8.5/cli/conf.d/zzz-octane-stability.ini now opcache.jit=disable + jit_buffer_size=0 (SHM segment not allocated at all). The segfault mill (short-lived CLI spawns) is dry as of now; running Octane workers keep JIT until the next blue/green deploy restarts them — that deploy completes D3. Contract copy committed to deploy/php/.
P0NEW finding — the backup lane was dead, now recovering. pgbackrest repo1 (/var/lib/pgbackrest, daily crons) was never stanza-created — every daily repo1 backup has failed forever; repo2 (NFS) broke ~Aug 2 with permission-denied on backup.info (readable again by Aug 4 — NAS-side change, cause unconfirmed). Net: newest base backup was 13 days old; only WAL archiving stayed healthy. Executed: repo2 diff backup started 18:10 (running); repo1 stanza-create + initial full scheduled 03:30; RAID md3 read-only scrub scheduled 01:00 (both transient systemd timers). Morning check: pgbackrest info shows fresh diff + repo1 full; mismatch_cnt compared against 2560. Root-cause the repo1 cron misconfig + NFS perms in the D5 slice.
DONED5 executed (2026-08-05 morning): scrub clean-ish, repo1 resurrected, backup redundancy restored.
DONED6 first half: runtime contract committed. Primary Octane unit (base + both drop-ins folded verbatim) + the CLI opcache ini now live in the repo (deploy/systemd/, deploy/php/, commit 2464be282c); sync_unit_files_from_repo installs the unit on the next deploy with zero behavior change. Remaining reviewed slice: extend the deploy installer to deploy/php/, then delete the no-op wrapper and point both units at /usr/bin/php8.5.
DONERoadRunner verdict: nothing wrong with RR itself. RoadRunner CLI 2025.1.14 (current generation); worker recycling active via --max-requests=5000; PSS flat across worker lifetime (no leak); the segfaults live in PHP's opcache+JIT SHM, not RR. No RR upgrade, no .rr.yaml, no supervisor memory cap needed — recycling already bounds drift.

Fleet addendum — 33.davidstrejc.cz (2026-08-05 morning)

P0D11 — 33.davidstrejc.cz (78.46.85.13, Draivix SaaS instance box): CPU storm, not RAM.
DONED12 — vyroba.draivix.com "extremely slow" (2026-08-05): root-caused and fixed, PG exonerated.

Explicit NON-decisions

Execution order & verification gate

#DecisionWindowProof required before "done"
1D1 recipient datatoday08:00 job green in scheduler log; daily PHP spawn count collapses
2D2 scheduler state (Adam's branch)this weekstaged permanent failure → 1 attempt; spawn count < 3 k/day
3D3 JIT off + A/B recordthis week (next deploy)0 segfaults in 7 days; PSS delta; p50/p95 unchanged
4D4 Hetzner cooling ticketfile now, window ASAPpackage temp < 80 °C under load; throttle counter static
5D5 backup + RAID scrubnext maintenance windowpgbackrest verified; mismatch_cnt trend recorded
6D6 runtime contract committedwith D3's deployhost↔repo unit/ini diff empty
7D7 search compilernext perf sliceLead p50/p95 + COUNT before/after numbers published
8D8 leads schema unificationafter D7erp:schema:diff clean; duplicate indexes gone
9D9 outbox reindexmonthly maintenanceoutbox total size < 1 GB post-reindex

Evidence trail: repo file:line citations verified against main @ a19161caa2; live host readings 2026-08-04 ~17:50 CEST (sensors, /proc/mdstat, smaps_rollup, kernel journal, pg_stat). Full audit conversation retained in the ops session. This page: /home/._hostname/public_html/index.html on 21.davidstrejc.cz — noindex, informational only.