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

  1. Ensure each consumer uses its own ref (call New per writer) instead of sharing one mutable ref
  2. Make sure every code path that acquires a ref releases it (defer ref.Release()) even on error
  3. Retry after the current holder releases the ref (locks are exclusive but transient)
  4. 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

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


AI-assisted analysis of dagger/dagger@82ba2681db (2026-09-05). Data as JSON: /api/errors/d24d6b28c767ff0b. Report an issue: GitHub.