{"record":{"id":"383e1bc08f26e625","repo":"golang-migrate/migrate","slug":"can-t-acquire-lock","errorCode":null,"errorMessage":"can't acquire lock","messagePattern":"can't acquire lock","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"database/driver.go","lineNumber":16,"sourceCode":"// Package database provides the Driver interface.\n// All database drivers must implement this interface, register themselves,\n// optionally provide a `WithInstance` function and pass the tests\n// in package database/testing.\npackage database\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\n\tiurl \"github.com/golang-migrate/migrate/v4/internal/url\"\n)\n\nvar (\n\tErrLocked    = fmt.Errorf(\"can't acquire lock\")\n\tErrNotLocked = fmt.Errorf(\"can't unlock, as not currently locked\")\n)\n\nconst NilVersion int = -1\n\nvar driversMu sync.RWMutex\nvar drivers = make(map[string]Driver)\n\n// Driver is the interface every database driver must implement.\n//\n// How to implement a database driver?\n//  1. Implement this interface.\n//  2. Optionally, add a function named `WithInstance`.\n//     This function should accept an existing DB instance and a Config{} struct\n//     and return a driver instance.\n//  3. Add a test that calls database/testing.go:Test()\n//  4. Add own tests for Open(), WithInstance() (when provided) and Close().\n//     All other functions are tested by tests in database/testing.","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/golang-migrate/migrate/blob/01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a/database/driver.go#L1-L34","documentation":"database.ErrLocked (\"can't acquire lock\") is returned by drivers' Lock() (and applyTableLock) when a migration lock is already held. Most drivers use an atomic isLocked flag; if CompareAndSwap(false, true) fails, someone (another goroutine, or the same instance) already locked this driver instance. It prevents concurrent migrations from racing.","triggerScenarios":"Calling m.Lock() twice on the same Migrate instance without Unlock; two goroutines sharing one driver instance both calling Lock; an earlier Lock whose deferred Unlock never ran (e.g. after a panic or early return).","commonSituations":"Concurrent migrate calls from the same process sharing a driver; leaked lock after a migration error skipped Unlock; running migrations from two workers on the same driver object in tests.","solutions":["Ensure every Lock has a matching Unlock, ideally via defer m.Unlock().","Serialize migration attempts in-process with your own mutex, or use separate driver instances per goroutine backed by a database-level advisory/table lock.","If a previous run leaked the lock, restart or recreate the driver instance (resets the in-memory flag), and check the DB lock table for stale rows."],"exampleFix":"// before\nm.Up() // Lock taken internally; retry loop calls again while held\n// after\nif err := m.Lock(); err != nil {\n    if errors.Is(err, database.ErrLocked) { log.Println(\"migration already running\"); return }\n    return err\n}\ndefer m.Unlock()\nif err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { log.Fatal(err) }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := m.Lock(); err != nil {\n    if errors.Is(err, database.ErrLocked) {\n        return fmt.Errorf(\"another migration is in progress\")\n    }\n    return err\n}\ndefer m.Unlock()","preventionTips":["Pair every Lock with a deferred Unlock immediately after a successful Lock.","Run only one migration path per driver instance at a time; serialize with your own mutex if needed.","Check stale lock rows (schema_lock) after crashed runs before retrying."],"tags":["locking","concurrency","migration"],"backgroundTag":"lock-already-held","analyzedSha":"01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a","analyzedAt":"2026-09-02T19:38:29.671Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}