{"record":{"id":"8201714c27600b3f","repo":"hashicorp/terraform","slug":"failed-to-lock-oci-state-s","errorCode":null,"errorMessage":"failed to lock oci state: %s","messagePattern":"failed to lock oci state: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/oci/backend_state.go","lineNumber":74,"sourceCode":"\t\treturn nil, diags\n\t}\n\n\texists := false\n\tfor _, s := range existing {\n\t\tif s == name {\n\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// We need to create the object so it's listed by States.\n\tif !exists {\n\t\t// take a lock on this state while we write it\n\t\tlockInfo := statemgr.NewLockInfo()\n\t\tlockInfo.Operation = \"init\"\n\t\tlockId, err := b.client.Lock(lockInfo)\n\t\tif err != nil {\n\t\t\treturn nil, diags.Append(fmt.Errorf(\"failed to lock oci state: %s\", err))\n\t\t}\n\n\t\t// Local helper function so we can call it multiple places\n\t\tlockUnlock := func(parent error) error {\n\t\t\tif err := stateMgr.Unlock(lockId); err != nil {\n\t\t\t\treturn fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)\n\t\t\t}\n\t\t\treturn parent\n\t\t}\n\n\t\t// Grab the value\n\t\t// This is to ensure that no one beat us to writing a state between\n\t\t// the `exists` check and taking the lock.\n\t\tif err := stateMgr.RefreshState(); err != nil {\n\t\t\terr = lockUnlock(err)\n\t\t\treturn nil, diags.Append(err)\n\t\t}\n","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/oci/backend_state.go#L56-L92","documentation":"Thrown by StateMgr when b.client.Lock fails during initial state creation for a workspace that does not yet exist. The OCI lock is implemented as a PutObject on the lock file with IfNoneMatch:\"*\" (client.go:275), so this fails whenever a lock-file object already exists at the computed lockFilePath. The error wraps the underlying lock failure, which is usually a statemgr.LockError carrying the Info of the current holder.","triggerScenarios":"Two concurrent `terraform init` runs against the same not-yet-created workspace; a previous process crashed holding the lock; a stale .tflock object remains in the bucket; running init while another user runs apply on the same workspace.","commonSituations":"CI pipelines racing on a shared workspace; a killed/OOM-killed run leaving the lock object behind; manual bucket inspection showing an orphaned `<workspace>.tflock` file; switching a workspace from another backend leaving stale locks.","solutions":["Wait for the in-progress operation to finish, then retry init — the lock will be released normally.","Confirm no operation is actually running, then run `terraform force-unlock <LOCK_ID>` using the ID from the wrapped LockError.Info.ID.","If force-unlock cannot fetch the lock (see errors 310-313), delete the lock object directly in the OCI bucket at path <workspaceKeyPrefix>/<name>.tflock.","Serialize init/apply on shared workspaces with an external mutex (CI lock, OCI lock via a separate object)."],"exampleFix":"// before: two CI jobs run `terraform init` concurrently on the same workspace\n// after: gate the init with an external lock so only one job initializes at a time\n//   - lock: \"tf-init-$WORKSPACE\"\n//     run: terraform init && terraform apply -auto-approve\n// recovery for an orphaned lock:\n//   terraform force-unlock <LOCK_ID_FROM_ERROR>","handlingStrategy":"retry","validationCode":"// Before acquiring the lock, check whether a lock object already exists.\nfunc lockExists(c *RemoteClient, ctx context.Context) (bool, error) {\n    _, err := c.objectStorageClient.HeadObject(ctx, objectstorage.HeadObjectRequest{\n        NamespaceName: common.String(c.namespace),\n        BucketName:    common.String(c.bucketName),\n        ObjectName:    common.String(c.lockFilePath),\n    })\n    if err == nil { return true, nil }\n    var se common.ServiceError\n    if errors.As(err, &se) && se.GetHTTPStatusCode() == 404 { return false, nil }\n    return false, err\n}","typeGuard":"// Distinguish a lock contention from other failures\nfunc isLockBusy(err error) bool {\n    var le *statemgr.LockError\n    return errors.As(err, &le)\n}","tryCatchPattern":"lockId, err := b.client.Lock(lockInfo)\nif err != nil {\n    var le *statemgr.LockError\n    if errors.As(err, &le) && le.Info != nil {\n        // surface the holder's ID so the user can decide/force-unlock\n        return fmt.Errorf(\"locked by %s (op=%s) since %s; force-unlock %s\",\n            le.Info.ID, le.Info.Operation, le.Info.Created, le.Info.ID)\n    }\n    return err\n}","preventionTips":["Serialize init/apply on shared workspaces with an external mutex (CI lock, separate OCI object).","Educate operators to use `terraform force-unlock <ID>` rather than deleting resources manually.","Monitor for orphaned .tflock objects and alert on lock files older than N hours.","Confirm no other run is active before force-unlocking."],"tags":["oci","state-locking","concurrency","terraform","object-storage"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}