exclusive_locks: clear renew-running flag before dropping isLocked (#10413)

On renewal failure the renew goroutine stored isLocked=false before its
deferred renewGoroutineRunning.Store(false) ran. A concurrent RequestLock
interleaving there reacquires the lease (sees isLocked=false), sets
isLocked=true, then its CompareAndSwap on renewGoroutineRunning fails because
the old goroutine's flag is still set — so no replacement renewer starts. The
lock is then held locally with nothing renewing it, and silently expires on
the master after the lease TTL, admitting a second holder.

Clear renewGoroutineRunning before isLocked on the failure path so the
reacquire path always starts a fresh renewer. Builds and vets clean; no
behavior change on the success path.

Claude-Session: https://claude.ai/code/session_01Ks16jnt4S7gdDk8cheQ3xu
This commit is contained in:
Chris Lu
2026-07-23 12:49:29 -07:00
committed by GitHub
parent 2e9b944e5c
commit fe0a357624
@@ -93,13 +93,15 @@ func (l *ExclusiveLocker) RequestLock(clientName string) {
if l.renewGoroutineRunning.CompareAndSwap(false, true) {
// start a goroutine to renew the lease
go func() {
defer l.renewGoroutineRunning.Store(false)
ctx2, cancel2 := context.WithCancel(context.Background())
defer cancel2()
for {
if err := l.renewLease(ctx2); err != nil {
glog.Warningf("Failed to renew lock %s: %v", l.lockName, err)
// clear the running flag before isLocked, so a RequestLock that
// reacquires (once isLocked is false) starts a replacement renewer
l.renewGoroutineRunning.Store(false)
l.isLocked.Store(false)
return
}