{"record":{"id":"0d75ea0cc269d6c2","repo":"gastownhall/beads","slug":"schema-release-migration-lock-w-returned-d","errorCode":null,"errorMessage":"schema: release migration lock: %w: returned %d","messagePattern":"schema: release migration lock: %w: returned (.+?)","errorType":"exception","errorClass":"ErrMigrationLockRelease","httpStatus":null,"severity":"warning","filePath":"internal/storage/schema/lock.go","lineNumber":380,"sourceCode":"\n// ReleaseMigrationLock releases the named schema migration lock from the same\n// pinned Dolt/MySQL session used to acquire it.\nfunc ReleaseMigrationLock(conn *sql.Conn, lockName string) error {\n\tcleanupCtx, cancel := context.WithTimeout(context.Background(), migrationLockCleanupTimeout)\n\tdefer cancel()\n\n\tvar released sql.NullInt64\n\tif err := conn.QueryRowContext(cleanupCtx, \"SELECT RELEASE_LOCK(?)\", lockName).Scan(&released); err != nil {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: %w\", ErrMigrationLockRelease, err)\n\t}\n\tif !released.Valid {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: returned NULL\", ErrMigrationLockRelease)\n\t}\n\tif released.Int64 != 1 {\n\t\tdiscardConn(conn)\n\t\treturn fmt.Errorf(\"schema: release migration lock: %w: returned %d\", ErrMigrationLockRelease, released.Int64)\n\t}\n\treturn nil\n}\n\nfunc discardConn(conn *sql.Conn) {\n\t_ = conn.Raw(func(driverConn any) error {\n\t\treturn driver.ErrBadConn\n\t})\n}\n","sourceCodeStart":362,"sourceCodeEnd":390,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/schema/lock.go#L362-L390","documentation":"RELEASE_LOCK returned a value other than 1 (and not NULL), e.g. 0, meaning the lock exists but is held by a different session. The library wraps ErrMigrationLockRelease with the returned value and discards the connection. Like the NULL case, it flags a broken acquire/release pairing.","triggerScenarios":"Calling ReleaseMigrationLock with a lockName whose lock is currently owned by another session; race where another process acquired the same-named lock after this session's lock expired or was dropped.","commonSituations":"Two processes using the same lockName with one having failed to acquire but proceeding to release; lock name collisions between different tools/jobs; mismatched lock names between acquire and release calls.","solutions":["Verify the release lockName exactly matches the one passed to AcquireMigrationLock.","Ensure the release happens only after a successful acquire (propagate acquire errors and skip release).","Use unique lock names per database/deployment to avoid cross-process collisions.","If 0 persists, inspect who holds the lock (performance_schema.metadata_locks or Dolt equivalents) and stop that holder."],"exampleFix":"// before: release called even when acquire failed\nerr := schema.AcquireMigrationLock(ctx, conn, name)\ndefer schema.ReleaseMigrationLock(conn, name)\n// after: release only on success\nif err := schema.AcquireMigrationLock(ctx, conn, name); err != nil {\n    return err\n}\ndefer schema.ReleaseMigrationLock(conn, name)","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := schema.ReleaseMigrationLock(conn, name); err != nil {\n    if errors.Is(err, schema.ErrMigrationLockRelease) && strings.Contains(err.Error(), \"returned 0\") {\n        // another session owns the lock: investigate contention\n    }\n}","preventionTips":["Use distinct lock names per deployment/job to avoid collisions.","Only release after a successful acquire.","Ensure constant lock names between acquire and release call sites.","Investigate other holders when release reports a foreign-owned lock."],"tags":["database","locking","race-condition","dolt"],"backgroundTag":"lock-not-owned","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}