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

  1. Ensure Unlock is called exactly once per successful Lock (guard with a sync.Once or flag)
  2. Check that the same topo.Server/factory instance is used for Lock and Unlock
  3. Inspect code paths that defer Unlock after an already-executed explicit Unlock
  4. 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

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


AI-assisted analysis of vitessio/vitess@01a25a7d17 (2026-09-01). Data as JSON: /api/errors/c30da4f9026edffe. Report an issue: GitHub.