Hi Greg,

> Attached is v4 which I hope fixes the various cf-bot build failures.

It clears the ones I hit.  I measured v4 on master b28a2b2bfa5, against v2 for
the FILEOPS cost, and checked the per-prefix test claim.  Two findings, plus an
adversarial pass that came up clean.

1. ROLLBACK and crash recovery now pay two fsyncs per FILEOPS undo record

fileops_sync_inverse() fsyncs the file and its parent after every applied
inverse, and since FILEOPS can no longer be deferred that path runs inline at
ROLLBACK and in the crash-recovery undo phase.  N inverses on one file is 2N
fsyncs.  For N chmods in one transaction (ROLLBACK timed, median of 3), the
rollback goes from 2.5 ms on v2 to 660 ms on v4 at N=1000, and from 77 ms to
22.8 s at N=32000.  Crash-recovery undo of N in-flight records, from the
server's own log: N=8000 is about 7 ms on v2 and about 5.6 s on v4.  With
fsync=off, v4 at N=1000 drops from 638 ms to 1.4 ms, so the cost is entirely
the fsyncs, not asserts; the forward direction (doing the N chmods) is
unchanged from v2.

Durability of the inverse before the CLR is the point, but it can be batched:
fsync each distinct path (and its parent) once per chain, before the record
that marks the chain done, instead of once per record.  As it stands a
transaction that touched one file 10k times takes about 7 s to roll back and
adds about 7 s of undo before the cluster opens after a crash.

2. "every prefix builds and passes its own tests" does not quite hold

All of 0001-0008 build without warnings and pass make check.  Their own module
tests do not all run at their prefix:

- 0002: test_fileops/001 sets max_logical_revert_workers, a GUC added in 0005,
  so the server will not start -- the same shape as the split issue Shihao
  reported.  test_fileops is not listed in src/test/modules until 0005, so the
  build farm does not run it there.
- 0004: six recovery tests need the flux AM (0019) or test_idxundo (0006).
- 0005: eight test_idxundo TAP files ship here but the module arrives in 0006.
  And test_fileops/016 surfaces a real issue at this prefix: with an injected
  fsync failure on an inverse during abort, the backend raises ERROR inside
  abort, re-enters abort five times, and ends in PANIC: ERRORDATA_STACK_SIZE
  exceeded; the PANIC elevel that makes it one clean failure arrives in 0007.
  On the full series, 016 passes.
- 0008: recovery 094 and 121 need the flux AM.

The full list with the exact files is in the attached notes.

3. Adversarial pass: nothing to report

On an ASan+UBSan build I ran crash injection across plain ROLLBACK, savepoints,
postmaster and backend kills, a kill during ROLLBACK, a crash inside the
recovery undo phase, and 2PC.  Each time the file tree came back exactly to the
committed baseline, with no sanitizer report and no assert (a COMMIT control
diverges, so the check is live).

Repros and the full measurements are attached.

Regards,
Manu
# UNDO/FILEOPS v3/v4 review: evidence (2026-10-13)

Target: **v4** (Greg, 2026-10-13 00:27, "Re: UNDO with FILEOPS, and the FLUX
table AM"), which supersedes v3 (2026-10-12) one day later; 0002, 0003 and
0006-0008 are byte-identical between v3 and v4 (see PATCH_MAP.md). Applied with
`git am` on master **b28a2b2bfa5** (2026-10-13), all 20 patches clean. Trees in
~/.cache/ctr-v3: normal (-O2), cassert (-O2 + asserts, same flags as the v2
install ~/undo-v2-inst used for the v2 numbers), asan (ASan+UBSan, gcc 16).
Machine: Ryzen AI 9 HX 370, NVMe + btrfs. Lane A CPUs, integrity sentinel clean.

## Finding 1 (measured, new in v3): ROLLBACK and crash recovery pay 2 fsyncs 
per FILEOPS UNDO record

v3 added `fileops_sync_inverse()` (fileops_undo.c ~697): after **every**
applied inverse it fsyncs the file **and** its parent directory (elevel PANIC).
The same apply path runs inline at ROLLBACK (FILEOPS can no longer be deferred,
`needs_inline_abort`, xactundo.c) and in the crash-recovery undo phase before
the cluster opens. N inverses on the same file -> 2N fsyncs of the same two
inodes.

A) Client-visible ROLLBACK latency, N chmods in one txn, only ROLLBACK timed,
   median of 3, every trial validated (file mode back to baseline). Same
   workload as our v2 report (logs/bench/rollback_raw.tsv, summary.txt):

   threshold    build   N=200    N=1000    N=8000     N=32000   (ms)
   65536        v2      0.64     2.47      19.6       77.0
   65536        v4c     132      660       5822       22851
   1e9 (inline) v4c     147      752       5423       21748
   1 (defer)    v4c     154      738       5870       23007

   ~0.7 ms per record (= this NVMe's fsync cost) vs ~2.4 us in v2: ~300x.
   undo_instant_abort_threshold no longer changes anything for FILEOPS (by
   design in v3: FILEOPS UNDO is always inline).

B) Discriminator (logs/fsync_discriminator.txt), N=1000, median of 3:

   build   fsync=on   fsync=off
   v2      1.9 ms     1.8 ms
   v4n     638 ms     1.4 ms      <- no asserts: the whole cost is fsync
   v4c     1006 ms    1.5 ms

