{"record":{"id":"27c178369e67a418","repo":"vitessio/vitess","slug":"bug-thread-with-id-v-already-finished","errorCode":null,"errorMessage":"BUG: thread with ID: %v already finished","messagePattern":"BUG: thread with ID: (.+?) already finished","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/throttler/throttler.go","lineNumber":237,"sourceCode":"\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\n// rate allotment across the other threads.\n// After ThreadFinished() is called, Throttle() must not be called anymore.\nfunc (t *ThrottlerImpl) ThreadFinished(threadID int) {","sourceCodeStart":219,"sourceCodeEnd":255,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/throttler/throttler.go#L219-L255","documentation":"ThrottlerImpl.Throttle() panics if the given threadID was already marked finished via ThreadFinished(). The throttler's contract is that after ThreadFinished() returns, that thread must never call Throttle() again; violating it indicates a worker loop that kept running past its completion signal.","triggerScenarios":"Calling Throttle(threadID) for a threadID on which ThreadFinished(threadID) was already called — typically a worker goroutine that continues its loop after marking itself finished, or a duplicated/reused threadID.","commonSituations":"Worker goroutines not respecting a stop flag before their next Throttle iteration; registering two threads with the same ID so one's ThreadFinished marks the other finished prematurely.","solutions":["Return from the worker loop immediately after calling ThreadFinished()","Ensure every thread registers a unique threadID (check newThread() usage)","Add a done-channel check before each Throttle call in the worker loop"],"exampleFix":"// before\nthrottler.ThreadFinished(threadID)\nfor { throttler.Throttle(threadID) }\n// after\nthrottler.ThreadFinished(threadID)\nreturn","handlingStrategy":"validation","validationCode":"if finished[threadID] {\n    return // skip Throttle; thread already marked finished\n}\nd := throttler.Throttle(threadID)","typeGuard":null,"tryCatchPattern":"defer func() {\n    if r := recover(); r != nil {\n        if strings.Contains(fmt.Sprint(r), \"already finished\") {\n            return\n        }\n        panic(r)\n    }\n}()","preventionTips":["Return immediately after ThreadFinished()","Ensure unique threadID per goroutine","Check a stop flag before each Throttle iteration"],"tags":["go","panic","throttler","lifecycle","thread-management"],"backgroundTag":"use-after-finish","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}