{"record":{"id":"01f20e2a479f36fe","repo":"hashicorp/terraform","slug":"getting-lock-info-got-an-error-v","errorCode":null,"errorMessage":"\ngetting lock info got an error: %#v","messagePattern":"\ngetting lock info got an error: %#v","errorType":"exception","errorClass":"statemgr.LockError","httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/oss/client.go","lineNumber":198,"sourceCode":"\t\t\t\tColumnName: \"Info\",\n\t\t\t\tValue:      string(info.Marshal()),\n\t\t\t},\n\t\t},\n\t\tCondition: &tablestore.RowCondition{\n\t\t\tRowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_NOT_EXIST,\n\t\t},\n\t}\n\n\tlog.Printf(\"[DEBUG] Recording state lock in tablestore: %#v; LOCKID:%s\", putParams, c.lockPath())\n\n\t_, err := c.otsClient.PutRow(&tablestore.PutRowRequest{\n\t\tPutRowChange: putParams,\n\t})\n\tif err != nil {\n\t\terr = fmt.Errorf(\"invoking PutRow got an error: %#v\", err)\n\t\tlockInfo, infoErr := c.getLockInfo()\n\t\tif infoErr != nil {\n\t\t\terr = errors.Join(err, fmt.Errorf(\"\\ngetting lock info got an error: %#v\", infoErr))\n\t\t}\n\t\tlockErr := &statemgr.LockError{\n\t\t\tErr:  err,\n\t\t\tInfo: lockInfo,\n\t\t}\n\t\tlog.Printf(\"[ERROR] state lock error: %s\", lockErr.Error())\n\t\treturn \"\", lockErr\n\t}\n\n\treturn info.ID, nil\n}\n\nfunc (c *RemoteClient) getMD5() ([]byte, error) {\n\tif c.otsTable == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tgetParams := &tablestore.SingleRowQueryCriteria{","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/oss/client.go#L180-L216","documentation":"In RemoteClient.Lock() (client.go:196-199), after PutRow already failed, the follow-up getLockInfo() call also failed; the two errors are joined with errors.Join. The leading newline is intentional so the lock-info failure appears on a new line beneath the PutRow failure. It means the client cannot even report who holds the conflicting lock.","triggerScenarios":"PutRow failed (e.g. conditional conflict or permission error) AND the subsequent GetRow to read the existing Info column also failed (table missing, tablestore:GetRow denied, OTS outage). The user gets a compound error with no lock attribution.","commonSituations":"RAM policy grants PutRow but not GetRow; OTS table deleted between the two calls; OTS regional outage affecting both reads and writes; credentials expired mid-lock attempt.","solutions":["Grant the credentials both tablestore:PutRow and tablestore:GetRow on the table.","Confirm the OTS table exists at ots_endpoint.","Retry once OTS recovers; inspect the TableStore row manually to find the holder.","If the row is stale, force-unlock by deleting the LockID row directly in the OTS console."],"exampleFix":"// before: policy missing GetRow\n//   Action: \"tablestore:PutRow\"  only\n// after:\n//   Action: [\"tablestore:PutRow\", \"tablestore:GetRow\", \"tablestore:DeleteRow\"]","handlingStrategy":"fallback","validationCode":"// Ensure GetRow is permitted alongside PutRow, and the table exists, before locking.\nfunc otsReadable(c *tablestore.TableStoreClient, table string) error {\n    _, err := c.DescribeTable(&tablestore.DescribeTableRequest{TableName: table})\n    return err\n}","typeGuard":null,"tryCatchPattern":"// If the compound error shows both PutRow + GetRow failures, fall back to\n// manual OTS row inspection in the console before retrying terraform.\nif strings.Contains(err.Error(), \"getting lock info got an error\") {\n    return manualOTSCleanup()\n}","preventionTips":["Grant the full lock lifecycle: tablestore:PutRow + GetRow + DeleteRow together.","Do not delete the OTS table while applies are in flight.","Monitor OTS health before locking-heavy operations."],"tags":["alibaba-cloud","tablestore","ots","remote-state","locking","permissions","go"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}