C) Crash with an in-flight txn of N FILEOPS records, immediate stop, restart;
   undo phase timed from the server's own log; undo phase completes before
   "ready to accept connections" in every trial; records applied == N
   (logs/bench/recovery_raw.tsv, recovery_v4_raw.tsv):

   N       v2        v4n        v4c
   2000    2 ms      1318 ms    1450 ms
   8000    7-8 ms    5570 ms    5640 ms
   32000   28-39 ms  (~22 s extrapolated, not run)

   So the v2 point "linear before open" still holds, with a ~700x larger slope.
   Forward cost (doing the N chmods) is ~0.9 ms/op in BOTH v2 and v4
   (logs/forward_cost.txt): the regression is entirely on the undo side.
   Redo of the same records is also fsync-bound: a crash during ROLLBACK of
   6000 records spent 7.7 s in redo (ASan build, logs/adv/S5b.server.log).

Why it matters / what to ask: durability of the inverse before the
abort/CLR is the point of the v3 fix, but it can be batched: fsync each
distinct path (and parent) once per chain, before the record that marks the
chain done, instead of once per record. As is, a txn that touched one file
10k times takes ~7 s to ROLLBACK and adds ~7 s of undo before open after a
crash. Repro: scripts/repro_rollback_fsync.sql (+ fsync_discriminator.sh).

## Finding 2 (measured): "every prefix builds and passes its own tests" does 
not hold

Clean checkout + clean configure (cassert, TAP, injection points) per prefix,
core `make check`, then the TAP/regress files the patch adds or modifies
(scripts/prefix_check.sh, logs/prefix/). Every prefix 0001-0008 **builds with 0
warnings and passes core make check (239/239)**. Own tests:

- 0001: PASS (pg_waldump/001).
- 0002: **FAIL** test_fileops/t/001_fileops_apply_failure.pl: sets
  `max_logical_revert_workers = 0`, a GUC added in 0005 -> "unrecognized
  configuration parameter", pg_ctl start fails. Same shape as Shihao's #1
  (059/061 setting a 0005 GUC). Not seen by the build farm/meson because
  test_fileops is not listed in src/test/modules/{Makefile,meson.build} until
  0005 (logs/meson_audit.txt). Recovery 060-063, 072: PASS.
- 0003: no own tests; build + core PASS.
- 0004: **FAIL** 6 tests die with `access method "flux" does not exist`
  (FLUX = 0019): recovery 073, 074, 075, 083, 089 and test_checksums/025 (all
  registered in meson at this prefix). recovery/114 needs
  shared_preload_libraries=test_idxundo (module added in 0006): pg_ctl start
  fails and prove bails out (115 [also FLUX], 117, 152 not reached).
  085/096 fail on pg_walinspect/amcheck not installed under the autoconf
  harness (recovery Makefile EXTRA_INSTALL not extended; meson installs all
  contrib) - minor, autoconf-only.
- 0005: **8 orphan TAP files**: test_idxundo/t/002-009 ship here but the module
  (Makefile, meson.build, .c) arrives in 0006 - nothing can run them.
  test_fileops/005 dies: reloption `index_undo` (0006). **test_fileops/016
  FAILS 3/5 and shows real misbehaviour at this prefix**: with an injected
  fsync failure on an inverse during abort, the backend raises ERROR inside
  abort, re-enters abort 5 times (5 abort records) and ends in
  `PANIC: ERRORDATA_STACK_SIZE exceeded` instead of one clean PANIC. The
  elevel=PANIC for that path is only introduced in 0007 (the hash patch).
  On the full series 005 and 016 PASS (verified).
- 0006, 0007: build + core PASS (own tests not run in this pass).
- 0008: **FAIL** recovery 094 and 121 (registered in meson here) die with
  `access method "flux" does not exist`.

Repro: scripts/repro_prefix_tests.sh (0002 and 0004 cases).

## Adversarial pass (ASan+UBSan, crash injection): nothing found

