{"record":{"id":"b35eb2e6baeff5a8","repo":"hashicorp/terraform","slug":"invalid-lock-id-q-current-id-q","errorCode":null,"errorMessage":"invalid lock id: %q. current id: %q","messagePattern":"invalid lock id: %q\\. current id: %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/command/clistate/local_state.go","lineNumber":218,"sourceCode":"\t\t}\n\n\t\treturn \"\", lockErr\n\t}\n\n\ts.lockID = info.ID\n\treturn s.lockID, s.writeLockInfo(info)\n}\n\nfunc (s *LocalState) Unlock(id string) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif s.lockID == \"\" {\n\t\treturn fmt.Errorf(\"LocalState not locked\")\n\t}\n\n\tif id != s.lockID {\n\t\tidErr := fmt.Errorf(\"invalid lock id: %q. current id: %q\", id, s.lockID)\n\t\tinfo, err := s.lockInfo()\n\t\tif err != nil {\n\t\t\tidErr = errors.Join(idErr, err)\n\t\t}\n\n\t\treturn &statemgr.LockError{\n\t\t\tErr:  idErr,\n\t\t\tInfo: info,\n\t\t}\n\t}\n\n\tos.Remove(s.lockInfoPath())\n\n\tfileName := s.stateFileOut.Name()\n\n\tunlockErr := s.unlock()\n\n\ts.stateFileOut.Close()","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/command/clistate/local_state.go#L200-L236","documentation":"Thrown by LocalState.Unlock when the caller-supplied lock ID does not match the ID currently held by this LocalState (s.lockID). Terraform uses the ID to ensure only the lock owner can release it — preventing one operation from unlocking another's lock. On mismatch the lock is left intact and the current holder's ID is reported so the operator can reconcile.","triggerScenarios":"Passing the wrong ID to Unlock (stale, from a different run, or a different state); concurrent processes each holding their own lock and confusing IDs; an orchestrator that generated/forwarded the wrong ID.","commonSituations":"Two Terraform processes targeting the same local state; CI retried a job and the second attempt tries to unlock with the first attempt's ID; a wrapper recorded the ID incorrectly.","solutions":["Use the exact lock ID returned by the Lock call that succeeded — do not infer or hard-code.","If the lock is genuinely stale from a dead process, run `terraform force-unlock <id>` with the reported current id.","Serialize concurrent runs against the same local state so IDs do not collide.","Log the lock ID at acquisition so the matching Unlock can be verified."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if id != heldID {\n    return fmt.Errorf(\"refusing to unlock: provided %q != held %q\", id, heldID)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Thread the exact returned lock ID to the matching Unlock.","For stale on-disk locks use `terraform force-unlock <id>` rather than Unlock.","Serialize concurrent Terraform runs against the same local state."],"tags":["state","locking","local-backend","concurrency"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}