hashicorp/nomad · error

variable doesn't hold a lock

Error message

variable doesn't hold a lock

What it means

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.

Source

Thrown at nomad/state/state_store_variables.go:18

// Copyright IBM Corp. 2015, 2026
// SPDX-License-Identifier: BUSL-1.1

package state

import (
	"errors"
	"fmt"
	"math"

	"github.com/hashicorp/go-memdb"
	"github.com/hashicorp/nomad/nomad/structs"
)

var (
	errVarAlreadyLocked = errors.New("variable already holds a lock")
	errVarNotFound      = errors.New("variable doesn't exist")
	errLockNotFound     = errors.New("variable doesn't hold a lock")
)

// Variables queries all the variables and is used only for
// snapshot/restore and key rotation
func (s *StateStore) Variables(ws memdb.WatchSet) (memdb.ResultIterator, error) {
	txn := s.db.ReadTxn()

	iter, err := txn.Get(TableVariables, indexID)
	if err != nil {
		return nil, err
	}

	ws.Add(iter.WatchCh())
	return iter, nil
}

// GetVariablesByNamespace returns an iterator that contains all
// variables belonging to the provided namespace.

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Check that the variable holds a lock before issuing a release/renew operation
  2. Re-fetch the variable via the Variables API to confirm its current lock state
  3. If the lock was already released, treat this as idempotent success in the caller
  4. Ensure the lock was created via VarLock before attempting lock lifecycle operations

Example fix

// before
store.VarLockRelease(1000, path, lockID) // panics/errors if no lock
// after
sv, _ := store.VariablesByID(nil, path)
if sv != nil && sv.Lock != nil && sv.Lock.ID == lockID {
    store.VarLockRelease(1000, path, lockID)
}
Defensive patterns

Strategy: validation

Validate before calling

func canReleaseOrRenew(sv *structs.Variable, lockID string) bool {
    return sv != nil && sv.Lock != nil && sv.Lock.ID != "" && sv.Lock.ID == lockID
}
if !canReleaseOrRenew(sv, myLockID) { /* skip or refetch variable */ }

Type guard

func holdsLock(sv *structs.Variable) bool {
    return sv != nil && sv.Lock != nil && sv.Lock.ID != ""
}

Try / catch

if err := store.VarLockRelease(idx, path, lockID); errors.Is(err, errLockNotFound) {
    // lock already gone; treat as idempotent success or refetch
    return nil
}

Prevention

When it happens

Trigger: 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 == "".

Common situations: 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.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/38b654d7f17a0dd2. Report an issue: GitHub.