Skip to content

test(dbfork): copy-on-write equivalence fixture via internal/fsclone#186

Merged
kuny0707 merged 2 commits into
tronprotocol:developfrom
barbatos2011:feat/snapshot-fixture-cow
Jun 15, 2026
Merged

test(dbfork): copy-on-write equivalence fixture via internal/fsclone#186
kuny0707 merged 2 commits into
tronprotocol:developfrom
barbatos2011:feat/snapshot-fixture-cow

Conversation

@barbatos2011

Copy link
Copy Markdown

What

Replace the dbfork equivalence gate's per-store byte copy with a copy-on-write clone, in a new reusable internal/fsclone package.

Why

TestEquivalence_GoVsJava materialises the 8 dbfork stores twice per run (scratchGo + scratchJava). Once the Nile snapshot is cached, that recursive copy is the dominant wall-clock cost — multi-GB on the RocksDB fixture. A copy-on-write clone shares the underlying extents, so each scratch copy is created in milliseconds at near-zero extra space; the first write to a block transparently splits it off, preserving the per-scratch independence the diff requires.

How

internal/fsclone.CloneDir(src, dst) (method, error) — build-tagged per platform:

  • darwin: clonefile(2) via x/sys/unix.Clonefile — recursively clones a directory tree in one syscall.
  • linux: walk + FICLONE per regular file (x/sys/unix.IoctlFileClone).
  • other: byte-copy fallback.

Safety contract (a reusable primitive, not just a test helper):

  • Stages into a temp dir it creates as a sibling of dst, then atomic rename into place.
  • Refuses a pre-existing dst — never merges or clobbers.
  • On failure removes only its own temp dir, never caller data.
  • Falls back to a byte copy only on copy-on-write-unsupported errnos (EXDEV/EOPNOTSUPP/ENOTSUP/ENOTTY/EINVAL/ENOSYS); real errors (EPERM, ENOSPC, unreadable source) propagate instead of being silently masked as a slow copy.
  • No manual copy_file_range layer — Go's io.Copy already uses it for *os.File on linux, so the fallback is kernel-accelerated where supported.

Equivalence test:

  • Scratch dirs are now created adjacent to the fixture (same filesystem) instead of under t.TempDir(). t.TempDir() resolves under os.TempDir(), frequently a different filesystem than the cached fixture — so a clone across them returns EXDEV and silently degrades to a full copy, meaning the optimization would fire nowhere. Co-locating scratch with the fixture is what lets the clone actually reflink.
  • Calls fsclone.CloneDir; the test-local cloneDir/copyDir/copyFile and their round-trip tests are deleted — there is now a single clone+copy implementation.

Tests

internal/fsclone covers: round-trip + independence; a seam that forces the byte-copy fallback deterministically on any host (so the fallback orchestration is tested even on CoW filesystems where the real clone always succeeds); real-error propagation; pre-existing-dst refusal; missing-source error; and the recursiveCopy fallback directly.

Verified: gofmt/vet clean, go build ./..., fsclone + dbfork unit tests pass, and cross-compile builds for linux amd64/arm64 (FICLONE path), darwin, and the windows fallback.

CI

dbfork-equivalence.yml gains a non-blocking copy-on-write probe that logs the fixture vs scratch device IDs and a cp --reflink=always result, so the run shows whether the clone actually reflinked on the runner or fell back to a copy. (GitHub's ext4 runners typically don't reflink — the win lands on copy-on-write filesystems; the probe makes that observable rather than guessed.)

Follow-up

TODOS.md captures a deferred public trond snapshot clone verb — left internal until a second real consumer (e.g. an agent warm-pool flow or apply --snapshot) exists.

@barbatos2011
barbatos2011 force-pushed the feat/snapshot-fixture-cow branch from 79b2381 to 7672226 Compare June 15, 2026 13:36
Replace the equivalence gate's per-store byte copy with a copy-on-write
clone. The gate materialises 8 fixture stores twice per run (scratchGo +
scratchJava); on a full RocksDB fixture that copy dominates wall-clock
once the snapshot is cached.

New internal/fsclone package (designed via /plan-eng-review tronprotocol#185):
- CloneDir(src,dst)->(method,err): clonefile(2) on darwin (recursive in
  one syscall), FICLONE per-file on linux, byte-copy fallback elsewhere.
  Build-tagged clone_{darwin,linux,fallback}.go + clone_unix.go errno
  classifier. golang.org/x/sys promoted to a direct dep.
- Safety: stages into a primitive-created temp dir and atomically
  renames into place; refuses a pre-existing dst; on failure removes only
  its own temp, never caller data. Falls back ONLY on CoW-unsupported
  errnos (EXDEV/EOPNOTSUPP/ENOTSUP/ENOTTY/EINVAL/ENOSYS); real errors
  propagate instead of being masked as a slow copy.
- Drops the manual copy_file_range layer: io.Copy already uses it for
  *os.File on linux.

Equivalence test:
- Scratch dirs now created ADJACENT to the fixture (same filesystem)
  instead of t.TempDir(); t.TempDir() lands under os.TempDir(), often a
  different filesystem, so the clone would EXDEV and silently fall back
  to a full copy -- the optimization would fire nowhere. (Codex finding.)
- Calls fsclone.CloneDir; test-local cloneDir/copyDir/copyFile and their
  round-trip tests deleted (single clone+copy impl now lives in fsclone).

Tests (internal/fsclone): round-trip + independence, a seam that forces
the byte-copy fallback deterministically on any host, real-error
propagation, dst-exists refusal, missing-src error, recursiveCopy
round-trip.

CI: dbfork-equivalence.yml gains a non-blocking CoW probe logging
fixture vs scratch device IDs + a cp --reflink=always result, so we can
see whether the clone actually reflinks on the runner or falls back.

TODOS.md: capture the deferred public 'trond snapshot clone' verb.
golangci-lint errcheck flagged the bare `defer os.RemoveAll(tmp)`.
Wrap it to explicitly discard the error — best-effort temp cleanup whose
failure is non-actionable. (Missed locally: ran go vet, not the full
golangci-lint.)
@barbatos2011
barbatos2011 force-pushed the feat/snapshot-fixture-cow branch from 821312e to f2efb38 Compare June 15, 2026 14:52
@kuny0707
kuny0707 merged commit ea39a09 into tronprotocol:develop Jun 15, 2026
12 checks passed
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.

2 participants