Skip to content

fix(safego): eliminate WithRestartTimeout data race by capturing restartTimeout at spawn - #1465

Open
kotwal-itpro wants to merge 1 commit into
jitsucom:newjitsufrom
kotwal-itpro:fix/safego-test-data-race
Open

fix(safego): eliminate WithRestartTimeout data race by capturing restartTimeout at spawn#1465
kotwal-itpro wants to merge 1 commit into
jitsucom:newjitsufrom
kotwal-itpro:fix/safego-test-data-race

Conversation

@kotwal-itpro

Copy link
Copy Markdown

Problem

Execution.WithRestartTimeout mutates exec.restartTimeout from the caller's goroutine, while the spawned goroutine's panic-recovery defer reads that same field on restart. When a caller uses the documented RunWithRestart(f).WithRestartTimeout(t) builder pattern (as safego's own TestHandlePanicAndRestart did), the goroutine is already running by the time the setter fires — any panic in the interim races the write.

go test -race ./bulker/jitsubase/safego/ reproduces it deterministically on current newjitsu:

==================
WARNING: DATA RACE
Read at 0x... by goroutine 9:
  github.com/jitsucom/bulker/jitsubase/safego.(*Execution).run.func1.1()
      bulker/jitsubase/safego/safego.go:56 +0x70
  runtime.gopanic()
      runtime/panic.go:860 +0x128
  github.com/jitsucom/bulker/jitsubase/safego.(*Execution).run.func1()
      bulker/jitsubase/safego/safego.go:62 +0x78

Previous write at 0x... by goroutine 8:
  github.com/jitsucom/bulker/jitsubase/safego.(*Execution).WithRestartTimeout()
      bulker/jitsubase/safego/safego.go:68 +0x19c
  github.com/jitsucom/bulker/jitsubase/safego.TestHandlePanicAndRestart()
      bulker/jitsubase/safego/safego_test.go:24 +0x198
==================
    testing.go:1712: race detected during execution of test
--- FAIL: TestHandlePanicAndRestart (0.30s)
FAIL	github.com/jitsucom/bulker/jitsubase/safego	0.669s

safego.RunWithRestart / safego.Run are called in 20+ production paths across bulker/, ingest/, kafkabase/, sync-controller/, bulkerapp/, eventslog/, and jitsubase/, so the primitive is load-bearing. No current production caller happens to chain WithRestartTimeout, so the race does not fire in shipped code — but the pattern is publicly exported and documented, so any future caller (or a copy-paste from the existing test) trips it.

Fix (backward compatible)

  1. New constructor RunWithRestartTimeout(f func(), timeout time.Duration) *Execution — sets the timeout before the goroutine spawns, eliminating the need for the mutating builder pattern in the common case.
  2. run() captures restartTimeout into a function-local at spawn time, so a subsequent mutation via the deprecated setter (or any future mutation of the shared Execution) cannot race the goroutine's recovery handler.
  3. WithRestartTimeout retained for source-level backward compatibility and marked with a Deprecated: doc comment pointing callers at the race-free constructor.

Test

Updates TestHandlePanicAndRestart to use the new constructor and an atomic.Int32 counter (the previous plain-int shared counter was itself flagged by -race).

Verified

  • go test -race -count=5 ./bulker/jitsubase/safego/ — 5/5 pass (was consistently failing before)
  • go build ./... clean on jitsubase, kafkabase, bulkerlib, bulkerapp, ingest, sync-controller
  • No breaking API changes: Run, RunWithRestart, and WithRestartTimeout all keep their signatures; new RunWithRestartTimeout is additive

…artTimeout at spawn

`Execution.WithRestartTimeout` mutates `exec.restartTimeout` from the
caller's goroutine, while the spawned goroutine's panic-recovery `defer`
reads that field on restart. When a caller uses the documented
`RunWithRestart(f).WithRestartTimeout(t)` builder pattern (as safego's
own TestHandlePanicAndRestart does), the goroutine is already running
by the time the setter fires — any panic in the interim races the write.
`go test -race ./bulker/jitsubase/safego/` reproduces it deterministically:

    WARNING: DATA RACE
    Read at 0x... by goroutine N:
      safego.(*Execution).run.func1.1() safego.go:56
    Previous write at 0x... by goroutine M:
      safego.(*Execution).WithRestartTimeout() safego.go:68

safego.RunWithRestart / Run are called in 20+ production paths across
bulker/, ingest/, kafkabase/, sync-controller/, bulkerapp/,
eventslog/, and jitsubase/, so the primitive is load-bearing. No
current production caller happens to chain WithRestartTimeout, so the
race does not fire in shipped code, but the pattern is publicly
exported and documented — any future caller trips it.

Fix (backward compatible):

  1. New constructor RunWithRestartTimeout(f, timeout) sets the timeout
     before the goroutine spawns, eliminating the need for the mutating
     builder pattern for the common case.
  2. run() captures restartTimeout into a function-local at spawn time,
     so a subsequent mutation via the deprecated setter (or any future
     mutation of the shared Execution) cannot race the goroutine's
     recovery handler.
  3. WithRestartTimeout is retained for source-level backward
     compatibility and marked with a Deprecated doc comment pointing
     callers at the race-free constructor.

Also updates TestHandlePanicAndRestart to use the new constructor and
an atomic.Int32 counter (the previous plain-int shared counter was
itself flagged by -race). The test now passes -race -count=5.

Verified:
  - `go test -race -count=5 ./bulker/jitsubase/safego/` — 5/5 pass
  - `go build ./...` clean on jitsubase, kafkabase, bulkerlib,
    bulkerapp, ingest, sync-controller
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant