{"record":{"id":"7ea918cbaa3d486e","repo":"vitessio/vitess","slug":"bug-thread-with-id-v-must-not-access-closed-thr","errorCode":null,"errorMessage":"BUG: thread with ID: %v must not access closed Throttler","messagePattern":"BUG: thread with ID: (.+?) must not access closed Throttler","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/throttler/throttler.go","lineNumber":234,"sourceCode":"\tgo func() {\n\t\tfor range rateUpdateChan {\n\t\t\tt.updateMaxRate()\n\t\t}\n\t}()\n\n\treturn t, nil\n}\n\n// Throttle returns a backoff duration which specifies for how long \"threadId\"\n// should wait before it issues the next request.\n// If the duration is zero, the thread is not throttled.\n// If the duration is not zero, the thread must call Throttle() again after\n// the backoff duration elapsed.\n// The maximum value for the returned backoff is 1 second since the throttler\n// internally operates on a per-second basis.\nfunc (t *ThrottlerImpl) Throttle(threadID int) time.Duration {\n\tif t.closed {\n\t\tpanic(fmt.Sprintf(\"BUG: thread with ID: %v must not access closed Throttler\", threadID))\n\t}\n\tif t.threadFinished[threadID] {\n\t\tpanic(fmt.Sprintf(\"BUG: thread with ID: %v already finished\", threadID))\n\t}\n\treturn t.threadThrottlers[threadID].throttle(t.nowFunc())\n}\n\n// MaxLag returns the max of all the last replication lag values seen across all tablets of\n// the provided type, excluding ignored tablets.\nfunc (t *ThrottlerImpl) MaxLag(tabletType topodata.TabletType) uint32 {\n\tcache := t.maxReplicationLagModule.lagCacheByType(tabletType)\n\tif cache == nil {\n\t\treturn 0\n\t}\n\treturn cache.maxLag()\n}\n\n// ThreadFinished marks threadID as finished and redistributes the thread's","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/throttler/throttler.go#L216-L252","documentation":"ThrottlerImpl.Throttle() panics if the throttler has been closed (via Close). After close, thread throttlers are torn down and any further Throttle() call from a worker thread is a lifecycle bug on the caller's side.","triggerScenarios":"Calling Throttle(threadID) on a ThrottlerImpl after another goroutine invoked Close() — e.g. a worker still running in its loop while the owner shut the throttler down during teardown.","commonSituations":"Shutdown ordering bugs in vreplication/migration streams: Close called while worker goroutines are mid-Throttle; tests closing the throttler in a defer before all threads finish.","solutions":["Signal worker threads to stop (context cancel / done channel) and wait for them to exit before calling Close()","Check t.closed or an equivalent guard in the caller's loop before each Throttle call","Ensure Close() is called only after all registered threads have called ThreadFinished()"],"exampleFix":"// before\nthrottler.Close()\nwg.Wait()\n// after\nwg.Wait()\nthrottler.Close()","handlingStrategy":"try-catch","validationCode":"select {\ncase <-done:\n    return\ndefault:\n    d := throttler.Throttle(threadID)\n    _ = d\n}","typeGuard":"func canThrottle(t *throttler.ThrottlerImpl) func() bool {\n    return func() bool { return !t.IsClosed() }\n}","tryCatchPattern":"defer func() {\n    if r := recover(); r != nil {\n        if strings.Contains(fmt.Sprint(r), \"closed Throttler\") {\n            return // benign during shutdown\n        }\n        panic(r)\n    }\n}()","preventionTips":["Stop worker loops via context cancel before Close()","wg.Wait() before calling Close()","Call ThreadFinished for every thread before shutdown"],"tags":["go","panic","throttler","lifecycle","shutdown"],"backgroundTag":"use-after-close","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}