{"record":{"id":"9f2080df5cd982c4","repo":"golang-migrate/migrate","slug":"unable-to-release-already-released-lock","errorCode":null,"errorMessage":"unable to release already released lock","messagePattern":"unable to release already released lock","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"database/spanner/spanner.go","lineNumber":41,"sourceCode":"\t\"google.golang.org/api/iterator\"\n)\n\nfunc init() {\n\tdb := Spanner{}\n\tdatabase.Register(\"spanner\", &db)\n}\n\n// DefaultMigrationsTable is used if no custom table is specified\nconst DefaultMigrationsTable = \"SchemaMigrations\"\n\n// Driver errors\nvar (\n\tErrNilConfig      = errors.New(\"no config\")\n\tErrNoDatabaseName = errors.New(\"no database name\")\n\tErrNoSchema       = errors.New(\"no schema\")\n\tErrDatabaseDirty  = errors.New(\"database is dirty\")\n\tErrLockHeld       = errors.New(\"unable to obtain lock\")\n\tErrLockNotHeld    = errors.New(\"unable to release already released lock\")\n)\n\n// Config used for a Spanner instance\ntype Config struct {\n\tMigrationsTable string\n\tDatabaseName    string\n\t// Whether to parse the migration DDL with spansql before\n\t// running them towards Spanner.\n\t// Parsing outputs clean DDL statements such as reformatted\n\t// and void of comments.\n\tCleanStatements bool\n}\n\n// Spanner implements database.Driver for Google Cloud Spanner\ntype Spanner struct {\n\tdb *DB\n\n\tconfig *Config","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/golang-migrate/migrate/blob/01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a/database/spanner/spanner.go#L23-L59","documentation":"spanner.ErrLockNotHeld ('unable to release already released lock') is returned by the Spanner driver's Unlock method when the current process tries to release the migration lock but no longer holds it. This typically means the lock expired, was never acquired, or was released by another path, and it prevents releasing a lock owned by someone else.","triggerScenarios":"Calling driver.Unlock() without a successful prior Lock(); double-unlocking in defer + explicit code paths; lock expiry between acquire and release; another process force-released the lock.","commonSituations":"Error-handling code that unlocks on every exit path even when Lock failed, long migrations that outlasted the lock TTL, duplicated cleanup logic in wrappers.","solutions":["Only call Unlock after Lock returned nil, and guard it with a flag so it runs at most once.","Ignore ErrLockNotHeld on shutdown if the lock was already released (treat as benign with errors.Is).","For long migrations, keep the lock alive (refresh/re-acquire) or shorten the migration so the lock does not expire mid-run."],"exampleFix":"// before\ndriver.Unlock() // called even when Lock failed or already released\n// after\nif locked {\n    if err := driver.Unlock(); err != nil && !errors.Is(err, spanner.ErrLockNotHeld) {\n        log.Printf(\"unlock failed: %v\", err)\n    }\n}","handlingStrategy":"try-catch","validationCode":"if !lockAcquired {\n    return fmt.Errorf(\"cannot unlock: lock was never acquired\")\n}","typeGuard":null,"tryCatchPattern":"if err := driver.Unlock(); err != nil && !errors.Is(err, spanner.ErrLockNotHeld) {\n    return fmt.Errorf(\"releasing migration lock: %w\", err)\n}","preventionTips":["Track lock ownership with a boolean and unlock exactly once (guard defers).","Never call Unlock when Lock returned an error.","Treat ErrLockNotHeld as benign during shutdown but log it to catch lock-expiry issues.","Keep migrations shorter than the lock TTL, or refresh long-held locks."],"tags":["go","locking","migration","spanner","go-migrate"],"backgroundTag":"lock-not-held","analyzedSha":"01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a","analyzedAt":"2026-09-02T19:38:29.671Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}