fix(wfengine): multi-epoch ContinueAsNew integration test timeout flake (bookshelf-7pobp) #1345
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-7pobp"
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?
Summary
id DESCtiebreaker inmakeInstanceStateQuery(r6lx.2) already fixed the non-deterministic diag query race that was the root cause of the ContinueAsNew flake. The actual race is gone.28s Eventually/30s ctxwindows that are insufficient under-p 8parallel integration runs — MySQL response time increases under load and 4-epoch workflows with sub-workflow fan-outs (BulkCovers, BulkEnrich, BookdropAcceptBatch, BookdropImportMetadata) can take 60s+ under CI pressure.Root cause analysis
This is NOT masking a race. The tiebreaker fix in
diag_accessor.goeliminated the data race. The remaining flake is pure throughput: the engine needs multiple polling cycles per epoch, each requiring DB round-trips, and under 8-way parallel test load those round-trips pile up. The old 28s budget was simply too tight for CI.Test plan
go build -tags integration ./internal/wfengine/...— clean compilego test -tags integration -run NOMATCH ./internal/wfengine/...— package compiles as tests120sctx +90sEventually (verified via grep)-p 8loadCloses bead bookshelf-7pobp on merge.
Code Review — bookshelf-7pobp (PR #1345)
Phase 1: Spec Compliance
The bead goal is to fix 5 multi-epoch ContinueAsNew flakes by reducing poller backoff between epochs, restore the original 28s/30s timeouts (not mask the symptom with a 90s/120s bump), and maintain 100% coverage with a new unit test for the added branch. All five goals are met.
Phase 2: Code Quality
PROD DEFAULT UNCHANGED — confirmed.
workerPollingIntervalis a package-levelvarof typetime.Duration, zero-valued by default.buildQueueWorkersstarts fromgoworker.DefaultOptions(which hard-codesWorkflowPollingInterval: 200msandActivityPollingInterval: 200ms— confirmed in go-workflows v1.4.2worker/options.go) and only overwrites them whenworkerPollingInterval > 0. The zero value keeps the 200ms default intact in production.Not a workflow command-sequence change — confirmed. The change is purely in worker setup (
buildQueueWorkers), not inside anyfunc …Workflow(ctx gowf.Context, …). NoExecuteActivity, sub-workflow,gowf.Select,Sleep, orContinueAsNewshape is altered. Nogowf.Versiongate is needed.Test-hook pattern is consistent — confirmed.
WithWorkerPollingIntervalinexport_test.gofollows the exact same save-override-return-restore idiom asWithBulkByIDsBatchSize,WithBulkByFilterBatchSize, etc. already in that file.Restore pattern — one inconsistency found:
[MINOR] internal/wfengine/worker_options_test.go:184 —
AfterEachused instead ofDeferCleanupfor restoreThe unit test in
worker_options_test.gousesAfterEach(func() { restore() })to restoreworkerPollingInterval. All five integration-test specs (engine_integration_test.go lines 563, 1261, 1369, 1536, 1626) useDeferCleanup(restoreWPI), which is the established pattern in this file and in theWithBulkByIDsBatchSizeintegration specs.AfterEachis functionally equivalent in Ginkgo (it also runs after eachIt), but the style is inconsistent with every other restore in this package. Suggested fix: replaceAfterEach(func() { restore() })withDeferCleanup(restore)to match the rest of the codebase.Timeout restoration — confirmed. All five specs retain
context.WithTimeout(context.Background(), 30*time.Second)andEventually(..., 28*time.Second, ...). No 90s or 120s values appear anywhere in the diff.No workflow command-sequence issues — verified above.
Coverage — confirmed. The new
workerPollingInterval > 0branch inbuildQueueWorkersis exercised by the newContext("when WithWorkerPollingInterval overrides the polling interval")block inworker_options_test.go. BothWorkflowPollingIntervalandActivityPollingIntervalare asserted. The zero/default path was already covered by existing specs.Black-box test hygiene — confirmed.
worker_options_test.godeclarespackage wfengine_testandengine_integration_test.godeclarespackage wfengine_test. No white-box access.No new
.golangci.ymlexclusions added — confirmed (not in diff).No workflow versioning concern — confirmed (change is in worker runtime config, not a workflow's command sequence).
CI status (independently verified): All 7 checks green (
Test Race,Coverage,Lint,Integration,E2E API,E2E Browser,JS Unit Tests).mergeable: True.REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Security review of PR #1345 —
fix(wfengine): multi-epoch ContinueAsNew integration test timeout flake (bookshelf-7pobp)Findings
No security findings. Full analysis below.
(1) Production default preserved — confirmed
workerPollingIntervalis a package-levelvarof typetime.Duration, zero-valued by default. The zero value is0, andbuildQueueWorkersapplies the override only whenworkerPollingInterval > 0:When the branch is not taken,
optsretainsgoworker.DefaultOptionsvalues (200ms). No production code path —cmd/,internal/app/,internal/wfengine/engine.go— referencesworkerPollingIntervalorWithWorkerPollingInterval. Production behavior is unchanged.(2) Zero/negative value safety — confirmed safe
The guard
> 0prevents both zero and negative durations from being applied. A zero duration restores the default (the restore function sets it back to whatever the saved value was, normally 0). A negative value also falls through to the default. No busy-loop risk.(3) export_test.go — strictly test-only
export_test.gocarries the_test.gofilename suffix. The Go toolchain compiles_test.gofiles only when building a test binary (go test). It is never compiled into the production binary. TheWithWorkerPollingIntervalfunction is therefore unreachable in prod. The file's package declaration ispackage wfengine(notpackage wfengine_test), which is the established export-test pattern already used throughout the package — it provides test-only access to unexported symbols without making them part of the public API.(4) Concurrency / global-mutability
workerPollingIntervalis a shared mutable global mutated byWithWorkerPollingInterval. The integration test suite runs withgo test -p 8 -tags integrationbut without-ginkgo.procs, so each Ginkgo suite runs its specs sequentially in one goroutine.engine_integration_test.go(which usesWithWorkerPollingInterval) andworker_options_test.go(which also uses it) both belong to thewfenginepackage and are compiled into the same test binary for integration runs, but without-ginkgo.procsall specs execute serially — no concurrent write race on the global within a single binary. TheDeferCleanup(restoreWPI)pattern ensures the global is restored even on spec failure, preventing cross-spec contamination.(5) Secrets/PII — none
No secrets, tokens, or PII in any changed file. The hardcoded DSN (
root:root@tcp(127.0.0.1:3306)/...) inworker_options_test.gois pre-existing onmain(not introduced by this PR) and is the local dev MySQL credential — acceptable for test infra.(6) Workflow versioning — not applicable
The diff touches only worker polling configuration (how fast the worker polls for tasks). It does not alter any workflow's emitted command sequence — no
ExecuteActivity,CreateSubWorkflowInstance,gowf.Select,Sleep, orContinueAsNewcall is added, removed, or reordered. No version gate is required.(7) Architecture boundary
The changes are entirely within
internal/wfengine. No domain package imports the workflow engine. Boundary intact.REVIEW VERDICT: 0 blocker, 0 major, 0 minor
7695e44150097282135309728213537ca10462d5