Repository navigation
Add expected-failure tests for unit state kept across builds - #42
Draft
jun-mercury wants to merge 1 commit into
Draft
jun-mercury wants to merge 1 commit into
jun-mercury wants to merge 1 commit into
Conversation
A server that outlives a build keeps the first build's view of a unit and serves it to the next: the unit env in the make state's HUG (insertUnitEnv, never evicted), the module graph nodes merged old-wins (mergeModuleGraphNodes keeps the old ModSummary with its preprocessed source), the interpreter's loader state (never unloaded) and the HPT. Nothing in the suite feeds a second build to the same WorkerState: Test.Env.newResumeSessionEnv discards it, "simulating a worker restart", which is buck-proxy's one-worker-per-build lifecycle (socket named after BUCK_BUILD_ID, killed on SIGTERM unless --remain). A worker kept alive with --remain, or a pool of servers on remote execution, violates that assumption. StaleUnitTest feeds one WorkerState two builds of one unit with a change between them and compares the second build's interface with a fresh WorkerState's, exports and ABI hash. Four sequences, each red today: - a module's source changes, same args: the old source is recompiled - the unit's args gain -DFOO: the module recompiles with the old flags - a TH splice reads a module whose value changed: the splice sees the old value, the interpreter keeps the first build's code loaded - a module joins a known unit and an existing module imports it: the importer recompiles from its old source The cases are wrapped in stillStale, which passes while the inner assertions fail and prints their report to stderr, and fails once a sequence comes out fresh, the signal to delete the wrapper. Set expectFailures to False to see the failures on their own terms. The harness runs the legacy metadata path (no --dep-units-static). On the static path the same fourth sequence panics instead, "Could not find ModSummary in the module graph" for the new module, reproduced on a real unit outside the suite. Assisted-by: Claude:claude-fable-5-1
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.
Draft. The four tests are red by design: they reproduce a bug this pull request does not fix, and a wrapper keeps the suite green until a fix lands.
Why
A ghc-worker server that outlives one build serves the first build's view of a unit to the next. The unit env in the make state's HUG is inserted by
insertUnitEnvand never evicted;mergeModuleGraphNodeskeeps the oldModuleNode, so a module's oldModSummaryand preprocessed source survive; the interpreter's loader state is never unloaded; the HPT is trusted. The code assumes buck-proxy's lifecycle, one worker per build (socket named afterBUCK_BUILD_ID, killed on SIGTERM unless--remain), and the comment inCache/Hpt.hssays the state is "initially empty" when a new build starts. The suite models that too:Test.Env.newResumeSessionEnvcreates a freshWorkerStatefor the second build, "simulating a worker restart". A worker kept with--remain, or a pool of servers on remote execution (our case), breaks the assumption, and the second build's output is wrong without any diagnostic.This pull request adds the missing shape to the suite: one
WorkerState, two builds of the same unit with a change between them, the second build's interface compared with a freshWorkerState's. It proposes no fix; the reviewers own that design.Scope
ghc-worker/test/StaleUnitTest.hs, new.Build(a unit's extra args, module sources, compiles in order) fed throughcomputeMetadataandcompileModuleWithDepsInHptthe wayTest.Builddoes,Iface(sorted exports and ABI hash read withreadBinIface),staleSequence, and the wrapperstillStale.ghc-worker/test/Main.hsandghc-worker/ghc-worker.cabalregister the module. The cabal edit is by hand; if the file is hix-generated at HEAD, regenerate it.tasty-expected-failurewould have been the idiom, but adding a package touchesops/packages.nix,haskell/BUCKand the generated cabal files, so the wrapper is ten lines overHedgehog.Internal.Property.runTestT: it passes while the inner assertions fail, prints their report to stderr so the stale output stays visible on a green run, and fails once a sequence comes out fresh, which is the signal to delete it.expectFailures = Falseshows the raw failures.The four cases, group "stale unit state across builds":
-DFOOadded to the unit's args; K recompiles with the old flags.spliced_1. A value-only change does not move the ABI hash under the suite's flags, which is why the splice writes the value into a name.Blast Radius
Test-only change; nothing in the server or the libraries moves. The suite stays green because of the wrapper. The cost of leaving the bug is that any deployment with servers that outlive a build ships stale interfaces and objects, and the
.hashfile the server writes beside them agrees with the stale output, so a build system caching on it cannot notice.Verification
Run on the 85f41f9 lineage with GHC 9.10.3 (this branch's base is upstream HEAD; see below):
With
expectFailures = False, "4 out of 4 tests failed", every failure atlongIface.exports === freshIface.exports:As shipped (
expectFailures = True): "All 4 tests passed", "Test suite ghc-worker-test: PASS", with fourstill stale: <case>reports on stderr carrying the same twoIfacevalues.The same sequences on a real unit, outside the suite.
src/Mercury/Sentry/Constantsfrom our monorepo (three modules, no project deps, no package dbs), one hand-started server fed the plan buck2 wrote, each build compared with one-shotghc -cof the same tree; the same result from an upstream 85f41f9 server and from our fork's:Fingerprints.hschanges its export's type: server and one-shot agree (B: ABI861d0cff..., exportsdefaultV2 default_).-DSENTRY_V2andFingerprints.hsa CPP-gated export: server ABI861d0cff...(unchanged from B), one-shot0a5b2f81...;defaultV3missing from the server's interface, and the.hashthe server writes says861d0cff....Extrajoins the unit andFingerprintsimports it:-c Extrapanics "Could not find ModSummary in the module graph for root-src-Mercury-Sentry-Constants-Constants:Mercury.Sentry.Constants.Extra",-c Fingerprintsfails with "Could not find module 'Mercury.Sentry.Constants.Extra'". This is the static metadata path (--dep-units-static); the suite's harness runs the legacy path, where the fourth sequence recompiles the importer from its old source instead of panicking.The same A, B, C, D driven by buck2 on remote execution through our pool of four servers per machine (
ghc-worker.remote=true), edits made in the checkout as a developer would (extra_compiler_flags = ["-DSENTRY_V2"]in the BUCK file for C, a new file in the glob for D): B right (ABI861d0cff...,.hashagrees), C stale (remote ABI861d0cff...vs one-shot0a5b2f81...,defaultV3missing), D fails the build with the same panic. One machine, about two minutes, destroyed afterwards.Not run: the suite at this branch's base, upstream
experimentsHEAD (3378700). HEAD needs two GHC patches our 9.10.3 lacks (ReachabilityIndex export, splitgetLinkDeps), so it cannot be built here. The commit was proven on 85f41f9, whereMain.hsand the cabal file are identical to HEAD's andghc-worker/BUCKglobstest/**/*.hs; the same commit on that base is pushed astest/stale-unit-across-builds-85f41f93on the fork for anyone who wants to rerun it. HEAD's incremental module graph commits (useIncrModGraph,mergeModuleGraphNodesappend-only) do not evict either, but the failure content at HEAD is unverified.Open question
The smallest change we can see that makes these pass is to key a unit's stored state on a fingerprint of its content (args file, source set, source hashes) rather than on the
UnitId, evict the unit env, its module graph nodes and HPT entries when the fingerprint changes, and reinitialise the interpreter's loader state for that unit. About 150 lines ininternal, worker only. Whether that is the right shape, or whether a worker should refuse a second build altogether, is a design call for the maintainers; this pull request only makes the current behaviour visible.Assisted-by: Claude:claude-fable-5-1