{"record":{"id":"cec5d8fab78d21b8","repo":"hashicorp/terraform","slug":"lock-id-q-does-not-match-existing-lock-cec5d8","errorCode":null,"errorMessage":"lock id %q does not match existing lock","messagePattern":"lock id %q does not match existing lock","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/oss/client.go","lineNumber":379,"sourceCode":"\t}\n\treturn lockInfo, nil\n}\nfunc (c *RemoteClient) Unlock(id string) error {\n\tif c.otsTable == \"\" {\n\t\treturn nil\n\t}\n\n\tlockErr := &statemgr.LockError{}\n\n\tlockInfo, err := c.getLockInfo()\n\tif err != nil {\n\t\tlockErr.Err = fmt.Errorf(\"failed to retrieve lock info: %s\", err)\n\t\treturn lockErr\n\t}\n\tlockErr.Info = lockInfo\n\n\tif lockInfo.ID != id {\n\t\tlockErr.Err = fmt.Errorf(\"lock id %q does not match existing lock\", id)\n\t\treturn lockErr\n\t}\n\tparams := &tablestore.DeleteRowRequest{\n\t\tDeleteRowChange: &tablestore.DeleteRowChange{\n\t\t\tTableName: c.otsTable,\n\t\t\tPrimaryKey: &tablestore.PrimaryKey{\n\t\t\t\tPrimaryKeys: []*tablestore.PrimaryKeyColumn{\n\t\t\t\t\t{\n\t\t\t\t\t\tColumnName: pkName,\n\t\t\t\t\t\tValue:      c.lockPath(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tCondition: &tablestore.RowCondition{\n\t\t\t\tRowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE,\n\t\t\t},\n\t\t},\n\t}","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/oss/client.go#L361-L397","documentation":"Thrown by RemoteClient.Unlock in the Alibaba Cloud OSS state backend when the lock ID supplied to unlock does not equal the ID stored in Tablestore (OTS) for this state. The backend refuses to delete the lock row because the caller is not the lock owner, preventing one process from clobbering another process's lock. The returned value is a *statemgr.LockError carrying the real (existing) lock info, so the caller can report who actually holds it.","triggerScenarios":"Calling Unlock(id) with an ID that differs from lockInfo.ID read via getLockInfo() from the OTS row at c.lockPath(). Happens when a stale lock ID from a previous run is reused, when force-unlock is attempted with a guessed/wrong ID, or when two writers race and one holds a different lock.","commonSituations":"A prior `tofu apply` crashed leaving a dangling lock; the operator copies the wrong lock ID from logs; CI reuses a cached lock ID; a teammate force-unlocked and re-locked between your read and unlock.","solutions":["Inspect the *statemgr.LockError.Info field returned - it holds the actual current LockInfo (ID, Who, Created, Operation); use that ID, not the one you passed.","Run `tofu force-unlock <correct-id>` with the ID printed by the failed command (the backend echoes the real ID).","If no real lock is active but a stale OTS row remains, delete the row at pkName=c.lockPath() from the tablestore table manually.","Ensure only one OpenTofu process operates on the workspace concurrently to avoid ID races."],"exampleFix":"// before\nerr := client.Unlock(staleID)\n// after\ninfo, _ := client.getLockInfo()\nerr := client.Unlock(info.ID)","handlingStrategy":"validation","validationCode":"// before unlocking, fetch the current lock info and compare IDs\ninfo, err := client.getLockInfo()\nif err != nil {\n    return err\n}\nif info.ID != id {\n    return fmt.Errorf(\"refusing to unlock: passed %q but stored lock is %q held by %s; use force-unlock %s\", id, info.ID, info.Who, info.ID)\n}","typeGuard":"func isLockIDMismatchErr(err error) bool {\n    var le *statemgr.LockError\n    return errors.As(err, &le) && le.Err != nil && strings.Contains(le.Err.Error(), \"does not match existing lock\")\n}","tryCatchPattern":"err := client.Unlock(id)\nvar le *statemgr.LockError\nif errors.As(err, &le) && le.Info != nil {\n    // surface the real holder instead of the wrong-id message\n    log.Printf(\"lock held by %s since %s; correct id %s\", le.Info.Who, le.Info.Created, le.Info.ID)\n}","preventionTips":["Always read the lock ID via getLockInfo() or the CLI output rather than hardcoding/caching it.","Pass *statemgr.LockError.Info up to the user so the correct ID is visible.","Never store lock IDs across runs - they are per-lock."],"tags":["oss","alibaba","state-lock","lock-mismatch","concurrency"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}