multica-ai/multica · warning · ErrRepoBusy

ErrRepoBusy

ErrRepoBusy

Error message

repository is busy

What it means

Sentinel error ErrRepoBusy in server/internal/daemon/repocache/cache.go: a foreground checkout requested a repository whose dedicated repoLock was held (by another clone/fetch/worktree-add/ref-update, or by low-priority maintenance), and the caller's bounded wait expired before the lock became free. git's own lockfiles cannot tolerate parallel mutations on one repo, so the cache serializes them; callers that advertised retry support are expected to convert this into a retryable response instead of blocking to the transport deadline.

Source

Thrown at server/internal/daemon/repocache/cache.go:176

}

// Cache manages bare git clones for workspace repositories.
type Cache struct {
	root   string // base directory for all caches (e.g. ~/multica_workspaces/.repos)
	logger *slog.Logger
	// repoLocks maps bare repo path → dedicated mutex. Any mutating operation
	// on a given bare repo (clone, fetch, worktree add, ref update) must
	// hold its lock — git's own lockfiles (packed-refs.lock, config.lock,
	// worktree admin dirs) don't tolerate parallel mutations on the same
	// repo. Separate repos are independent and run concurrently.
	repoLocks sync.Map // barePath -> *repoLock
}

// ErrRepoBusy means a foreground checkout could not acquire its repository
// within the caller's bounded wait. Callers that advertised retry support can
// turn this into a retryable HTTP response instead of waiting until their
// transport deadline expires.
var ErrRepoBusy = errors.New("repository is busy")

// Activity is the path-free repository coordination state exposed through the
// daemon health endpoint. It is diagnostic only.
type Activity struct {
	MaintenanceActive int
	ForegroundWaiters int
}

// repoLock is a foreground-priority mutex. Ordinary cache mutations serialize
// exactly as they did with sync.Mutex. Low-priority maintenance is different:
// it only starts on an idle repository and receives a context that is cancelled
// as soon as a foreground operation queues. The maintenance holder remains
// responsible for stopping its Git process tree before unlocking.
type repoLock struct {
	mu                sync.Mutex
	held              bool
	maintenance       bool
	maintenanceCancel context.CancelCauseFunc

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. Retry the operation after a backoff — the lock holder will usually finish and the retry acquires the lock.
  2. Stagger task starts that target the same repo to avoid lock contention.
  3. If it recurs constantly, inspect the daemon health Activity (ForegroundWaiters/MaintenanceActive) to see whether maintenance is starving foreground ops.
  4. Callers: map ErrRepoBusy to a retryable HTTP response (e.g. 429/503 with Retry-After) as the doc comment instructs, not to a hard failure.

Example fix

// go — caller-side retryable mapping
if errors.Is(err, repocache.ErrRepoBusy) {
    w.Header().Set("Retry-After", "2")
    http.Error(w, "repository is busy, retry shortly", http.StatusTooManyRequests)
    return
}
Defensive patterns

Strategy: retry

Try / catch

if errors.Is(err, repocache.ErrRepoBusy) {
    w.Header().Set("Retry-After", "2")
    http.Error(w, "repository is busy, retry shortly", http.StatusTooManyRequests)
    return
}
// client side: honor Retry-After and re-issue the request

Prevention

When it happens

Trigger: Two tasks targeting the same repo starting near-simultaneously; a background maintenance pass (GC/fetch) holding the lock when a foreground checkout queues; a large clone starving subsequent waiters past their bounded wait.

Common situations: Parallel agent runs on the same repository; scheduled repo maintenance overlapping task start; big monorepo clones taking longer than the foreground wait budget.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/c8387533b3ecbff6. Report an issue: GitHub.