dagger/dagger · warning
%s is locked
Error message
%s is locked
What it means
Cache refs are exclusively locked: a record with rec.locked == true is currently held by another consumer, and GetMutable refuses to hand out a second handle, wrapping ErrLocked as "%s is locked". The lock is cleared when the current holder releases the ref.
Source
Thrown at engine/snapshots/manager.go:412
bklog.G(context.TODO()).WithFields(ref.traceLogFields()).Trace("acquired cache ref")
return ref, nil
}
func (cm *snapshotManager) GetMutable(ctx context.Context, id string, opts ...RefOption) (MutableRef, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
rec, err := cm.getRecord(ctx, id, opts...)
if err != nil {
return nil, err
}
if !rec.mutable {
return nil, errors.Wrapf(errInvalid, "%s is not mutable", id)
}
if rec.locked {
return nil, errors.Wrapf(ErrLocked, "%s is locked", id)
}
rec.locked = true
ref := &mutableRef{
cm: cm,
refMetadata: refMetadata{snapshotID: rec.md.getSnapshotID(), md: rec.md},
triggerLastUsed: true,
}
bklog.G(context.TODO()).WithFields(ref.traceLogFields()).Trace("acquired cache ref")
return ref, nil
}
func (cm *snapshotManager) GetMutableBySnapshotID(ctx context.Context, snapshotID string, opts ...RefOption) (MutableRef, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
if err := cm.rehydrateSnapshotMetadataLocked(ctx, snapshotID, false); err != nil {
return nil, err
}
rec, err := cm.getRecord(ctx, snapshotID, opts...)View on GitHub (pinned to 82ba2681db)
Solutions
- Ensure each consumer uses its own ref (call New per writer) instead of sharing one mutable ref
- Make sure every code path that acquires a ref releases it (defer ref.Release()) even on error
- Retry after the current holder releases the ref (locks are exclusive but transient)
- Restructure the build so write access is serialized through a single owner goroutine
Example fix
// before: leaked lock on error
mr, _ := cm.GetMutable(ctx, id)
if err := write(mr); err != nil {
return err // mr never released -> stays locked
}
// after
mr, err := cm.GetMutable(ctx, id)
if err != nil { return err }
defer mr.Release() Defensive patterns
Strategy: try-catch
Validate before calling
// only attempt acquisition if your ownership table says the ref is free
if refLocked(id) { return ErrBusy } Try / catch
mr, err := cm.GetMutable(ctx, id)
if err != nil && errors.Is(err, ErrLocked) {
return fmt.Errorf("ref %s in use by another consumer; retry later", id)
} Prevention
- Always pair ref acquisition with defer mr.Release()
- Give each concurrent writer its own ref via New rather than sharing
- Track ref ownership in your orchestrator to prevent double acquisition
When it happens
Trigger: Two consumers calling GetMutable (or New followed by GetMutable) for the same ref ID concurrently, or calling GetMutable for a ref whose previous holder never released it (leaked/abandoned lock).
Common situations: Parallel build steps sharing one cache ref; a goroutine that errored out without calling Release, leaving locked=true; New itself sets locked=true, so any later GetMutable before release hits this.
Related errors
- get current query for cache locks: %w
- missing engine locker
- cache lock key for mount %d: %w
- acquire shared operation lease: %w
- cache wait completed without initialized result
AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05).
Data as JSON: /api/errors/d24d6b28c767ff0b.
Report an issue: GitHub.