{"record":{"id":"c437fa2437bdf9f4","repo":"hashicorp/nomad","slug":"acquire-conflict-w","errorCode":null,"errorMessage":"acquire conflict %w","messagePattern":"acquire conflict %w","errorType":"exception","errorClass":"ErrLockConflict","httpStatus":409,"severity":"warning","filePath":"api/locks.go","lineNumber":115,"sourceCode":"\n//\tAcquire will make the actual call to acquire the lock over the variable using\n//\tthe ttl in the Locks to create the VariableLock. It will return the\n//\tpath of the variable holding the lock.\n//\n// Acquire returns the path to the variable holding the lock.\nfunc (l *Locks) Acquire(ctx context.Context) (string, error) {\n\n\tvar out Variable\n\n\t_, err := l.c.retryPut(ctx, \"/v1/var/\"+l.variable.Path+\"?lock-acquire\", l.variable, &out, &l.WriteOptions)\n\tif err != nil {\n\t\tcallErr, ok := err.(UnexpectedResponseError)\n\n\t\t// http.StatusConflict means the lock is already held. This will happen\n\t\t// under the normal execution if multiple instances are fighting for the same lock and\n\t\t// doesn't disrupt the flow.\n\t\tif ok && callErr.statusCode == http.StatusConflict {\n\t\t\treturn \"\", fmt.Errorf(\"acquire conflict %w\", ErrLockConflict)\n\t\t}\n\n\t\treturn \"\", err\n\t}\n\n\tl.variable.Lock = out.Lock\n\n\treturn l.variable.Path, nil\n}\n\n// Release makes the call to release the lock over a variable, even if the ttl\n// has not yet passed.\n// In case of a call to release a non held lock, Release returns ErrLockConflict.\nfunc (l *Locks) Release(ctx context.Context) error {\n\tvar out Variable\n\n\trv := &Variable{\n\t\tLock: &VariableLock{","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/hashicorp/nomad/blob/482b49bf1aec006f089bcfc7e632d8f6ac303e5e/api/locks.go#L97-L133","documentation":"Lock.Acquire wraps ErrLockConflict when the variables/lock acquire HTTP call returns 409 Conflict, producing 'acquire conflict %w'. This is expected behavior when multiple instances contend for the same lock — the caller should interpret the wrapped ErrLockConflict via errors.Is rather than string matching.","triggerScenarios":"Two or more processes call api.Lock.Acquire for the same lock variable concurrently; the server already has a holder, so it replies HTTP 409.","commonSituations":"Highly available deployments racing at startup for leadership; lock TTL expired and re-acquired by another instance; retry loops without jitter hammering the same lock.","solutions":["Detect the conflict with errors.Is(err, api.ErrLockConflict) and implement retry-with-backoff or exit gracefully","Randomize/jitter acquisition attempts to reduce contention","Check the current lock holder (nomad var inspect) if the conflict seems stale","Ensure the lock is released on shutdown so holders don't linger"],"exampleFix":"// before\nlockID, err := l.Acquire(ctx)\nif err != nil { return err }\n// after\nlockID, err := l.Acquire(ctx)\nif errors.Is(err, api.ErrLockConflict) {\n    time.Sleep(jitter(2 * time.Second))\n    return retryAcquire(ctx)\n} else if err != nil {\n    return err\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isLockConflict(err error) bool {\n    return errors.Is(err, api.ErrLockConflict)\n}","tryCatchPattern":"lockID, err := l.Acquire(ctx)\nswitch {\ncase errors.Is(err, api.ErrLockConflict):\n    return retryWithBackoff(ctx)\ncase err != nil:\n    return err\ndefault:\n    return useLock(ctx, lockID)\n}","preventionTips":["Always compare with errors.Is(err, api.ErrLockConflict), never string matching","Add jittered backoff around acquisition retries","Release locks deterministically on process shutdown"],"tags":["locking","concurrency","conflict"],"backgroundTag":"lock-conflict","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"}