gastownhall/beads · error

acquiring lock (non-blocking)

Error message

acquiring lock (non-blocking): %w

What it means

AcquireSyncLock with wait=false attempts a non-blocking exclusive flock; a contention result is converted to SyncLockHeldError, but any other flock failure is wrapped as this error. It means the non-blocking acquire failed for a reason other than the lock simply being held.

Solutions

  1. Inspect the wrapped error to find the underlying OS failure and fix it (permissions, filesystem support)
  2. Delete a stale/corrupt .linear-sync.lock file (only if no sync is running) and retry
  3. Move the beads directory to a local filesystem if flock is unsupported
  4. If the intent was to skip when another sync runs, note that contention is reported as SyncLockHeldError instead — handle that type separately

Example fix

// before: treating all errors the same
lock, err := linear.AcquireSyncLock(beadsDir, false)
if err != nil { return err }
// after: distinguish contention from real failure
lock, err := linear.AcquireSyncLock(beadsDir, false)
var held *linear.SyncLockHeldError
if errors.As(err, &held) {
    return nil // another sync running; skip
}
if err != nil { return fmt.Errorf("lock failure: %w", err) }
Defensive patterns

Strategy: type-guard

Type guard

func isSyncLockHeld(err error) bool {
    var held *linear.SyncLockHeldError
    return errors.As(err, &held)
}
// Usage: if !isSyncLockHeld(err) { return err } // real failure, not contention

Try / catch

lock, err := linear.AcquireSyncLock(beadsDir, false)
if err != nil {
    if isSyncLockHeld(err) {
        return nil // expected contention; skip run
    }
    return fmt.Errorf("non-blocking lock acquire failed: %w", err)
}
defer lock.Release()

Prevention

When it happens

Trigger: Calling AcquireSyncLock(beadsDir, false) when lockfile.FlockExclusiveNonBlocking returns an error that is neither lockfile.IsLocked(err) nor ErrLockBusy — e.g. I/O errors, EBADF, or platform flock anomalies.

Common situations: Corrupted or unreadable lock file; filesystem not supporting flock; running on a platform where the lock helper hits an unexpected OS error; unusual permission changes on .linear-sync.lock after creation.

Related errors


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

Appendix: source

Thrown at internal/linear/synclock.go:60

	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600) // #nosec G304 -- lockPath is constrained to the beads directory.
	if err != nil {
		return nil, fmt.Errorf("opening lock file: %w", err)
	}

	if wait {
		if err := lockfile.FlockExclusiveBlocking(f); err != nil {
			_ = f.Close()
			return nil, fmt.Errorf("acquiring lock (blocking): %w", err)
		}
	} else {
		if err := lockfile.FlockExclusiveNonBlocking(f); err != nil {
			if lockfile.IsLocked(err) || err == lockfile.ErrLockBusy {
				info := readContendedSyncLockInfo(infoPath)
				_ = f.Close()
				return nil, &SyncLockHeldError{Info: info}
			}
			_ = f.Close()
			return nil, fmt.Errorf("acquiring lock (non-blocking): %w", err)
		}
	}

	metadata, err := publishSyncLockInfo(f, infoPath)
	if err != nil {
		_ = lockfile.FlockUnlock(f)
		_ = f.Close()
		return nil, fmt.Errorf("writing lock info: %w", err)
	}

	return &SyncLock{infoPath: infoPath, file: f, metadata: metadata}, nil
}

// Release releases the sync lock. The kernel-lock file is NOT removed — doing
// so after unlocking creates a race where a blocked waiter acquires the old
// inode while a new process creates a fresh file at the same path, splitting
// lock identity. On Unix, inline owner metadata is truncated while still
// holding the lock. On Windows, a separate advisory record is cleared while

View on GitHub (pinned to 71377f2769)