Repository navigation
🐛 Fix: a read after a commit is not uncommitted work - #202
Draft
medazizktata25 wants to merge 3 commits into
Draft
medazizktata25 wants to merge 3 commits into
medazizktata25 wants to merge 3 commits into
Conversation
`postgres_checkout_lifecycle` fails at step 06 on main: `gfs checkout main` is refused with "uncommitted changes ... (base/5/1249)" although nothing wrote to the database. The same refusal reaches users: insert rows, commit, run a `SELECT count(*)`, switch to another branch and back, and the switch back is refused on the table's file. Cause, measured. base/5/1249 is pg_attribute. Replaying the test's steps with the gfs binary and recording main/0 after each one, the file is unchanged by the restore at step 04 and changed by the stop at step 05: same size, 7 bytes different, all on page 16, every one the HEAP_XMIN_COMMITTED bit of t_infomask (0x0100) on the attribute rows pgbench's CREATE TABLEs inserted (xmin 739/740). Page LSN and checksum unchanged. In the user scenario, 1000 bytes of the table's file changed, all of them that same bit, one per row read. That is a PostgreSQL hint bit: the first read of a row after its inserting transaction commits records the commit in the row header and dirties the page, which reaches disk at the next checkpoint or shutdown. The dirty check compares size and mtime against the commit's file list, so it reports the page. Not a recent regression: the same replay against e51fc61 (before Guepard-Corp#191, Guepard-Corp#193, Guepard-Corp#194, Guepard-Corp#180, Guepard-Corp#183, Guepard-Corp#184, Guepard-Corp#186, Guepard-Corp#196) refuses at the same step on the same file. Fix. The provider now says whether a changed file holds the same data as the commit's snapshot copy of it. PostgreSQL compares relation main forks page by page and, on heap pages only, disregards what PostgreSQL itself masks as hints when it checks a replayed page (bufmask.c / heap_mask): pd_lsn, pd_checksum, pd_prune_xid, the PD_* hint flags and the four xmin/xmax hint bits. Index pages, other forks, other files and any other byte are still compared exactly, and an unreadable file still counts as changed. Other providers keep today's answer. Still refused, by design or not yet handled: real writes (the new e2e test asserts a same-length UPDATE is refused), and physical changes autovacuum makes on its own (pruning, visibility map), which are not hints.
…rinted cli_runner::run_gfs captures stdout by redirecting the whole process's stdout with gag, so anything else printed while a command runs lands in that command's output. postgres_checkout_lifecycle runs past 60 s on a loaded machine, and libtest's "test postgres_checkout_lifecycle has been running for over 60 seconds" notice then became the checkout's stdout: checkout stdout should show switched; got: test postgres_checkout_lifecycle has been running for over 60 seconds The test only passed at all because each "Switched to" assertion sat behind `if !stdout.is_empty()`, an escape hatch for in-process capture losing gfs's own output. The four checkouts in that test now run gfs as a subprocess, whose stdout holds only what gfs printed, and the assertions are unconditional. The new run_gfs_subprocess_checked counts only exit 0 as success; run_gfs_subprocess counts 0, 1 and 2 (for schema diff), which would have let a failed checkout pass "checkout should succeed".
…ch -d, and say why Guepard-Corp#180 reserved the branch name `detached` and also added, in `branch -d`, a branch that leaves `workspaces/detached` in place and prints a notice through a bare println!. Its comment says a repository that already carries a `detached` branch still reaches it. It cannot: `delete_branch` calls `validate_branch_name` on its first line, which refuses `detached` (any case, any `detached/...`) before anything else runs, and it is the only caller. Run with the real CLI, on a repository whose `detached` branch was created by a gfs from before Guepard-Corp#180 (e51fc61), with an uncommitted file in a detached working copy: $ gfs branch -d detached (82070fa) error: Invalid repository layout: branch name 'detached' is reserved ... rc=1 (DETACHED, detached/x: the same) and the file survives. So the guard and its println! are dead code; removed, and the comment now names the check that does the protecting. A regression test recreates that repository (the ref is the same 64-byte file the older gfs wrote) and asserts the deletion is refused and the detached working copy survives, so loosening the validation fails it. Not changed: such a branch cannot be deleted at all with this gfs, which is safe but leaves the user no way to remove the name.
medazizktata25
requested a deployment
to
pr-validation
October 27, 2026 14:50 — with
GitHub Actions
Waiting
medazizktata25
requested a deployment
to
pr-validation
October 27, 2026 14:50 — with
GitHub Actions
Waiting
medazizktata25
requested a deployment
to
pr-validation
October 27, 2026 14:50 — with
GitHub Actions
Waiting
This branch is waiting to be deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related Issue
None filed.
What
After a commit, reading the database is enough to make the next checkout refuse:
nothing having written to it. A user hits it like this: insert rows, commit, run a
SELECT count(*), switch to another branch and back. The switch back is refused on the table's file.postgres_checkout_lifecyclefails at step 06 onmain(d5a0e83) for the same reason.Cause. The first read of a row after its inserting transaction commits sets PostgreSQL hint bits in the row header and dirties the page, which reaches disk at the next checkpoint or shutdown. The dirty check compares size and mtime against the commit's file list, so it reports the page.
base/5/1249ispg_attribute. Measured when the fix was written (see the first commit message): the bytes that change are all theHEAP_XMIN_COMMITTEDbit oft_infomask, on page 16; the page LSN and checksum are unchanged.How
Three commits:
fix(checkout): a read after a commit is not uncommitted work.DatabaseProvider::holds_same_data; the default keeps today's answer).bufmask.c/heap_mask):pd_lsn,pd_checksum,pd_prune_xid, thePD_*hint flags, and the four xmin/xmax hint bits.fix(test): assert on what gfs checkout printed, not on what libtest printed.cli_runner::run_gfscaptures stdout by redirecting the whole process's stdout, so libtest's "has been running for over 60 seconds" notice could become a checkout's output.postgres_checkout_lifecyclenow run gfs as a subprocess, and their assertions are unconditional.run_gfs_subprocess_checkedcounts only exit 0 as success.fix(branch): drop the unreachable guard #180 left in branch -d, and say why.delete_branchrefuses the reserved namedetachedon its first line, so the guard A branch named 'detached' destroyed every detached working copy #180 added after it is dead code; this removes it.detachedbranch was made by an older gfs, and asserts the deletion is refused and the detached working copy survives.Review Guide
crates/adapters/db-providers/src/pg_page.rs: the page comparison and its masks, against PostgreSQL'sheap_mask.crates/domain/src/usecases/repository/checkout_repo_usecase.rs: where the dirty check asks the provider.crates/domain/src/ports/database_provider.rsandpostgresql.rs: the new provider method.cli/tests/e2e_checkout_after_read.rs(new),cli/tests/common/cli_runner.rs,e2e_checkout_postgres.rs,e2e_checkout.rs.Testing
On Rust 1.93.1, macOS with Colima for Docker, using
gfs-postgres:16/:17images built from this tree.postgres_checkout_lifecycle: all 6 steps pass, 59.6 s. Onmainit fails at step 06 with the refusal above.e2e_checkout_after_read(new): passes.UPDATEis still refused.holds_same_dataforced tofalse, both e2e tests fail. The lifecycle test fails with exactly the originaluncommitted changes … (base/5/1249). Restored, both pass.pg_pageunit tests 6/6, domain checkout tests 10/10,e2e_checkout11/11 (including the newdetachedregression test).cargo test --locked: 888 passed, 1 failed, 3 ignored, across 60 test binaries. The failure isclone_range_and_temporal_hydration_elide, which fails identically onmain; its fix is on its own branch.cargo fmt --checkclean.cargo clippy --locked --all-targets --all-features -- -D warningsclean, run on Linux (rust:1.93.1-bookworm).detachedregression test was not checked against a deliberately loosened validation.Documentation
pg_page.rsname the PostgreSQL source they mirror.User Impact
--forcestill discards them.Checklist
cargo test). One fails, identically onmain; see Testing.cargo clippy --all-targets --all-features -- -D warnings), on Linux.cargo fmt --check)