gastownhall/beads · warning
interrupted waiting for cache lock on %s: %w
Error message
interrupted waiting for cache lock on %s: %w
What it means
acquireLock() selects on both the lock poll and the caller's context while waiting for the cache lock. This error means the context passed to Ensure()/Push() was cancelled or its deadline expired while blocked on lock contention. The underlying ctx.Err() (context.Canceled or context.DeadlineExceeded) is wrapped so callers can errors.Is() it.
Source
Thrown at internal/remotecache/cache.go:262
// Poll with timeout
deadline := time.Now().Add(2 * time.Minute)
for {
err := lockfile.FlockExclusiveNonBlocking(f)
if err == nil {
return f, nil
}
if !lockfile.IsLocked(err) {
_ = f.Close()
return nil, err
}
if time.Now().After(deadline) {
_ = f.Close()
return nil, fmt.Errorf("timeout waiting for cache lock on %s", remoteURL)
}
select {
case <-ctx.Done():
_ = f.Close()
return nil, fmt.Errorf("interrupted waiting for cache lock on %s: %w", remoteURL, ctx.Err())
case <-time.After(100 * time.Millisecond):
}
}
}
// releaseLock releases a cache entry file lock.
// The lock file is intentionally NOT removed: deleting it after unlock creates
// a TOCTOU race where another process's newly-acquired lock gets deleted.
// Stale lock files are cleaned up by acquireLock's age check instead.
func (c *Cache) releaseLock(f *os.File) {
if f != nil {
_ = lockfile.FlockUnlock(f)
_ = f.Close()
}
}
// readMeta reads the cache metadata for a remote URL.
func (c *Cache) readMeta(remoteURL string) *CacheMeta {View on GitHub (pinned to 71377f2769)
Solutions
- Treat as expected cancellation: check errors.Is(err, context.Canceled) / context.DeadlineExceeded and unwind cleanly.
- If wait time is legitimate, pass a longer-lived context (context.Background() or a bigger deadline).
- Resolve the underlying contention (see lock timeout guidance) so waits are short.
- Retry the operation with a fresh context once the other process finishes.
Example fix
// before: cancelling a background sync after 5s ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := cache.Ensure(ctx, url) // lock wait often exceeds 5s // after: generous deadline for lock waits ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() _, err := cache.Ensure(ctx, url)
Defensive patterns
Strategy: try-catch
Validate before calling
// check the context budget you're giving the call
deadline, ok := ctx.Deadline()
if ok && time.Until(deadline) < 30*time.Second {
fmt.Fprintln(os.Stderr, "warning: context may expire while waiting for cache lock")
} Try / catch
if _, err := cache.Ensure(ctx, url); err != nil {
switch {
case errors.Is(err, context.Canceled):
return fmt.Errorf("sync cancelled by user: %w", err)
case errors.Is(err, context.DeadlineExceeded):
return fmt.Errorf("sync deadline too short for lock wait: %w", err)
}
return err
} Prevention
- Pass a context with a deadline that accommodates up to ~2 minutes of lock waiting.
- Wire SIGINT/SIGTERM to context cancellation and treat this error as normal shutdown.
- Don't reuse short-lived HTTP/request contexts for long cache operations.
- Use errors.Is against context.Canceled/DeadlineExceeded to distinguish cancellation from real lock failures.
When it happens
Trigger: Cache.Ensure() or Cache.Push() called with a context that gets cancelled (Ctrl-C/SIGINT, timeout, parent cancellation) while another process holds the .lock for the same remoteURL.
Common situations: User hits Ctrl-C during a slow sync blocked behind another process; command run with a short timeout (e.g. exec.CommandContext or HTTP request context) that expires while waiting; orchestrator cancelling jobs.
Related errors
- workspacegate: acquiring %s: %w
- workspacegate: waiting for %s: %w
- timeout waiting for cache lock on %s
- util: creating lock directory: %w
- util: opening lock file: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/ced27ae7f3de660b.
Report an issue: GitHub.