scripts/adv_fileops.sh on inst-asan. Victim txn = 13 mixed FILEOPS ops
(chmod, write, truncate, create, mkdir, create-in-new-dir, link, symlink,
setxattr, removexattr, rename) + NPAD chmods to widen windows. Invariant: test
tree (type, mode, size, md5, link target, user xattrs) == committed baseline;
server log has no PANIC/TRAP/assert; no ASan/UBSan report.

  S0 negative control (same ops COMMITTED)            DIVERGED (harness valid)
  S1 plain ROLLBACK                                    OK
  S2 SAVEPOINT / ROLLBACK TO + more ops, ROLLBACK      OK
  S3 in-flight, postmaster immediate stop              OK (3011 records undone 
before open)
  S4 in-flight, kill -9 backend (crash-restart)        OK (3011 records)
  S5 kill -9 backend DURING its ROLLBACK               OK ("Failed process was 
running: ROLLBACK")
  S5b same, snapshot at FIRST accepted connection      already reverted at open 
(redo finished it)
  S6 crash DURING the recovery undo phase, restart     OK (1.8 s into phase; 
2nd recovery re-applies
                                                       the whole chain, 6011 
records, idempotent)
  S7 PREPARE TRANSACTION, crash, ROLLBACK PREPARED     OK (prepared xact 
skipped by undo, kept)

No sanitizer report in any scenario. (The 6 -Wnonnull warnings of the ASan
build are gcc-16 false positives in upstream code: path.c, indexcmds.c,
tablecmds.c, md.c.) Also confirmed by construction: v3's alignment bug (undo
record headers at non-MAXALIGN'd offsets) is what v4 fixes; we tested v4.

Harness notes (cost us time, not findings): FILEOPS rename is deferred to
commit, so writing to the new name inside the txn errors out (the first S1/S2
"OK" were vacuous until S0 caught it); `pg_ctl start` waits by default (-W
needed to crash inside the undo phase).

## Not done in this pass
- 0006/0007 own TAP suites; 0005 recovery 090/126/134 and test_fileops
  002-009 at that prefix.
- Deeper adversarial (ENOSPC/EIO during inline apply -> ATM fallback path at
  xactundo.c ~1183, CREATE DATABASE crash windows, standby).
- v2-vs-v4 ABBA second round (effect size 300x makes it moot).
-- Minimal repro: v3/v4 ROLLBACK of N FILEOPS operations costs ~N x (fsync of
-- the file + fsync of its parent directory), applied inline before ROLLBACK
-- returns (fileops_sync_inverse() after every inverse). Same with crash-loser
-- undo before the cluster opens.  Run with psql against a v4 build:
--   CREATE EXTENSION test_fileops;   then \i this file, once with fsync=on and
--   once after ALTER SYSTEM SET fsync=off; SELECT pg_reload_conf();
-- Measured (NVMe, btrfs), N=1000: v2 1.9 ms | v4 (no cassert) 638 ms with
-- fsync=on, 1.4 ms with fsync=off.
SELECT test_fileops_create_tempfile('fsync_repro.dat') AS f \gset
BEGIN;
SELECT count(test_fileops_chmod(:'f', 384 + (g % 2) * 36)) FROM 
generate_series(1, 1000) g;
\timing on
ROLLBACK;
\timing off
#!/usr/bin/env bash
# Minimal repro: patches' own tests depend on later patches.
#   repro_prefix_tests.sh <postgres-src-with-series-as-commits> <tag-0002> 
<tag-0004>
# 1) at 0002: test_fileops/t/001 sets max_logical_revert_workers (added in 0005)
#    -> FATAL: unrecognized configuration parameter; pg_ctl start fails.
# 2) at 0004: recovery/t/073 (and 074,075,083,089, test_checksums/025) do
#    CREATE TABLE ... USING flux (added in 0019)
#    -> ERROR: access method "flux" does not exist.
#    recovery/t/114 needs shared_preload_libraries=test_idxundo (added in 0006).
set -eu
SRC=$1; T2=$2; T4=$3; W=$(mktemp -d)
run() {  # $1 tag, $2 test dir, $3 tests
  git -C "$SRC" worktree add -q --detach "$W/$1" "$1"
  (cd "$W/$1" && ./configure -q --enable-cassert --enable-tap-tests 
--enable-injection-points \
     --without-icu >/dev/null && make -s -j8 >/dev/null && make -s -C contrib 
-j8 >/dev/null \
   && make -C "$2" check PROVE_TESTS="$3" 2>&1 | tail -5)
  grep -hE 'FATAL|ERROR' "$W/$1/$2"/tmp_check/log/* | head -3
}
run "$T2" src/test/modules/test_fileops t/001_fileops_apply_failure.pl
run "$T4" src/test/recovery t/073_perbackend_2pc_rollback.pl

Reply via email to