ID |
|
|---|---|
Status |
Backlog |
Bucket |
cleanup |
Priority |
2 |
Created |
2026-08-24 |
Updated |
2026-09-27 |
A mutation test seeds film_actor rows a query test asserts the absence of
graphitron-sakila-example’s execution tier runs every test class against one shared PostgreSQL
database, and two classes disagree about who owns the `film_actor seed.
DmlBulkMutationsExecutionTest.deleteFilmActorsByNodeId_bulkRows_deletesAllViaRowIn inserts the
pairs (actor 2, film 3) and (actor 3, film 4) before its mutation and deletes them in a
finally, on a comment that reasons the pairs are safe because neither is in init.sql’s seed.
That reasoning covers a sequential run and not a concurrent one.
`GraphQLQueryTest.splitTableField_conditionJoin_returnsActorsPerFilm reads film 3’s actors through
the condition-join split-rows path and asserts the answer is exactly {1}, so while the mutation
test holds its transient row the query test’s assertion is false. Observed on a full
mvn install -Plocal-db, failing with [1, 2] against an expected [1]; the same tree passes when
the class or the module runs on its own, and passed a second full build, so the two classes have to
be running concurrently for it to land. The two other classes that write film_actor
(TenantDivinedRoutingExecutionTest, TenantFanOutExecutionTest) use film ids in the hundreds and
are clear of every seeded read, which is the shape the fix wants: a writer either picks rows outside
every reader’s assertion window or takes a row nobody else reads. Worth answering because the
failure presents as a correctness defect in condition-join emission, which is where the next reader
of a red build will spend their afternoon, and because CI runs the reactor with -T 1C, so the
concurrency that produces it is the normal case rather than the unlucky one.
Two more occurrences, both 2026-08-25 and on consecutive full installs on one machine, widen both
sides of the race. The same writer class has a third seeding site the paragraph above does not
list, seedFilmActor(3, 3), and a second reader observed it:
RoutineFieldExecutionTest.correlatedChildRoutineReturnsPerParentRows failed with [2, 3, 5]
against an expected [2, 5] for actor 3, the extra film 3 being exactly that transient pair, gone
from the database by the time anyone looked. The very next run failed a different method of the
same reader (childRoutineThenHopsChainJoinsOutOfRoutineResultPerParent, actor 2 showing the
already-listed (2, 3) pair), so on that machine the two classes overlap more often than not. The
fix should inventory every seedFilmActor call rather than the two pairs first observed, and the
reader set is every per-parent film assertion in the module, not one condition-join case.
A fourth occurrence, 2026-09-22, moves the race onto a second table and so widens what the fix has
to cover. The same writer class inserts and deletes film rows with language_id = 1 around its
bulk-insert cases, and OptionalNodeIdProjectionExecutionTest.anOmittedNodeIdReachesAConditionMethodAsNull
read across that window: its unconstrained query returned [1, 2, 3, 4, 5] where the query
constrained to English returned [1, 2, 3, 4, 5, 58], which is impossible from one database state,
an unconstrained read being a superset of a constrained one by construction. Observed on a full
mvn install -Plocal-db; the class on its own and the whole module on the same tree both pass. So
the inventory the paragraph above asks for is not only seedFilmActor: it is every row the writer
class touches, film included, and the reader set is every execution case that compares two reads
of one table rather than only the per-parent film assertions.
A fourth occurrence, 2026-09-27, and this one lands on a writer this item had cleared. The text
above reasons that TenantFanOutExecutionTest and its sibling are safe because they use film ids in
the hundreds, clear of every seeded read. That reasoning is about what they write. This failure is a
read: downedTenant_byTimeout_classifiesTenantFanOutTimedOut expected three films and got
[null, null], so the tenant-1 fan-out arm returned nulls and the appended element for the timed-out
tenant never arrived. Observed on a full mvn clean install; the class on its own passes on the same
tree. The harvest carrying it touched no file in graphitron-sakila-example and nothing tenant-side,
which is what makes it this item’s rather than that work’s.
Worth recording because it widens the reader set the same way the third occurrence did. A writer picking rows outside every reader’s window does not protect that writer’s own reads, and a fan-out case reads through several datasources at once, so the window it is exposed to is every one of them.
A doubt about the evidence in this item, raised by a session that caught itself making the
mistake. "Passed alone, passed on a rerun" does not discriminate a flake from a stale artifact,
because both of those rebuild the thing that was stale. It reads as nondeterminism and is equally
consistent with a tree that was simply wrong, and the execution tier is where that bites hardest: it
runs generated code out of graphitron-sakila-example/target, which a plain install leaves in
place and a rebuild silently corrects.
That lands on this item’s own record. The first and third occurrences were both observed on
mvn install -Plocal-db, with no clean, so staleness is not excluded for either. The fourth was
observed on a full mvn clean install, and the log shows clean:3.5.0:clean running on
graphitron-sakila-example in that same build, so the generated code it executed was produced by
the build that failed; a full reactor also resolves its inter-module dependencies from its own
outputs rather than from installed jars, which is the other way staleness enters and only bites a
-pl build. So the fourth stands and the other two want re-reading.
What follows is that the flake reading now rests on one occurrence rather than three, and that this item may be two items: a nondeterministic writer, and a build-hygiene trap that has been producing phantom failures nobody could reproduce. Whoever takes the Spec should settle which before designing a fix, because the two want different answers.
Adjacent to R823, which records a different execution-tier test reading a mutating table; whether the two want one answer (a convention about which rows an execution test may write) or two is for the Spec to decide.