gastownhall/beads · error

acquiring lock (non-blocking): %w

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.

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)

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.