flake(wfengine): extend tester timeout so deadlock-detection stops false-firing under CI load (bookshelf-v8o1o) #1462
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-v8o1o"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Root cause
The go-workflows tester's deadlock-detection heuristic (
tester.go:458-470ingithub.com/cschleiden/go-workflows/tester@v1.4.1) waitsTestTimeout(default 10s wall-clock) for the next event before panicking with
"No new events generated during workflow execution and no pending timers, workflow blocked?". Activities are executed on real goroutines(
scheduleActivity) even though workflow time is simulated via a mock clock,so under CI CPU starvation that 10s wall-clock budget can be exhausted even
though the workflow is NOT actually deadlocked — producing an intermittent
false-positive panic. Confirmed by reading
tester.goaround line 470 andtester/options.go(WithTestTimeoutdirectly setsoptions.TestTimeout,the value read at line 458).
Observed on CI Coverage job for PR #1461 (
internal/wfengineTemplateGenerateWorkflow"when activity fails returns an error from theworkflow" spec). Already documented in-code that the error-propagation specs
use large
RetryOptionstimers, but the timers themselves are not theproblem — this is the tester's real-time idle-wait budget, not workflow
simulated time.
Fix
Added a shared
newWorkflowTester[TResult]wrapper ininternal/wfengine/test_helpers_test.gothat passestester.WithTestTimeout(60 * time.Second)to every tester construction, thenmechanically switched all 240 call sites across 26
internal/wfengine/*_test.gofiles from
tester.NewWorkflowTester[T](...)tonewWorkflowTester[T](...)(same call syntax, so every existing test keeps its exact assertions — no
weakening, no deleted specs, no shrunk retry timers).
replay_golden_test.gohad its now-unused
testerimport removed.Test plan
go vet ./internal/wfengine/...— cleango test ./internal/wfengine/...— 2072 specs passginkgo -repeat=2 ./internal/wfengine/...withGOMAXPROCS=1(simulates CICPU starvation) — 2072 specs x2 runs, all green, no tester panics
make test,make lint— passmake coverage— 100% oninternal/wfengine(and repo-wide gate passes)Closes bead bookshelf-v8o1o on merge.
Security review (adversarial, read-only) — PR #1462 (bd-bookshelf-v8o1o)
Scope confirmed: all 28 changed files match
internal/wfengine/*_test.go; no production code (internal/,cmd/, etc. outside*_test.go) is touched. Diffed non-mechanical lines and confirmed the ONLY substantive additions are in the newtest_helpers_test.go: atesterIdleTimeout = 60 * time.Secondconstant and anewWorkflowTester[TResult]wrapper aroundtester.NewWorkflowTesterthat appendstester.WithTestTimeout(testerIdleTimeout). Every other changed line across the 27 other files is a 1:1 mechanical substitution oftester.NewWorkflowTester[...](...)→newWorkflowTester[...](...)— noExpect/assertion lines, stub behavior, retry policy, permanent-error classification, version-gating, or fan-out-bound assertions were touched.Checks:
git diff --name-only origin/main...origin/bd-bookshelf-v8o1oreturns onlyinternal/wfengine/*_test.gopaths.Skip/XIt/PIt/removedExpectfound. Black-box discipline preserved (package wfengine_test, per project convention).No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Security review (adversarial, read-only) — PR #1462 (bd-bookshelf-v8o1o)
Scope confirmed: all 28 changed files match
internal/wfengine/*_test.go; no production code (internal/,cmd/, etc. outside*_test.go) is touched. Diffed non-mechanical lines and confirmed the ONLY substantive additions are in the newtest_helpers_test.go: atesterIdleTimeout = 60 * time.Secondconstant and anewWorkflowTester[TResult]wrapper aroundtester.NewWorkflowTesterthat appendstester.WithTestTimeout(testerIdleTimeout). Every other changed line across the 27 other files is a 1:1 mechanical substitution oftester.NewWorkflowTester[...](...)→newWorkflowTester[...](...)— noExpect/assertion lines, stub behavior, retry policy, permanent-error classification, version-gating, or fan-out-bound assertions were touched.Checks:
git diff --name-only origin/main...origin/bd-bookshelf-v8o1oreturns onlyinternal/wfengine/*_test.gopaths.Skip/XIt/PIt/removedExpectfound. Black-box discipline preserved (package wfengine_test, per project convention).No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Code Review: PR #1462 (bd-bookshelf-v8o1o)
Reviewed diff only (per policy, CI is behavioral truth -- did not re-run tests).
Summary: Purely mechanical test-only fix. Introduces
newWorkflowTester[TResult]intest_helpers_test.go(packagewfengine_test) that wrapstester.NewWorkflowTester[TResult]and appendstester.WithTestTimeout(60*time.Second)via variadicopts = append(opts, ...). All 240 call sites across 26 files renamedtester.NewWorkflowTester[T]->newWorkflowTester[T], with no other change. Diff stat matches the bead description exactly: 28 files, +264/-241, entirely withininternal/wfengine/.Verification performed
tester.options.TestTimeoutdefault is 10s (tester.go:197) and the timeout is a per-iteration idle-wait budget reset every time a new event/callback/timer fires (tester.go:436-471ingo-workflows@v1.4.2), not a hard wall-clock for the whole test. So the 60s change cannot silently extend a genuinely deadlocked test past 60s of actual idleness -- it only tolerates CPU-starvation-induced idle gaps between real events. This matches the in-code rationale comment.optsare preserved and only the timeout option is appended (append-after, not prepend/replace), and sinceWorkflowTesterOptionfuncs are applied in order with last-write-wins semantics onTestTimeout, appending after caller-supplied opts means the wrapper's 60s always "wins" forTestTimeoutspecifically -- no other option field is affected.tester.WithTestTimeout(60*time.Second)(bulk_workflow_test.go:809, recalc_scores_workflow_test.go x8, scan_workflow_test.go:340, simple_workflows_test.go:305) -- all already used the same 60s value, so the append is a harmless no-op duplicate at those sites, not a conflict.NewWorkflowTesterrenames (variable nameswt,wt2,wt3,wtEmpty) -- zero unrelated edits found (no assertion changes, no skipped specs, no Register/Execute alterations).replay_golden_test.go's removed"github.com/cschleiden/go-workflows/tester"import is correctly dead after all 15 call sites in that file switched to the local helper; rest of import list left intact.newWorkflowTesterlives in a_test.gofile underpackage wfengine_test(black-box convention) -- not exported to production code.internal/wfengine/*_test.go-- no production code, no other package.Findings
[MINOR] internal/wfengine/bulk_workflow_test.go:119, recalc_scores_workflow_test.go:186,195,204,213,222,231,240,249,258, scan_workflow_test.go:316, simple_workflows_test.go:374 -- now-redundant explicit
tester.WithTestTimeout(60*time.Second)optionThese call sites still pass an explicit
tester.WithTestTimeout(60*time.Second)alongside the newnewWorkflowTesterwrapper, which now also appends the same 60s value. Functionally harmless (last-applied-wins, same value), but it's dead/confusing code post-refactor -- a future reader may wonder whether it's double-counted. Suggest a fast-follow cleanup PR to drop the now-redundant explicit options at these ~12 sites, keeping only cases that ever need a different timeout than the shared default (none currently do).REVIEW VERDICT: 0 blocker, 0 major, 1 minor