vitessio/vitess · error
node %v is not locked
Error message
node %v is not locked
What it means
memorytopo's Unlock (via unlock) checks that the node at the locked path still holds an active lock channel before closing it. If the node has no lock recorded, this error is returned, indicating the lock was already released, lost, or was never acquired on that node.
Source
Thrown at go/vt/topo/memorytopo/lock.go:172
// Unlock is part of the topo.LockDescriptor interface.
func (ld *memoryTopoLockDescriptor) Unlock(ctx context.Context) error {
return ld.c.unlock(ctx, ld.dirPath)
}
func (c *Conn) unlock(ctx context.Context, dirPath string) error {
if c.closed.Load() {
return ErrConnectionClosed
}
c.factory.mu.Lock()
defer c.factory.mu.Unlock()
n := c.factory.nodeByPath(c.cell, dirPath)
if n == nil {
return topo.NewError(topo.NoNode, dirPath)
}
if n.lock == nil {
return fmt.Errorf("node %v is not locked", dirPath)
}
close(n.lock)
n.lock = nil
n.lockContents = ""
return nil
}
View on GitHub (pinned to 01a25a7d17)
Solutions
- Ensure Unlock is called exactly once per successful Lock (guard with a sync.Once or flag)
- Check that the same topo.Server/factory instance is used for Lock and Unlock
- Inspect code paths that defer Unlock after an already-executed explicit Unlock
- In tests, avoid recreating the memorytopo factory between Lock and Unlock
Example fix
// before
unlock := func() { ts.Unlock(ctx, l, "action") }
defer unlock()
if err := doWork(); err != nil {
unlock() // double unlock
return err
}
// after
unlock := sync.OnceFunc(func() { ts.Unlock(ctx, l, "action") })
defer unlock()
if err := doWork(); err != nil {
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
// ensure single unlock: guard before calling
donce := &sync.Once{}
release := func() { donce.Do(func() { ts.Unlock(ctx, lockDescriptor, "action") }) } Type guard
func isNotLockedErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "is not locked")
} Try / catch
if err := ts.Unlock(ctx, lockDescriptor, "action"); err != nil {
if strings.Contains(err.Error(), "is not locked") {
log.Warn("lock already released, ignoring")
} else {
return err
}
} Prevention
- Call Unlock exactly once per successful Lock (defer + sync.Once)
- Do not recreate or reset the topo server between Lock and Unlock
- Never ignore the Lock error and still call Unlock afterwards
When it happens
Trigger: Calling topo.Lock/Unlock (backed by memorytopo) when: Unlock is called twice for the same lock; the node was recreated (factory reset / nodeByPath returned a fresh node) so n.lock is nil; the lock was already closed by a concurrent Unlock.
Common situations: Double-defer Unlock in error handling paths; topo server restarted or memorytopo factory reset between Lock and Unlock (common in tests); LockDirectoryContext expired and cleaned up, then Unlock called anyway.
Related errors
- delete(%v, %v) failed: it's a directory
- cannot watch directory %v in cell %v
- obtainQueueLock: empty queue node: %v
- obtainQueueLock: no previous queue node found: %v
- lock already acquired
AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01).
Data as JSON: /api/errors/c30da4f9026edffe.
Report an issue: GitHub.