{"record":{"id":"38b654d7f17a0dd2","repo":"hashicorp/nomad","slug":"variable-doesn-t-hold-a-lock","errorCode":null,"errorMessage":"variable doesn't hold a lock","messagePattern":"variable doesn't hold a lock","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"nomad/state/state_store_variables.go","lineNumber":18,"sourceCode":"// Copyright IBM Corp. 2015, 2026\n// SPDX-License-Identifier: BUSL-1.1\n\npackage state\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\n\t\"github.com/hashicorp/go-memdb\"\n\t\"github.com/hashicorp/nomad/nomad/structs\"\n)\n\nvar (\n\terrVarAlreadyLocked = errors.New(\"variable already holds a lock\")\n\terrVarNotFound      = errors.New(\"variable doesn't exist\")\n\terrLockNotFound     = errors.New(\"variable doesn't hold a lock\")\n)\n\n// Variables queries all the variables and is used only for\n// snapshot/restore and key rotation\nfunc (s *StateStore) Variables(ws memdb.WatchSet) (memdb.ResultIterator, error) {\n\ttxn := s.db.ReadTxn()\n\n\titer, err := txn.Get(TableVariables, indexID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tws.Add(iter.WatchCh())\n\treturn iter, nil\n}\n\n// GetVariablesByNamespace returns an iterator that contains all\n// variables belonging to the provided namespace.","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/nomad/state/state_store_variables.go#L1-L36","documentation":"errLockNotFound is a sentinel error in Nomad's state store for Variables. It is returned when a variable update operation (such as a lock release or lock renewal) references a variable whose Lock field is nil or whose Lock.ID is empty — i.e. the variable exists but does not currently hold a lock, so there is nothing to release or renew.","triggerScenarios":"Calling VarLockRelease or RenewLock (or the matching RPCs against the state store) on a variable that has no lock attached; VarLock/Delete ops in a state-store apply where sv.Lock == nil || sv.Lock.ID == \"\".","commonSituations":"A client lost the lock (it was released or force-cleared by another process) and later tries to release/renew it again using stale session data; double-release after a failed write; tests simulating a variable at path without a lock.","solutions":["Check that the variable holds a lock before issuing a release/renew operation","Re-fetch the variable via the Variables API to confirm its current lock state","If the lock was already released, treat this as idempotent success in the caller","Ensure the lock was created via VarLock before attempting lock lifecycle operations"],"exampleFix":"// before\nstore.VarLockRelease(1000, path, lockID) // panics/errors if no lock\n// after\nsv, _ := store.VariablesByID(nil, path)\nif sv != nil && sv.Lock != nil && sv.Lock.ID == lockID {\n    store.VarLockRelease(1000, path, lockID)\n}","handlingStrategy":"validation","validationCode":"func canReleaseOrRenew(sv *structs.Variable, lockID string) bool {\n    return sv != nil && sv.Lock != nil && sv.Lock.ID != \"\" && sv.Lock.ID == lockID\n}\nif !canReleaseOrRenew(sv, myLockID) { /* skip or refetch variable */ }","typeGuard":"func holdsLock(sv *structs.Variable) bool {\n    return sv != nil && sv.Lock != nil && sv.Lock.ID != \"\"\n}","tryCatchPattern":"if err := store.VarLockRelease(idx, path, lockID); errors.Is(err, errLockNotFound) {\n    // lock already gone; treat as idempotent success or refetch\n    return nil\n}","preventionTips":["Always create the lock via VarLock before releasing/renewing","Refetch the variable to check Lock state before lock operations","Treat lock release as idempotent in callers","Watch for concurrent releases that can race your operation"],"tags":["nomad","state-store","variables","locking"],"backgroundTag":"lock-not-held","analyzedSha":"482b49bf1aec006f089bcfc7e632d8f6ac303e5e","analyzedAt":"2026-09-04T07:54:14.808Z","contentChangedAt":"2026-09-04T07:54:14.808Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}