{"record":{"id":"0371103f176df72b","repo":"golang-migrate/migrate","slug":"unable-to-obtain-lock","errorCode":null,"errorMessage":"unable to obtain lock","messagePattern":"unable to obtain lock","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"database/spanner/spanner.go","lineNumber":40,"sourceCode":"\tadminpb \"cloud.google.com/go/spanner/admin/database/apiv1/databasepb\"\n\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","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/golang-migrate/migrate/blob/01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a/database/spanner/spanner.go#L22-L58","documentation":"spanner.ErrLockHeld ('unable to obtain lock') is returned by the Spanner driver's Lock method when another process already holds the advisory migration lock for the database. The driver uses the lock so only one migrate instance mutates the schema at a time; failing to acquire it returns this sentinel.","triggerScenarios":"Two migrate processes (or two deploy jobs) running concurrently against the same Spanner database; a previous holder crashed without unlocking and its lock row/lease is still considered active.","commonSituations":"Parallel CI pipelines deploying the same service, blue/green deploys where both sides run migrations on startup, a stale lock left by a killed migration container.","solutions":["Ensure only one migration runner executes at a time (serialize deploys, use a job queue or leader election).","Wait and retry Lock until the current holder finishes (migrate's Lock retries internally; add backoff around your run).","If the holder is definitely dead, manually clear the lock state the driver persists, then retry."],"exampleFix":"// before\ndriver, _ := spanner.WithInstance(client, cfg) // Lock() fails if another runner holds it\n// after\nif err := database.SetMigrationLockTimeout(...); err != nil { ... }\n// or run migrations exclusively:\nif err := driver.Lock(); err != nil {\n    if errors.Is(err, spanner.ErrLockHeld) {\n        time.Sleep(10 * time.Second)\n        err = driver.Lock()\n    }\n}","handlingStrategy":"retry","validationCode":"if err := driver.Lock(); err != nil {\n    if errors.Is(err, spanner.ErrLockHeld) {\n        return fmt.Errorf(\"another migration is running; retry later\")\n    }\n    return err\n}","typeGuard":null,"tryCatchPattern":"err := retry.Do(func() error {\n    if lerr := driver.Lock(); lerr != nil {\n        if errors.Is(lerr, spanner.ErrLockHeld) {\n            return retry.RetryableError(lerr)\n        }\n        return retry.UnrecoverableError(lerr)\n    }\n    return nil\n}, retry.Attempts(5), retry.Delay(10*time.Second))","preventionTips":["Serialize deploys so only one migration runner targets a database at a time.","Set a bounded lock timeout/backoff instead of failing the deploy instantly.","Alert on ErrLockHeld to detect concurrent pipelines or stale locks.","Clean up lock state when a migration runner is definitively dead."],"tags":["go","locking","migration","spanner","go-migrate"],"backgroundTag":"lock-contention","analyzedSha":"01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a","analyzedAt":"2026-09-02T19:38:29.671Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}