Repository navigation
test(harness): bound the deadlock partner read/cleanup and report the thrown error (Ref #68, #37) - #75
Conversation
… thrown error (Ref #68, #37) Test support only. The partner stdout read used stream_set_timeout() + blocking fgets(), which proc_open pipes do not honour: a frozen partner hung the run past its declared 30 s deadline, and the old shutdown (SIGTERM + proc_close) blocks forever on a stopped child on Linux. - add PartnerProcess: stream_select() reads against a monotonic deadline, sanitized/truncated diagnostics, idempotent stop() that escalates SIGTERM -> SIGKILL, reaps the child and closes all pipes - DeadlockRetryIntegrationTestCase uses it; the 30 s deadline, the 15 s awaitBlockedLockingRead() wait and all assertions are unchanged - exhaustion test: assertCount(4) message now reports callback attempts, deadlocks provoked and the thrown class/SQLSTATE/driver code/message - PartnerProcessIntegrationTest: frozen real partner fails within the deadline and is killed and reaped; normal partner positive control No change to src/, retry logic, dependencies, CI or standards. The original "2 vs 4" failure remains UNVERIFIED. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Technical Lead — Fresh Full Re-Review on exact HEAD: MERGE NO-GO (2 concrete test-harness reliability findings)Independent evidence: Reviewed CI is green, NOT the blocker: exact-HEAD Run #39935592448 10/10 SUCCESS. Independently checked Lowest job F75-01 — BLOCKER: negative SIGSTOP test has a startup race (new flake)
F75-02 — BLOCKER: advertised bounded stop is not enforced if SIGKILL fails
Both findings arise inside this PR's exact objective (deterministic timeout and bounded test process lifecycle) and can be fixed in its test-only scope. No new runtime work or weakening of assertions. Separate already existing issue is not a substitute for these blockers. Separate out-of-scope discovery filed: #76 tracks the same legacy pipe/teardown pattern in Required delivery: one or more normal commits to PR #75 (no amend/force-push) addressing F75-01/02, targeted RED/GREEN/negative controls on real MySQL, repeat Linux and macOS-relevant cases, local full gates and new exact-HEAD GitHub 10/10. Include Base/HEAD/Tree with proofs in an updated PR report. Lead will perform another fresh exact-Tree review before MERGE GO. No merge or auto-merge now. |
…stop (F75-01, F75-02; Ref #68) F75-01: the frozen-partner control spawned the real partner and then sent SIGSTOP, so READY could be written first. It now starts the real partner script behind `sh -c 'kill -STOP $$; exec ...'`: the shell stops itself before exec, so nothing can be written before the freeze. A new PartnerProcess::awaitStopped() startup barrier waits for the kernel to report the stop (no sleep, no scheduling assumption). F75-02: stop() ignored failed SIGTERM/SIGKILL delivery and always called proc_close(), which blocks without bound on a child that is still running. Signal delivery now falls back to `kill`, stop() only reaps a child it has seen exit, and otherwise closes the pipes and throws with diagnostics without calling proc_close() (the child is leaked and a later stop() can retry). Teardown stops every partner even if one fails. A test seam on PartnerProcess::start() drives the failed-termination path. Test support only; no change to src/, retry logic, dependencies or CI. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Executor update — F75-01 / F75-02 fixed in a new commit (no amend, no force-push)
F75-01 — deterministic freeze-before-READYThe control now starts the real
F75-02 — no unbounded
|
Technical Lead — Independent Fresh Full Re-Review #2: MERGE NO-GO — F75-01 still has a deterministic Linux timing failureIdentities independently re-checked: PR #75 OPEN, mergeable, 2 sequential commits (no amend), Base CI verified independently, not blocking: exact-HEAD Actions run #39949251793 COMPLETED SUCCESS 10/10, including Lowest PHP 8.2 real MySQL 8.4.10 (audit clean, PHPStan no errors, Unit 383, Regression 97, Integration 226 twice, DB residue clean), Integration 8.2 + 8.5 (each Consumer Harness 2 passes + 226 Integration twice + residue clean), and final CI Gate. Local RED/GREEN and saturation tests remain executor-reported, not independently run by Lead. Disposition of previous findings
Required narrow fix: Preserve the stopped status from PR documentation follow-up: The PR body still cites old HEAD Decision: MERGE NO-GO on HEAD |
…barrier cannot lose it (F75-01b; Ref #68) The kernel reports a stop only to the first status query that sees it. PartnerProcess::start() queries the status to read the pid, so a child that stopped itself before that query had its stop event consumed and awaitStopped() waited for it until its deadline. All status queries now go through status(), which latches a reported stop; start() latches the one it makes, and awaitStopped() accepts a stop observed at any earlier point. Adds tests for a stop that happens after start() returned and for a child that never stops (barrier fails within its deadline). Test support only; no change to src/, retry logic, dependencies or CI. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Technical Lead — FINAL Fresh Full Acceptance Re-Review #3: MERGE GO for exact HEAD / TreeIdentifiers / scope verified independently on GitHub
Prior findings — all closed for PR acceptance
Exact-head independent GitHub CIRun #39966122280 attempt 1 on exact
Final decisionMERGE GO for this precise HEAD Owner's explicit authorization to merge remains required. No merge, auto-merge, new repo, tag or release executed by Lead. Once Owner squash-merges the exact reviewed HEAD, verify squash commit and Tree equals |
…ocess (Closes #76, Ref #37) (#77) * test(harness): run the LazySavepoint deadlock partner through PartnerProcess and clean up fail-safe (Ref #76, #37) Replaces the hand-rolled proc_open/fgets partner handling (nominal 30 s stream_set_timeout that does not bound a blocking read, teardown that terminated and closed without checking) with the bounded PartnerProcess from #75. tearDown now attempts every cleanup (stop partners, roll back connections, drop schema) and rethrows the first failure afterwards. Adds a frozen-LazySavepoint-partner negative to PartnerProcessIntegrationTest. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> * test(harness): surface rollback failures in tearDown and keep first-failure cleanup honest (Ref #76, #37, F77-01) tearDown no longer swallows inTransaction()/rollBack() failures (a false rollBack() counts too). All failures are collected after every cleanup step was attempted: one is rethrown as is, several are reported with the first as primary (a failed PartnerProcess::stop() stays first). Docblock now promises attempts, not guaranteed release. Adds two regression tests: a real rollback failure (killed server session) and a failed partner stop with the database cleanup still performed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]> --------- Co-authored-by: Claude Sonnet 5.5 <[email protected]>
Summary
Test-support-only hardening for #68 (Lead authorization: F68-01 / F68-02). Ref #68, #37. The original intermittent
2 vs 4failure stays UNVERIFIED; this PR neither explains nor claims to fix it, it makes the harness bounded and any recurrence diagnosable. #76 (the same legacy pattern inScopedOrderingManagerLazySavepointTest) is deliberately not touched.PartnerProcess(new,tests/Support/MySql/): stdout reads usestream_select()against a monotonichrtime()deadline (no busy spin) and fail with sanitized, truncated diagnostics (pid, waited time, running state, partner stderr).DeadlockRetryIntegrationTestCaseuses it; the 30 s partner deadline, the 15 sawaitBlockedLockingRead()wait, the 3-retry bound and every assertion are unchanged.stop(): close stdin → SIGTERM → SIGKILL after 2 s (delivery falls back tokillifproc_terminatefails) → reap → close pipes. It only callsproc_close()for a child it has seen exit; otherwise it closes the pipes and throws with diagnostics (pid, SIGTERM/SIGKILL delivery status), keeping the resource so a laterstop()retries. Teardown stops every partner and rethrows the first failure. Limitation: a child that survives SIGKILL is leaked and PHP may wait for it at shutdown.DeadlockPartner.phpbehindsh -c 'kill -STOP $$; exec …', so READY cannot be written before the freeze.PartnerProcess::awaitStopped()is the startup barrier. Because the kernel reports a stop only to the first status query that sees it, all status queries go throughstatus(), which latches the event — including the querystart()makes to read the pid — so a stop that happened beforestart()returned is accepted instead of lost.assertCount(4)keeps its strength and now reports callback attempts, deadlocks provoked and the thrown class / SQLSTATE / driver code / message;assertSame($finalDeadlock, $thrown)and the strict SQLSTATE/1213 checks are untouched.PartnerProcessIntegrationTest, 5 tests): frozen-before-exec partner fails within the deadline and is killed/reaped; fail-closedstop()with failed signal delivery (seam) then retry; barrier accepts a stop that happens afterstart()returned; barrier fails within its deadline for a child that never stops; normal partner READY positive control.No change to
src/, retry/transaction logic, dependencies, PHP requirements, CI, schema, standards or security policy. Windows is unsupported by the partner harness (POSIX pipes/signals; the test class skips there).Identity
main@a486cc7224945628e737c7f90978b0faad53c20c, tree1a7beb6871698010a4727b2b9f52da0d9fd3fedf8312752→8830047→ HEAD77e795040b7c3dd8e63afcadb89b53a38c1b4152, treed3446e86fd60fa62dbe407bd7db84445d39551a0tests/Support/MySql/PartnerProcess.php(new),tests/Support/MySql/DeadlockRetryIntegrationTestCase.php,tests/Integration/Pdo/Transaction/PdoDeadlockRetryRunnerIntegrationTest.php,tests/Integration/Support/PartnerProcessIntegrationTest.php(new)RED → GREEN (real MySQL 8.4.10 container, scratch copies; injected delays exist only in scratch)
readPartnerLine()(declared 30 s) on a SIGSTOPed real partner returned after 45.0 s, only when a watchdog killed the childproc_terminate+proc_closeon a stopped child returned only after a 20 s watchdogstop()bounded; TERM→KILL 2.0 sstop()with signal delivery failing: returned only after 20 s watchdogproc_close(); retry with delivery restored 2.0 s, child gone8830047+ 3 s delay betweenproc_open()and the firstproc_get_status()(macOS, real MySQL):did not stop within 10.0 s; Linuxphp:8.2-cli, 12 trials, 0.2 s delay: observed stop 0/12Callback attempts: 3; real deadlocks provoked: 2; runner threw RuntimeException (SQLSTATE n/a, driver code n/a): …Positive / stress controls: no-delay
PartnerProcessIntegrationTest15/15 idle and 10/10 under CPU saturation (48 burners, load avg ≥ 50); exhaustion test 20/20 repeated (both runners, 4 real deadlocks each, same final exception object); noDeadlockPartnerprocess left behind.Local gate matrix (real MySQL 8.4.10 container, PHP 8.4.4)
{lowest, Composer 2.8.6} and {latest, Composer 2.10.2}:
validate --strict, update, platform check,audit --abandoned=fail(clean),dump-autoload --strict-psr, syntax, PHPStan[OK] No errors, php-cs-fixer, unit 383, regression 97, integration 228 (×2), consumer harness,check:residue,check:whitespace— all passed. PHP 8.2/8.3/8.5 are covered by this PR's CI.Risks / notes
awaitStopped()latch records that the child stopped at some point; it is not cleared by SIGCONT (documented).stop()can take up to ~4 s only for a child that ignores SIGTERM; the normal path is immediate.🤖 Generated with Claude Code