golang-migrate/migrate · warning
unable to release already released lock
Error message
unable to release already released lock
What it means
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.
Source
Thrown at database/spanner/spanner.go:41
"google.golang.org/api/iterator"
)
func init() {
db := Spanner{}
database.Register("spanner", &db)
}
// DefaultMigrationsTable is used if no custom table is specified
const DefaultMigrationsTable = "SchemaMigrations"
// Driver errors
var (
ErrNilConfig = errors.New("no config")
ErrNoDatabaseName = errors.New("no database name")
ErrNoSchema = errors.New("no schema")
ErrDatabaseDirty = errors.New("database is dirty")
ErrLockHeld = errors.New("unable to obtain lock")
ErrLockNotHeld = errors.New("unable to release already released lock")
)
// Config used for a Spanner instance
type Config struct {
MigrationsTable string
DatabaseName string
// Whether to parse the migration DDL with spansql before
// running them towards Spanner.
// Parsing outputs clean DDL statements such as reformatted
// and void of comments.
CleanStatements bool
}
// Spanner implements database.Driver for Google Cloud Spanner
type Spanner struct {
db *DB
config *ConfigView on GitHub (pinned to 01a9643f14)
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.
Example fix
// before
driver.Unlock() // called even when Lock failed or already released
// after
if locked {
if err := driver.Unlock(); err != nil && !errors.Is(err, spanner.ErrLockNotHeld) {
log.Printf("unlock failed: %v", err)
}
} Defensive patterns
Strategy: try-catch
Validate before calling
if !lockAcquired {
return fmt.Errorf("cannot unlock: lock was never acquired")
} Try / catch
if err := driver.Unlock(); err != nil && !errors.Is(err, spanner.ErrLockNotHeld) {
return fmt.Errorf("releasing migration lock: %w", err)
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02).
Data as JSON: /api/errors/9f2080df5cd982c4.
Report an issue: GitHub.