{"record":{"id":"3470cb6ba46fd92d","repo":"golang-migrate/migrate","slug":"can-t-unlock-as-not-currently-locked","errorCode":null,"errorMessage":"can't unlock, as not currently locked","messagePattern":"can't unlock, as not currently locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"database/driver.go","lineNumber":17,"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.\n//     Saves you some time and makes sure all database drivers behave the same way.","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/golang-migrate/migrate/blob/01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a/database/driver.go#L1-L35","documentation":"database.ErrNotLocked (\"can't unlock, as not currently locked\") is returned by drivers' Unlock() when the driver's isLocked flag is false — i.e. Unlock was called without a prior successful Lock, or the lock was already released. It guards the symmetry of the Lock/Unlock protocol.","triggerScenarios":"Calling m.Unlock() twice; calling Unlock on a freshly created Migrate/driver instance; a failed Lock() followed by an unconditional deferred Unlock.","commonSituations":"Deferred Unlock running after a Lock error; double-release in retry logic; sharing a driver across goroutines where one already unlocked.","solutions":["Only call Unlock when Lock returned nil (check err before deferring, or defer a closure that tracks lock success).","Remove duplicate Unlock calls in retry/cleanup paths.","If you cannot trace the double unlock, wrap Unlock and treat errors.Is(err, database.ErrNotLocked) as benign and log instead of failing."],"exampleFix":"// before\nif err := m.Lock(); err != nil { return err } // then later in cleanup: m.Unlock() runs even on this path\n// after\nlocked := false\nif err := m.Lock(); err != nil { return err }\nlocked = true\ndefer func() { if locked { m.Unlock() } }()","handlingStrategy":"type-guard","validationCode":"if !isLocked {\n    return fmt.Errorf(\"cannot unlock: lock was never acquired\")\n}","typeGuard":"func canUnlock(m *migrate.Migrate, locked bool) bool { return locked }","tryCatchPattern":"if err := m.Unlock(); err != nil {\n    if errors.Is(err, database.ErrNotLocked) {\n        log.Println(\"unlock skipped: not locked\")\n        return nil\n    }\n    return err\n}","preventionTips":["Track lock ownership with a boolean and only unlock when you locked.","Never call Unlock after a failed Lock attempt.","Avoid duplicate cleanup paths that both unlock."],"tags":["locking","concurrency","migration"],"backgroundTag":"unlock-when-not-locked","analyzedSha":"01a9643f1475e75bb6d6224ddeaf9d8e2434ca8a","analyzedAt":"2026-09-02T19:38:29.671Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}