{"record":{"id":"8c467c339a790c46","repo":"jaegertracing/jaeger","slug":"aihealth-start-called-twice","errorCode":null,"errorMessage":"aihealth: Start called twice","messagePattern":"aihealth: Start called twice","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/jaeger/internal/extension/jaegerquery/internal/jaegerai/aihealth/checker.go","lineNumber":50,"sourceCode":"\tTimeout  time.Duration\n\tLogger   *zap.Logger\n\n\tcurrent atomic.Bool\n\n\tcancel context.CancelFunc\n\tdone   chan struct{}\n}\n\n// Current returns the most recently observed health state. Initial value is\n// false until the first check completes.\nfunc (c *Checker) Current() bool { return c.current.Load() }\n\n// Start launches the checker's background goroutine. The first check runs\n// immediately so callers see the truth as soon as possible; subsequent\n// checks are spaced by Interval. Start may be called only once per Checker.\nfunc (c *Checker) Start(ctx context.Context) {\n\tif c.done != nil {\n\t\tpanic(\"aihealth: Start called twice\")\n\t}\n\tc.Logger.Info(\"Starting AI health checker\", zap.Duration(\"interval\", c.Interval), zap.Duration(\"timeout\", c.Timeout))\n\tctx, cancel := context.WithCancel(ctx)\n\tc.cancel = cancel\n\tc.done = make(chan struct{})\n\tgo c.run(ctx)\n}\n\n// Stop signals the background goroutine to exit and waits for it. Safe to\n// call multiple times; safe to call before Start (no-op).\nfunc (c *Checker) Stop() {\n\tif c.done == nil {\n\t\treturn\n\t}\n\tc.cancel()\n\t<-c.done\n}\n","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/jaegertracing/jaeger/blob/806f4447841ecdb60519f408b004a599d515f437/cmd/jaeger/internal/extension/jaegerquery/internal/jaegerai/aihealth/checker.go#L32-L68","documentation":"Checker.Start launches a background goroutine that periodically reports AI health, and a Checker supports exactly one lifecycle: Start must be called once; a second call panics because c.done is already set. This guards against double-starting timers/cancel channels that would leak goroutines.","triggerScenarios":"Calling Checker.Start(ctx) twice on the same Checker instance, e.g. invoking Start again after an earlier Start without constructing a fresh Checker.","commonSituations":"Test setups that share a Checker across subtests; extension hot-reload paths calling Start again; retry logic that re-invokes Start on transient failure instead of creating a new Checker.","solutions":["Ensure Start is called exactly once per Checker instance, typically during extension startup only","If a restart is needed, build a new Checker instead of reusing the old one","If you intentionally need re-startability, reset c.done/cancel in a Stop() method before calling Start again"],"exampleFix":"// before\nchecker.Start(ctx)\n// later, on reconnect\nchecker.Start(ctx) // panics\n// after\nchecker.Start(ctx)\n// on reconnect\nchecker = jaegerai.NewChecker(cfg, deps)\nchecker.Start(ctx)","handlingStrategy":"validation","validationCode":"if c.done != nil { return errors.New(\"checker already started\") }","typeGuard":"func started(c *Checker) bool { return c.done != nil }","tryCatchPattern":"defer func() { if r := recover(); r != nil { t.Fatalf(\"Start called twice: %v\", r) } }()","preventionTips":["Start the checker only in one place (extension Start), guarded by sync.Once","Construct a fresh Checker for any restart scenario","Write a lifecycle test asserting Start is called once per instance"],"tags":["panic","lifecycle","double-start","goroutine"],"backgroundTag":"already-started","analyzedSha":"806f4447841ecdb60519f408b004a599d515f437","analyzedAt":"2026-09-01T02:39:22.140Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}