{"record":{"id":"c30da4f9026edffe","repo":"vitessio/vitess","slug":"node-v-is-not-locked","errorCode":null,"errorMessage":"node %v is not locked","messagePattern":"node (.+?) is not locked","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"go/vt/topo/memorytopo/lock.go","lineNumber":172,"sourceCode":"// Unlock is part of the topo.LockDescriptor interface.\nfunc (ld *memoryTopoLockDescriptor) Unlock(ctx context.Context) error {\n\treturn ld.c.unlock(ctx, ld.dirPath)\n}\n\nfunc (c *Conn) unlock(ctx context.Context, dirPath string) error {\n\tif c.closed.Load() {\n\t\treturn ErrConnectionClosed\n\t}\n\n\tc.factory.mu.Lock()\n\tdefer c.factory.mu.Unlock()\n\n\tn := c.factory.nodeByPath(c.cell, dirPath)\n\tif n == nil {\n\t\treturn topo.NewError(topo.NoNode, dirPath)\n\t}\n\tif n.lock == nil {\n\t\treturn fmt.Errorf(\"node %v is not locked\", dirPath)\n\t}\n\tclose(n.lock)\n\tn.lock = nil\n\tn.lockContents = \"\"\n\treturn nil\n}\n","sourceCodeStart":154,"sourceCodeEnd":179,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/topo/memorytopo/lock.go#L154-L179","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nunlock := func() { ts.Unlock(ctx, l, \"action\") }\ndefer unlock()\nif err := doWork(); err != nil {\n    unlock() // double unlock\n    return err\n}\n// after\nunlock := sync.OnceFunc(func() { ts.Unlock(ctx, l, \"action\") })\ndefer unlock()\nif err := doWork(); err != nil {\n    return err\n}","handlingStrategy":"try-catch","validationCode":"// ensure single unlock: guard before calling\ndonce := &sync.Once{}\nrelease := func() { donce.Do(func() { ts.Unlock(ctx, lockDescriptor, \"action\") }) }","typeGuard":"func isNotLockedErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"is not locked\")\n}","tryCatchPattern":"if err := ts.Unlock(ctx, lockDescriptor, \"action\"); err != nil {\n    if strings.Contains(err.Error(), \"is not locked\") {\n        log.Warn(\"lock already released, ignoring\")\n    } else {\n        return err\n    }\n}","preventionTips":["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"],"tags":["topo","memorytopo","locking","unlock"],"backgroundTag":"topo-lock-not-held","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}