{"record":{"id":"95fde91f955a379b","repo":"hashicorp/terraform","slug":"writing-q-failed-v","errorCode":null,"errorMessage":"writing %q failed: %v","messagePattern":"writing %q failed: (.+?)","errorType":"exception","errorClass":"statemgr.LockError","httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/gcs/client.go","lineNumber":118,"sourceCode":"\t// we can't set the ID until the info is written\n\tinfo.Path = c.lockFileURL()\n\n\tinfoJson, err := json.Marshal(info)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tlockFile := c.lockFile()\n\tw := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx)\n\terr = func() error {\n\t\tif _, err := w.Write(infoJson); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn w.Close()\n\t}()\n\n\tif err != nil {\n\t\treturn \"\", c.lockError(fmt.Errorf(\"writing %q failed: %v\", c.lockFileURL(), err))\n\t}\n\n\tinfo.ID = strconv.FormatInt(w.Attrs().Generation, 10)\n\n\treturn info.ID, nil\n}\n\nfunc (c *remoteClient) Unlock(id string) error {\n\tctx := context.TODO()\n\n\tgen, err := strconv.ParseInt(id, 10, 64)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Lock ID should be numerical value, got '%s'\", id)\n\t}\n\n\tif err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(ctx); err != nil {\n\t\treturn c.lockError(err)\n\t}","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/gcs/client.go#L100-L136","documentation":"remoteClient.Lock() writes the .tflock object with a DoesNotExist precondition (so only one writer can win). If Write or Close on that conditional writer fails, the error is wrapped through lockError into a *statemgr.LockError. Most commonly this is the precondition failure meaning someone else already holds the lock; it also fires on permission/quota/network errors.","triggerScenarios":"'terraform apply' / init on a fresh workspace where the .tflock already exists (another apply/plan/init is running) → precondition 412; or SA lacks storage.objects.create on the prefix; or transient GCS error during the lock write.","commonSituations":"Concurrent CI jobs on the same workspace; a crashed prior run left the .tflock behind; SA missing create permission on the lock path; someone is running a long apply.","solutions":["If the wrapped error indicates precondition/412, another process holds the lock: wait for it to finish or 'terraform force-unlock <ID>' only if it's truly stale.","Confirm SA has storage.objects.create on the bucket (needed to create .tflock).","Check the existing lock: 'gsutil cat gs://<bucket>/<prefix>/<ws>.tflock' to see who holds it (LockInfo with Operation, Who, Created).","Prevent concurrency: serialize CI for a given workspace; use distinct workspaces for parallel pipelines."],"exampleFix":"# recover from stale lock\ngsutil cat gs://bucket/prefix/default.tflock   # inspect holder\nterraform force-unlock 1681234567890   # generation-based ID\nterraform apply","handlingStrategy":"try-catch","validationCode":"// Pre-check whether a lock exists\nctx := context.Background()\n_, err := client.Bucket(bucket).Object(lockPath).Attrs(ctx)\nif err == nil { return fmt.Errorf(\"workspace already locked; check .tflock\") }","typeGuard":null,"tryCatchPattern":"_, err := client.Lock(info)\nif le, ok := err.(*statemgr.LockError); ok {\n    // inspect le.Info to surface holder; offer force-unlock with the generation\n}","preventionTips":["Serialize CI per workspace to avoid lock contention.","Always surface lock holder info so operators can decide wait vs force-unlock.","Confirm SA has storage.objects.create for the lock path."],"tags":["gcs","gcp","state-lock","concurrency","iam","force-unlock"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}