{"record":{"id":"03abe795e4533992","repo":"gastownhall/beads","slug":"timeout-waiting-for-cache-lock-on-s","errorCode":null,"errorMessage":"timeout waiting for cache lock on %s","messagePattern":"timeout waiting for cache lock on (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/remotecache/cache.go","lineNumber":257,"sourceCode":"\tf, err := os.OpenFile(lp, os.O_CREATE|os.O_RDWR, 0o600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Poll with timeout\n\tdeadline := time.Now().Add(2 * time.Minute)\n\tfor {\n\t\terr := lockfile.FlockExclusiveNonBlocking(f)\n\t\tif err == nil {\n\t\t\treturn f, nil\n\t\t}\n\t\tif !lockfile.IsLocked(err) {\n\t\t\t_ = f.Close()\n\t\t\treturn nil, err\n\t\t}\n\t\tif time.Now().After(deadline) {\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"timeout waiting for cache lock on %s\", remoteURL)\n\t\t}\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\t_ = f.Close()\n\t\t\treturn nil, fmt.Errorf(\"interrupted waiting for cache lock on %s: %w\", remoteURL, ctx.Err())\n\t\tcase <-time.After(100 * time.Millisecond):\n\t\t}\n\t}\n}\n\n// releaseLock releases a cache entry file lock.\n// The lock file is intentionally NOT removed: deleting it after unlock creates\n// a TOCTOU race where another process's newly-acquired lock gets deleted.\n// Stale lock files are cleaned up by acquireLock's age check instead.\nfunc (c *Cache) releaseLock(f *os.File) {\n\tif f != nil {\n\t\t_ = lockfile.FlockUnlock(f)\n\t\t_ = f.Close()","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/remotecache/cache.go#L239-L275","documentation":"acquireLock() polls a non-blocking exclusive flock on the cache entry's .lock file for up to 2 minutes (stale locks older than 5 minutes are removed first). This error means another process held the lock continuously for the whole deadline, so the operation was abandoned rather than mutating the dolt database concurrently.","triggerScenarios":"Cache.Ensure() or Cache.Push() on a remoteURL whose .lock is held by another live process (long-running bd sync, stuck dolt command) for >2 minutes; the lock file is younger than staleLockAge so it is not treated as stale.","commonSituations":"Two shells running `bd sync`/`bd pull` on the same remote simultaneously; a hung dolt operation leaving a live-looking lock; backup/indexing software holding the file; very slow network making a legitimate push exceed 2 minutes.","solutions":["Identify and finish/kill the other process holding the lock (lsof/fuser on <cache-entry>/.lock).","If no process is using it and the lock is genuinely stale-but-young, remove the .lock file (or wait out staleLockAge) and retry.","Serialize usage: avoid running multiple bd commands against the same remote concurrently (scripts, cron overlap).","Retry after a delay — this is a transient contention error, not data corruption."],"exampleFix":"// before: immediate retry, same contention\nif err := cache.Push(ctx, url); err != nil { return err }\n// after: detect lock timeout and back off\nif err := cache.Push(ctx, url); err != nil {\n    if strings.Contains(err.Error(), \"timeout waiting for cache lock\") {\n        return fmt.Errorf(\"another bd process is syncing %s; retry later\", url)\n    }\n    return err\n}","handlingStrategy":"retry","validationCode":"lockPath := filepath.Join(cacheDir, \"beads\", \"remotes\", remotecache.CacheKey(remoteURL), \".lock\")\nif info, err := os.Stat(lockPath); err == nil {\n    age := time.Since(info.ModTime())\n    if age < 5*time.Minute {\n        return fmt.Errorf(\"cache lock active (%.0fs old); wait or stop the other process\", age.Seconds())\n    }\n}","typeGuard":null,"tryCatchPattern":"err := doSync(ctx) // Ensure/Push inside\nif err != nil && strings.Contains(err.Error(), \"timeout waiting for cache lock\") {\n    // check no bd process is running, optionally clear a stale .lock, then retry once\n    select {\n    case <-time.After(time.Minute):\n        return doSync(ctx)\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}","preventionTips":["Avoid overlapping scheduled jobs (cron/CI) that sync the same remote.","Use a supervisor/mutex in your own tooling to serialize remote operations.","Inspect `lsof <cache-entry>/.lock` before manually deleting a lock file.","Keep sync durations short (fresh FreshFor TTL) so locks are released quickly."],"tags":["go","locking","flock","timeout","concurrency"],"backgroundTag":"file-lock-timeout","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}