hashicorp/terraform · error · statemgr.LockError
invoking PutRow got an error: %#v
Error message
invoking PutRow got an error: %#v
What it means
In RemoteClient.Lock() (client.go:191-195), otsClient.PutRow failed. The PutRow uses RowExistenceExpectation_EXPECT_NOT_EXIST (line 185), so the most common cause is that a lock row already exists (conditional insert rejected) - i.e. the state is already locked. %#v includes the OTS error code. Lock() then enriches by fetching existing lock info and wraps it in a statemgr.LockError.
Source
Thrown at internal/backend/remote-state/oss/client.go:195
},
Columns: []tablestore.AttributeColumn{
{
ColumnName: "Info",
Value: string(info.Marshal()),
},
},
Condition: &tablestore.RowCondition{
RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_NOT_EXIST,
},
}
log.Printf("[DEBUG] Recording state lock in tablestore: %#v; LOCKID:%s", putParams, c.lockPath())
_, err := c.otsClient.PutRow(&tablestore.PutRowRequest{
PutRowChange: putParams,
})
if err != nil {
err = fmt.Errorf("invoking PutRow got an error: %#v", err)
lockInfo, infoErr := c.getLockInfo()
if infoErr != nil {
err = errors.Join(err, fmt.Errorf("\ngetting lock info got an error: %#v", infoErr))
}
lockErr := &statemgr.LockError{
Err: err,
Info: lockInfo,
}
log.Printf("[ERROR] state lock error: %s", lockErr.Error())
return "", lockErr
}
return info.ID, nil
}
func (c *RemoteClient) getMD5() ([]byte, error) {
if c.otsTable == "" {
return nil, nilView on GitHub (pinned to c9def3e214)
Solutions
- Check the embedded lock info (the error includes getLockInfo output) to see who holds it; coordinate or wait.
- If stale, run terraform force-unlock <LOCK_ID> then retry.
- Grant tablestore:PutRow; ensure the OTS table exists.
- Serialize CI runs per workspace to avoid contention.
Example fix
# before: two runners lock same workspace -> PutRow conditional fail # fix: clear stale lock terraform force-unlock 9f0e... terraform apply
Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-check the lock row; if present, do not even attempt Lock - surface it.
func lockPresent(c *tablestore.TableStoreClient, table, path string) bool {
resp, err := c.GetRow(&tablestore.GetRowRequest{SingleRowQueryCriteria: &tablestore.SingleRowQueryCriteria{
TableName: table, PrimaryKey: pk(path), MaxVersion: 1,
}})
return err == nil && len(resp.GetColumnMap().Columns["Info"]) > 0
} Try / catch
// On LockError, extract the held lock info and either wait or force-unlock.
if le, ok := err.(*statemgr.LockError); ok {
if le.Info != nil {
fmt.Printf("held by %s since %s; force-unlock %s\n", le.Info.Who, le.Info.Created, le.Info.ID)
}
} Prevention
- One writer per workspace at a time; queue CI jobs.
- Always release locks (clean exit) or force-unlock on failure.
- Confirm tablestore:PutRow permission.
When it happens
Trigger: Conditional PutRow into the OTS table fails: OTSConditionCheckFail (row exists -> already locked), throttling, AccessDenied on tablestore:PutRow, or table missing. Because the condition is EXPECT_NOT_EXIST, a pre-existing lock row reliably produces this.
Common situations: Concurrent terraform apply/plan on the same workspace; a previous run crashed holding the lock; CI parallelism on one workspace; OTS table deleted; insufficient PutRow permissions.
Related errors
- error describing table store %s: %#v
- failed to lock OSS state: %s
- Error unlocking Alibaba Cloud OSS state file: Lock ID: %s E
- getting lock info got an error: %#v
- failed to retrieve lock info: %s
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/d134d5c7678a5a06.
Report an issue: GitHub.