{"record":{"id":"b3571dc4631b292e","repo":"vitessio/vitess","slug":"timestampbusy-when-borrowing-a-time","errorCode":null,"errorMessage":"timestampBusy when borrowing a time","messagePattern":"timestampBusy when borrowing a time","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/pools/smartconnpool/timestamp.go","lineNumber":89,"sourceCode":"\treturn monotonicNow() - t.get()\n}\n\n// update sets this timestamp's value to the current monotonic time\nfunc (t *timestamp) update() {\n\tt.nano.Store(int64(monotonicNow()))\n}\n\n// borrow attempts to borrow this timestamp atomically.\n// It only succeeds if we can ensure that nobody else has marked\n// this timestamp as expired. When succeeded, the timestamp\n// is cleared as \"busy\" as it no longer tracks an expiration point.\nfunc (t *timestamp) borrow() bool {\n\tstamp := t.nano.Load()\n\tswitch stamp {\n\tcase timestampExpired:\n\t\treturn false\n\tcase timestampBusy:\n\t\tpanic(\"timestampBusy when borrowing a time\")\n\tdefault:\n\t\treturn t.nano.CompareAndSwap(stamp, timestampBusy)\n\t}\n}\n\n// expired attempts to atomically expire this timestamp.\n// It only succeeds if we can ensure the timestamp hasn't been\n// concurrently expired or borrowed.\nfunc (t *timestamp) expired(now time.Duration, timeout time.Duration) bool {\n\tstamp := t.nano.Load()\n\tif stamp == timestampExpired {\n\t\treturn false\n\t}\n\tif stamp == timestampBusy {\n\t\treturn false\n\t}\n\tif now-time.Duration(stamp) > timeout {\n\t\treturn t.nano.CompareAndSwap(stamp, timestampExpired)","sourceCodeStart":71,"sourceCodeEnd":107,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/pools/smartconnpool/timestamp.go#L71-L107","documentation":"The timestamp state machine (used by idle-timeout tracking) only allows CAS transitions between a numeric stamp and the special timestampBusy sentinel. borrow() panics if it observes timestampBusy, which is impossible in correct usage because only the single goroutine that set Busy is allowed to read it and settle it back. Hitting this panic means two owners are contending for the same timestamp or a settle/timeout path double-used it.","triggerScenarios":"Calling borrow() while another goroutine already holds the timestamp in timestampBusy state — i.e. concurrent borrow/expire racing without honoring the single-owner invariant, or a bug in expire/settle that left the stamp stuck at Busy.","commonSituations":"Races introduced by modifying the idle-timeout expiration logic; reusing a resource's timestamp after its owner timed out; lock-free code changes in smartconnpool.","solutions":["Fix the caller/race so borrow is not invoked on a Busy timestamp (single-owner invariant)","Inspect the expire/settle code paths for a missed CompareAndSwap back from timestampBusy","Add synchronization or fall back to returning false (treat Busy as unavailable) if concurrent borrows are expected"],"exampleFix":"// before\ncase timestampBusy:\n    panic(\"timestampBusy when borrowing a time\")\n// after (if concurrency is expected)\ncase timestampBusy:\n    return false // busy, not borrowable","handlingStrategy":"validation","validationCode":"// before relying on borrow(), ensure single-owner usage:\n// only the goroutine that owns the resource should call borrow/settle","typeGuard":"func borrowable(t *timestamp) bool {\n    s := t.nano.Load()\n    return s != timestampBusy && s != timestampExpired\n}","tryCatchPattern":null,"preventionTips":["Never share a timestamp across goroutines without an owner handoff protocol","After modifying expire/settle logic, audit that every timestampBusy is CAS'ed back","Treat Busy as non-borrowable in new call sites instead of assuming exclusivity"],"tags":["go","panic","concurrency","atomic"],"backgroundTag":"concurrent-state-race","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}