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 *Config

View on GitHub (pinned to 01a9643f14)

Solutions

  1. Only call Unlock after Lock returned nil, and guard it with a flag so it runs at most once.
  2. Ignore ErrLockNotHeld on shutdown if the lock was already released (treat as benign with errors.Is).
  3. 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

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.