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, nil

View on GitHub (pinned to c9def3e214)

Solutions

  1. Check the embedded lock info (the error includes getLockInfo output) to see who holds it; coordinate or wait.
  2. If stale, run terraform force-unlock <LOCK_ID> then retry.
  3. Grant tablestore:PutRow; ensure the OTS table exists.
  4. 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

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


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/d134d5c7678a5a06. Report an issue: GitHub.