gastownhall/beads · error

%w (lock release also failed: %w)

Error message

%w (lock release also failed: %w)

What it means

MigrateUpWithLock's deferred lock-release failed while the migration itself had also failed. The library combines both errors with two %w verbs so errors.Is/As match the primary migration error first and the release error (ErrMigrationLockRelease) second, without errors.Join's newline.

Source

Thrown at internal/storage/schema/lock.go:260

			// mode that leaves the GET_LOCK saturation this exists to remove
			// looking like a mystery. Say so where BD_DEBUG/-v can see it.
			debug.Logf("schema: convergence fast path unavailable for %q, taking the migration lock: %v\n",
				databaseName, convergedErr)
		case converged:
			return 0, nil
		}
	}

	lockName := MigrationLockName(databaseName)
	if err := AcquireMigrationLock(ctx, conn, lockName); err != nil {
		return 0, err
	}
	defer func() {
		if releaseErr := ReleaseMigrationLock(conn, lockName); releaseErr != nil {
			if err != nil {
				// Two %w verbs keep errors.Is/As working for both errors
				// without errors.Join's separator newline, primary first.
				err = fmt.Errorf("%w (lock release also failed: %w)", err, releaseErr)
			} else {
				err = errors.Join(err, releaseErr)
			}
		}
	}()
	if o.lockedPreparation != nil && o.lockedPreparation.fn != nil {
		capability, preparationErr := o.lockedPreparation.fn(ctx, conn)
		if preparationErr != nil {
			return 0, preparationErr
		}
		if capability != nil {
			o.freshBootstrapHeal = &freshBootstrapHealRequest{
				capability: capability,
				endpoint:   o.lockedPreparation.endpoint,
			}
		}
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check errors.Is(err, ErrMigrationLockRelease) alongside the primary error to know cleanup was uncertain.
  2. The library discards the connection on release failure, so the server will free the session-scoped lock when the dead session is reaped — retry after a short delay.
  3. Fix the primary migration error first; the release failure is usually a symptom of the same connection problem.
  4. If locks appear stuck, verify no long-lived sessions hold GET_LOCK (SELECT * FROM performance_schema.metadata_locks or Dolt equivalents) and restart the sql-server if needed.

Example fix

// before
if err != nil { return err } // hides release failure
// after
if err != nil {
    if errors.Is(err, schema.ErrMigrationLockRelease) {
        // primary err is also wrapped; connection was discarded; safe to retry
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

if err := conn.PingContext(ctx); err != nil { /* reacquire a healthy pinned conn before migrating */ }

Type guard

func releaseAlsoFailed(err error) bool { return errors.Is(err, schema.ErrMigrationLockRelease) }

Try / catch

applied, err := schema.MigrateUpWithLock(ctx, conn, db)
if err != nil {
    if errors.Is(err, schema.ErrMigrationLockRelease) {
        // release uncertain; conn was discarded; lock frees when session dies — retry after delay
    }
    // inspect primary error first: it is the leading %w
    return err
}

Prevention

When it happens

Trigger: RELEASE_LOCK on the pinned conn errors (broken/timed-out connection, context issues) at the same time MigrateUp already returned an error — e.g. server dropped the session mid-migration.

Common situations: Dolt sql-server restart or network partition during a failing migration; idle-connection kill between GET_LOCK and RELEASE_LOCK; the migration lock left held until the session dies.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/789187a744de2c8d. Report an issue: GitHub.