{"record":{"id":"1c4953d4456dde37","repo":"hashicorp/terraform","slug":"error-unlocking-postgres-state-s","errorCode":null,"errorMessage":"error unlocking Postgres state: %s","messagePattern":"error unlocking Postgres state: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/backend/remote-state/pg/backend_state.go","lineNumber":103,"sourceCode":"\t\t\tbreak\n\t\t}\n\t}\n\n\t// Grab a lock, we use this to write an empty state if one doesn't\n\t// exist already. We have to write an empty state as a sentinel value\n\t// so Workspaces() knows it exists.\n\tif !exists {\n\t\tlockInfo := statemgr.NewLockInfo()\n\t\tlockInfo.Operation = \"init\"\n\t\tlockId, err := stateMgr.Lock(lockInfo)\n\t\tif err != nil {\n\t\t\treturn nil, diags.Append(fmt.Errorf(\"failed to lock state in Postgres: %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(`error unlocking Postgres state: %s`, err)\n\t\t\t}\n\t\t\treturn parent\n\t\t}\n\n\t\tif v := stateMgr.State(); v == nil {\n\t\t\tif err := stateMgr.WriteState(states.NewState()); err != nil {\n\t\t\t\terr = lockUnlock(err)\n\t\t\t\treturn nil, diags.Append(err)\n\t\t\t}\n\t\t\tif err := stateMgr.PersistState(nil); err != nil {\n\t\t\t\terr = lockUnlock(err)\n\t\t\t\treturn nil, diags.Append(err)\n\t\t\t}\n\t\t}\n\n\t\t// Unlock, the state should now be initialized\n\t\tif err := lockUnlock(nil); err != nil {\n\t\t\treturn nil, diags.Append(err)","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/pg/backend_state.go#L85-L121","documentation":"Thrown by the lockUnlock cleanup helper in pg/backend_state.go:103 when stateMgr.Unlock(lockId) fails during the first-time-init path. It surfaces after the primary operation already hit an error (or completed) and the compensating release of the Postgres advisory lock itself failed. Because advisory locks are session-bound, this commonly means the DB session was lost between acquiring and releasing.","triggerScenarios":"The Postgres connection dropped between Lock and Unlock; pg_advisory_unlock query errored; a pooler (e.g. PgBouncer in transaction mode) recycled the session so the lock is no longer held by this connection; the lock was already released out-of-band.","commonSituations":"Network blip to Postgres mid-init; PgBouncer resetting the backend; long init interrupted by a timeout; another process force-unlocked the advisory lock concurrently.","solutions":["Query pg_locks for the workspace lock id (or -1 creation lock) to confirm no stale advisory lock remains.","If a stale lock exists, pg_terminate_backend() the holding pid.","Retry `terraform init`; the failed partial state is cleaned up by re-running.","Check DB/pooler connectivity and idle timeout settings."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Confirm the advisory lock the init path will use is free before starting\n// SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND objid IN (-1, <wsid>);","typeGuard":null,"tryCatchPattern":"// Treat unlock-cleanup failures as warnings; the primary op may still be salvageable\n// if unlockErr := stateMgr.Unlock(lockID); unlockErr != nil {\n//   log.Warn(\"unlock failed; may need manual advisory lock release\", \"err\", unlockErr)\n// }","preventionTips":["Avoid PgBouncer transaction-mode pooling for advisory-lock sessions.","Set DB idle/statement timeouts longer than the longest apply.","Document the pg_locks query operators use to find stale locks."],"tags":["postgres","locking","cleanup","remote-state"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}