{"record":{"id":"e03c12530e51ee81","repo":"charmbracelet/crush","slug":"acquire-config-lock-w","errorCode":null,"errorMessage":"acquire config lock: %w","messagePattern":"acquire config lock: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/config/store.go","lineNumber":287,"sourceCode":"// as soon as the file access is complete — no I/O should be performed\n// while the lock is held.\nfunc (s *ConfigStore) lockConfig(scope Scope) (func(), error) {\n\ts.mu.Lock()\n\tpath, err := s.configPath(scope)\n\tif err != nil {\n\t\ts.mu.Unlock()\n\t\treturn nil, err\n\t}\n\tif err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {\n\t\ts.mu.Unlock()\n\t\treturn nil, fmt.Errorf(\"create config directory: %w\", err)\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), configLockDeadline)\n\tdefer cancel()\n\trelease, err := lock.File(ctx, path+\".lock\")\n\tif err != nil {\n\t\ts.mu.Unlock()\n\t\treturn nil, fmt.Errorf(\"acquire config lock: %w\", err)\n\t}\n\treturn func() {\n\t\trelease()\n\t\ts.mu.Unlock()\n\t}, nil\n}\n\n// atomicWrite handles the lock-read-transform-write-unlock cycle for\n// config file mutations. The fn callback receives the current file\n// contents (raw bytes, or {} if the file is missing) and must return the\n// new contents. fn must be pure — no I/O, no network calls.\nfunc (s *ConfigStore) atomicWrite(scope Scope, fn func(current []byte) ([]byte, error)) error {\n\tunlock, err := s.lockConfig(scope)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer unlock()\n","sourceCodeStart":269,"sourceCodeEnd":305,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/config/store.go#L269-L305","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Wait a moment and retry — the deadline means another process briefly held the lock","Check for other running crush processes (pgrep) and stop the conflicting one","Move config to a local filesystem if it resides on NFS/SMB without working flock","Remove a genuinely stale .lock file only after confirming no process holds it"],"exampleFix":"// before\nerr := store.SetConfigField(scope, key, val) // hard failure on lock contention\n// after\nfor i := 0; i < 3; i++ {\n    err := store.SetConfigField(scope, key, val)\n    if err == nil || !strings.Contains(err.Error(), \"acquire config lock\") {\n        break\n    }\n    time.Sleep(500 * time.Millisecond) // retry on transient lock contention\n}","handlingStrategy":"retry","validationCode":"if _, err := os.Stat(configPath + \".lock\"); err == nil {\n    // lock exists; check for a live holder before retrying\n}\nif lsofHoldsLock(configPath+\".lock\") {\n    return errors.New(\"another crush process is writing config; wait and retry\")\n}","typeGuard":"func isLockTimeout(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"acquire config lock\") && errors.Is(err, context.DeadlineExceeded)\n}","tryCatchPattern":"var lastErr error\nfor i := 0; i < 5; i++ {\n    err := store.SetConfigField(scope, key, val)\n    if err == nil || !isLockTimeout(err) {\n        return err\n    }\n    lastErr = err\n    time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)\n}\nreturn lastErr","preventionTips":["Avoid running multiple crush instances that mutate config simultaneously","Keep the config on a local filesystem with working flock support (not NFS/SMB)","Kill -9'd processes release flock on exit, but check for wedged holders with lsof/fuser","Keep writes short; never do network I/O while holding the config lock"],"tags":["filesystem","locking","concurrency"],"backgroundTag":"config-lock-contention","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}