{"record":{"id":"9c091f85eff90b63","repo":"hashicorp/terraform","slug":"state-q-already-locked","errorCode":null,"errorMessage":"state %q already locked","messagePattern":"state %q already locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/consul/client.go","lineNumber":382,"sourceCode":"\tdefer c.mu.Unlock()\n\n\tif !c.lockState {\n\t\treturn \"\", nil\n\t}\n\n\tc.info = info\n\n\t// These checks only are to ensure we strictly follow the specification.\n\t// Terraform shouldn't ever re-lock, so provide errors for the 2 possible\n\t// states if this is called.\n\tselect {\n\tcase <-c.lockCh:\n\t\t// We had a lock, but lost it.\n\t\treturn \"\", errors.New(\"lost consul lock, cannot re-lock\")\n\tdefault:\n\t\tif c.lockCh != nil {\n\t\t\t// we have an active lock already\n\t\t\treturn \"\", fmt.Errorf(\"state %q already locked\", c.Path)\n\t\t}\n\t}\n\n\treturn c.lock()\n}\n\n// the lock implementation.\n// Only to be called while holding Client.mu\nfunc (c *RemoteClient) lock() (string, error) {\n\t// We create a new session here, so it can be canceled when the lock is\n\t// lost or unlocked.\n\tlockSession, err := c.createSession()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// store the session ID for correlation with consul logs\n\tc.info.Info = \"consul session: \" + lockSession","sourceCodeStart":364,"sourceCodeEnd":400,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/consul/client.go#L364-L400","documentation":"RemoteClient.Lock guards against being called twice on the same client. c.lockCh tracks the in-flight lock: if it is non-nil and the channel has not been drained (default branch of select with lockCh != nil), a lock is already held and Terraform is attempting to re-lock, which violates the locking specification.","triggerScenarios":"RemoteClient.Lock(info) invoked when c.lockCh != nil and <-c.lockCh does not return (default branch taken).","commonSituations":"An internal Terraform bug double-locks the same RemoteClient; a custom caller wrapping the backend invokes Lock directly while Terraform also holds the lock; the client instance was reused across operations. Genuine hits from stock Terraform should be reported as bugs.","solutions":["Report as a Terraform bug with the stack trace, backend configuration, and Terraform version.","If you wrap the backend with custom code, ensure you are not invoking Lock twice on the same RemoteClient.","Restart the Terraform process; a fresh client has lockCh == nil."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// isHoldingLock reports whether this RemoteClient already holds a lock.\n// (Internal use; stock Terraform should never double-lock.)\nfunc isHoldingLock(c *RemoteClient) bool {\n    c.mu.Lock()\n    defer c.mu.Unlock()\n    return c.lockCh != nil\n}","tryCatchPattern":null,"preventionTips":["Do not reuse a single RemoteClient across overlapping operations.","If you wrap the backend, never call Lock/Unlock yourself; let Terraform drive it.","Report any genuine hit as a Terraform bug with a goroutine dump.","Keep your Terraform build current; locking invariants are tightened over time."],"tags":["consul","backend","state-locking","defensive"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}