Repository navigation
test(taskservice): prove stale/failed refresh keeps the active task store (issue #24144 regression coverage) - #30147
Conversation
e0df2bc to
74af65c
Compare
74af65c to
848f56d
Compare
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
848f56d to
2931438
Compare
2931438 to
63ef183
Compare
13066b9 to
268515a
Compare
268515a to
e6822ac
Compare
e6822ac to
12d837c
Compare
12d837c to
09c60c3
Compare
09c60c3 to
d048f07
Compare
…tore Issue matrixorigin#24144 reported that refreshableTaskStorage.refresh() closed the active task store before rejecting a stale lastAddress request and before confirming that a replacement store was created, leaving the holder serving a closed store that surfaced as "sql: database is closed" in SQL task execution (sql_task.sql overlap section lost one run and one sink row in compose pessimistic CI). The production ordering fix already exists on main (26226d2, PR matrixorigin#27135): refresh rejects closed/stale requests before any factory work, preserves mu.store and mu.lastAddress when address resolution or Create fails, publishes the replacement together under the lock after a generation re-check, and retires only a distinct previous store. Issue matrixorigin#24144 was never linked to that fix and lacked its named regression coverage: the requested tests did not exist, and memTaskStorage's no-op Close/PingContext made pointer assertions blind to premature closure. This adds the issue-named regression tests plus supporting lifecycle coverage to pkg/taskservice/task_service_holder_test.go: - TestRefreshTaskStorageSkipStaleRefreshKeepsCurrentStore and TestRefreshTaskStorageCreateFailureKeepsCurrentStore: stale request and create/nil-store/address failures keep the current store installed, usable (verified through a closure-sensitive store whose ping fails with "sql: database is closed" once closed) and lastAddress unchanged, with factory-call counters. - Stale request after replacement, fixed-factory reuse, same-address fresh-instance replacement, initial-failure recovery with the queued not-ready refresh observed, terminal close state, genuine store-error propagation, and a retirement-ordering test with a deterministic positive-observation oracle (goroutine-stack proof that refresh is blocked on the publication write lock while a public operation is parked inside the store), nonblocking event recording, bounded waits, and per-launch worker registration. Validation: differential runs against the verbatim pre-matrixorigin#27135 refresh() fail 7 of the 9 new tests with premature-close, lastAddress-advance, double-close, and lock-held-across-factory-work evidence; targeted mutants (early return, single/duplicate early retirement, missing retirement) all fail deterministically with completed cleanup; the fixed code passes the focused set, race x3, race x20 on the revised tests, and the full package.
d048f07 to
34f751c
Compare
|
mo-bot review · Commit: Review completed. Read the assessment. |
XuPeng-SH
left a comment
There was a problem hiding this comment.
mo-bot review · mo-bot-local · review
Commit: 34f751cf801e0d8015dbdcc38e97dea23614f454
Assessment: approve
Reviewed the complete test-only delta and storage lifecycle owners. The closure-sensitive fixture strengthens existing coverage with observable operation results, retirement counts and ordering assertions. Barrier release, worker joining and holder cleanup are correctly ordered. No actionable defects found; production behavior and SQL performance are unchanged.
Model: gpt-6-astra / xhigh
Session: 01a12d8f-8e48-7622-ad17-b605f08450ce · slot 1 · generation 0
Validation:
-
All nine new tests passed in normal mode: focused-normal.log, receipt 1791998835797597715-2.json; package execution took 0.088s.
-
Recovery and retirement-ordering tests each passed 100 repetitions under -race, with nonempty selections and exit 0: receipts 1791998803618704105-2.json and 1791998803618247719-2.json.
-
A diagnostic Go overlay introducing early retirement triggered the intended ordering assertion and exited without hanging: mutation-early-retirement.log, receipt 1791998831341298114-2.json. This expected negative-control failure is not a candidate failure.
-
Isolation preflight passed. Reconciled 192 native inputs and 46 retained artifacts against the current sources and repository build receipt before testing.
-
Reused successful frozen-head Ubuntu UT and arm64 static-analysis CI. Source integrity, unchanged checkout and diff checks passed; validation-audit.json maps commands, logs, source fingerprints and exits under the assigned diagnostic directory.
Limitations:
-
The ordering oracle depends on runtime stack-frame names; local validation covered the current Linux/amd64 toolchain.
-
Author-reported historical mutation counts and live SQL replay lacked retained receipts and were not independently credited. No BVT rerun was required for this test-only change.
-
The linked issue's full text was absent; its contract was assessed from the supplied PR description and existing SQL overlap case.
Merge Queue Status
This pull request spent 8 minutes 43 seconds in the queue, with no time running CI. ReasonPull request #30147 has been dequeued Pull request from fork cannot be queued. This pull request comes from a fork, and Mergify needs the author's permission to update its branch.
HintYou should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it. Tick the box to put this pull request back in the merge queue (same as
|
What type of PR is this?
Which issue(s) this PR fixes:
Related to #24144
What this PR does / why we need it:
Adds the regression coverage that issue #24144 asked for, proving the task-storage
refresh lifecycle contract that the issue's CI failure violated.
Root cause (already fixed on main; this PR adds the missing proof)
refreshableTaskStorage.refresh()used to execute_ = s.mu.store.Close()atfunction entry — before rejecting a stale
lastAddressrequest and beforestoreFactory.Create()confirmed a replacement existed — and advanceds.mu.lastAddressto the new address beforeCreatesucceeded. A stale refreshor a failed creation therefore left the holder serving a closed store (or the old
store paired with the replacement address), and the ping that precedes every
forwarded SQL-task operation surfaced
sql: database is closed: in composepessimistic CI,
execute task sql_task_overlapfailed,overlap_sinkreturned 1instead of 2, and only one
SUCCESSrun was recorded.The production ordering fix is already on main (commit
26226d2a57, PR #27135,which referenced issue #27120 and never linked #24144): refresh now rejects
closed/stale requests before any factory work, preserves both
mu.storeandmu.lastAddresswhen address resolution orCreatefails, publishes thereplacement store/address together under the lock after a generation re-check,
and retires only a distinct previous store. Issue #24144 lacked its named
regression coverage: the two requested tests did not exist, and existing
coverage could not detect premature closure because
memTaskStorage.CloseandPingContextare no-op successes, so pointer assertions passed even when theinstalled store had been closed.
This PR is intentionally test-only (
pkg/taskservice/task_service_holder_test.go,+602 lines) so #24144 can be closed with concrete regression evidence. No
production behavior changes.
Exact changes
TestRefreshTaskStorageSkipStaleRefreshKeepsCurrentStore(issue-named):a usable store at s1 plus a stale request carrying s2 → zero closes, zero
address/Create calls, unchanged store/address, and successful subsequent
public
AddSQLTask/QuerySQLTaskthrough a closure-sensitive store whosePingContextfails withsql: database is closedonce closed.TestRefreshTaskStorageCreateFailureKeepsCurrentStore(issue-named): tableover Create error, nil store, and address-factory error at s2 while serving
s1 → current store preserved and usable,
lastAddressstays s1.TestRefreshTaskStorageStaleRequestAfterReplacementKeepsNewStore: a laterserialized request still carrying the old address must not close the new
store; the distinct previous store is retired exactly once.
TestRefreshTaskStorageFixedFactoryReuseKeepsStoreOpen: a fixed factoryreturning the active instance must not retire it.
TestRefreshTaskStorageSameAddressReplacementPublishesNewInstance: a freshinstance at the same address is a distinct replacement (retire once, publish).
TestRefreshTaskStorageRecoversAfterInitialFailure: failed initial and queuednot-ready refreshes keep the holder not-ready (
ErrNotReady); an observedrecovery refresh publishes a working store (atomic failure flag, factory
attempts announced on a channel so the async worker's read and the test's
write are happens-before ordered).
TestRefreshTaskStorageClosedStateIsTerminal: afterClose,maybeRefreshreturns false, refresh does no factory work and no secondclose, and
Closeis idempotent.TestRefreshTaskStorageStoreErrorReachesCaller: a genuine store ping errorreaches the caller; preserving ownership must not fabricate success.
TestRefreshTaskStorageOperationCompletesBeforeRetirement: deterministicordering proof — refresh parks inside the factory (entry pre-check already
passed), a public operation then parks inside the gated store holding
mu.RLock, the resumed refresh is positively observed (goroutine-stackinspection) blocked on the publication write lock, any retirement attempt or
refresh completion while the reader is parked fails immediately, and only then
is the operation released; bounded waits assert exactly the two recorded
events in the order
add-completed,retired(detecting missing andduplicated retirement), with nonblocking event recording, per-launch worker
registration, and cleanup that releases barriers, joins workers, then closes
the holder.
Issue-to-test proof
refresh()(
git show 26226d2a57^) swapped in, 7 of the 9 new tests fail promptly withthe issue's exact defects — premature close on stale request, on all three
failed-refresh injections, on stale-after-replacement, and on fixed-factory
reuse;
lastAddressadvanced on failed creation; double close after terminalClose; and the ordering test fails because the pre-fix refresh holds thewrite lock across factory work. With the fixed code all pass.
5/5 in 0.00 s), single early retirement before the publication lock
(10/10 in ≤0.08 s), duplicate early retirement (5/5 in 0.00 s, workers
joined), and missing retirement (3/3 at the 15 s bound) all fail
deterministically with completed cleanup; the healthy implementation passes
20/20 plain and 20/20 under
-race.sql_task.sqloverlap section (two manual executions withsleep(1)between, 10 s retry-polls) returned
overlap_sinkcount = 2 and twoSUCCESSruns with zerosql: database is closedoccurrences, matchingtest/distributed/cases/task/sql_task.result.Test commands
All green (focused 27 tests including the 9 new; full package includes the
sql-mock trigger/acquire-complete and SQL-task executor tests).
Residual risks
referenced taskservice: stale refresh can close active task store and surface as "sql: database is closed" #24144 — hence the non-closing reference here.
refresh frame and
sync.(*RWMutex).Lock/runtime_SemacquireRWMutex; a futurerefresh redesign fails the test at its 15 s bound with an accurate message
(intended regression behavior, verified by the mutant runs).
taskservice), consistent with theexisting holder tests;
requireassertions stay outsidemu.RLock(
holderState()helper) and barrier releases precede worker join and holderclose in defer order.