charmbracelet/crush · error

acquire config lock: %w

Error message

acquire config lock: %w

What it means

lockConfig takes a cross-process advisory flock (path+'.lock') with a bounded deadline (configLockDeadline). This error wraps a failure to acquire that lock within the deadline or an OS-level flock error — another process holds the config lock, or the lock file cannot be created/locked.

Source

Thrown at internal/config/store.go:287

// as soon as the file access is complete — no I/O should be performed
// while the lock is held.
func (s *ConfigStore) lockConfig(scope Scope) (func(), error) {
	s.mu.Lock()
	path, err := s.configPath(scope)
	if err != nil {
		s.mu.Unlock()
		return nil, err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		s.mu.Unlock()
		return nil, fmt.Errorf("create config directory: %w", err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), configLockDeadline)
	defer cancel()
	release, err := lock.File(ctx, path+".lock")
	if err != nil {
		s.mu.Unlock()
		return nil, fmt.Errorf("acquire config lock: %w", err)
	}
	return func() {
		release()
		s.mu.Unlock()
	}, nil
}

// atomicWrite handles the lock-read-transform-write-unlock cycle for
// config file mutations. The fn callback receives the current file
// contents (raw bytes, or {} if the file is missing) and must return the
// new contents. fn must be pure — no I/O, no network calls.
func (s *ConfigStore) atomicWrite(scope Scope, fn func(current []byte) ([]byte, error)) error {
	unlock, err := s.lockConfig(scope)
	if err != nil {
		return err
	}
	defer unlock()

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Wait a moment and retry — the deadline means another process briefly held the lock
  2. Check for other running crush processes (pgrep) and stop the conflicting one
  3. Move config to a local filesystem if it resides on NFS/SMB without working flock
  4. Remove a genuinely stale .lock file only after confirming no process holds it

Example fix

// before
err := store.SetConfigField(scope, key, val) // hard failure on lock contention
// after
for i := 0; i < 3; i++ {
    err := store.SetConfigField(scope, key, val)
    if err == nil || !strings.Contains(err.Error(), "acquire config lock") {
        break
    }
    time.Sleep(500 * time.Millisecond) // retry on transient lock contention
}
Defensive patterns

Strategy: retry

Validate before calling

if _, err := os.Stat(configPath + ".lock"); err == nil {
    // lock exists; check for a live holder before retrying
}
if lsofHoldsLock(configPath+".lock") {
    return errors.New("another crush process is writing config; wait and retry")
}

Type guard

func isLockTimeout(err error) bool {
    return err != nil && strings.Contains(err.Error(), "acquire config lock") && errors.Is(err, context.DeadlineExceeded)
}

Try / catch

var lastErr error
for i := 0; i < 5; i++ {
    err := store.SetConfigField(scope, key, val)
    if err == nil || !isLockTimeout(err) {
        return err
    }
    lastErr = err
    time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)
}
return lastErr

Prevention

When it happens

Trigger: Running two crush instances (or a crashed process leaking a stale lock) that concurrently call atomicWrite; the lock file lives on a filesystem without flock support (some network mounts); the deadline expires while waiting for a peer's long-held lock.

Common situations: Two crush sessions editing config simultaneously; an editor/script with a long-running config operation; NFS/SMB mounts where flock fails; leftover .lock file from a killed process (usually harmless — flock releases on fd close, but broken fs semantics can stall).

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/e03c12530e51ee81. Report an issue: GitHub.