gastownhall/beads · error

util: opening lock file: %w

Error message

util: opening lock file: %w

What it means

TryLock opens (or creates) the lock file with O_CREATE|O_RDWR mode 0600 before attempting an exclusive flock; this error wraps the os.OpenFile failure. The lock was never taken because the file itself could not be opened.

Source

Thrown at internal/storage/dbproxy/util/flock.go:33

}

// Lock holds an exclusive flock on a file.
type Lock struct {
	f *os.File
}

// TryLock attempts to acquire a non-blocking exclusive flock on lockPath.
// The parent directory is created (mode 0700) if it does not exist. On
// contention returns an error that satisfies lockfile.IsLocked, so callers
// can detect "another holder is alive" and produce their own contextual
// error message.
func TryLock(lockPath string) (*Lock, error) {
	if err := os.MkdirAll(filepath.Dir(lockPath), 0700); err != nil {
		return nil, fmt.Errorf("util: creating lock directory: %w", err)
	}
	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600) //nolint:gosec // lockPath comes from caller-derived dirs, not user input
	if err != nil {
		return nil, fmt.Errorf("util: opening lock file: %w", err)
	}
	if err := lockfile.FlockExclusiveNonBlocking(f); err != nil {
		_ = f.Close()
		return nil, err
	}
	return &Lock{f: f}, nil
}

// File returns the underlying *os.File. Useful for fork+exec lock-fd
// inheritance: pass it via cmd.ExtraFiles, then Close() the parent's fd —
// the child retains the lock through its inherited fd, which references the
// same open file description.
func (l *Lock) File() *os.File {
	return l.f
}

// Unlock releases the flock and closes the underlying file. Panics on
// failure to prevent silent deadlocks.

View on GitHub (pinned to 71377f2769)

Solutions

  1. If the lock file exists but is owned by another user, chown it or remove it (confirm no live holder first via lsof/fuser)
  2. Check the path is a file, not a directory: rm the directory or point TryLock at a different path
  3. Fix file permissions so the current user has read/write (0600) on the lock file
  4. Ensure the volume is mounted read-write; on immutable storage, relocate the lock path

Example fix

// before
f, _ := os.Stat(lockPath) // blindly TryLock
lock, err := util.TryLock(lockPath)
// after
if info, serr := os.Stat(lockPath); serr == nil && info.IsDir() {
    return fmt.Errorf("lock path %s is a directory", lockPath)
}
lock, err := util.TryLock(lockPath)
Defensive patterns

Strategy: validation

Validate before calling

if st, err := os.Stat(lockPath); err == nil {
    if st.IsDir() { return fmt.Errorf("%s is a directory", lockPath) }
    if err := syscall.Access(lockPath, syscall.W_OK); err != nil {
        return fmt.Errorf("lock file %s not writable (owner %d): %w", lockPath, st.Sys().(*syscall.Stat_t).Uid, err)
    }
}

Try / catch

lock, err := util.TryLock(lockPath)
if err != nil {
    if errors.Is(err, fs.ErrPermission) {
        os.Remove(lockPath) // stale lock from another user; verify no live holder first
        lock, err = util.TryLock(lockPath)
    }
    if err != nil { return err }
}
defer lock.Close()

Prevention

When it happens

Trigger: TryLock called with a lockPath whose final component cannot be opened for read/write — the path exists as a directory, permission denied on an existing lock file owned by another user, or immutable/read-only filesystem.

Common situations: A previous run as root left a 0600 lock file now unwritable by the app user; the lockPath names an existing directory; stale lock file left in a read-only-mounted volume after a crash.

Related errors


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