{"record":{"id":"d24d6b28c767ff0b","repo":"dagger/dagger","slug":"s-is-locked","errorCode":null,"errorMessage":"%s is locked","messagePattern":"(.+?) is locked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"engine/snapshots/manager.go","lineNumber":412,"sourceCode":"\tbklog.G(context.TODO()).WithFields(ref.traceLogFields()).Trace(\"acquired cache ref\")\n\treturn ref, nil\n}\n\nfunc (cm *snapshotManager) GetMutable(ctx context.Context, id string, opts ...RefOption) (MutableRef, error) {\n\tcm.mu.Lock()\n\tdefer cm.mu.Unlock()\n\n\trec, err := cm.getRecord(ctx, id, opts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !rec.mutable {\n\t\treturn nil, errors.Wrapf(errInvalid, \"%s is not mutable\", id)\n\t}\n\n\tif rec.locked {\n\t\treturn nil, errors.Wrapf(ErrLocked, \"%s is locked\", id)\n\t}\n\trec.locked = true\n\tref := &mutableRef{\n\t\tcm:              cm,\n\t\trefMetadata:     refMetadata{snapshotID: rec.md.getSnapshotID(), md: rec.md},\n\t\ttriggerLastUsed: true,\n\t}\n\tbklog.G(context.TODO()).WithFields(ref.traceLogFields()).Trace(\"acquired cache ref\")\n\treturn ref, nil\n}\n\nfunc (cm *snapshotManager) GetMutableBySnapshotID(ctx context.Context, snapshotID string, opts ...RefOption) (MutableRef, error) {\n\tcm.mu.Lock()\n\tdefer cm.mu.Unlock()\n\tif err := cm.rehydrateSnapshotMetadataLocked(ctx, snapshotID, false); err != nil {\n\t\treturn nil, err\n\t}\n\trec, err := cm.getRecord(ctx, snapshotID, opts...)","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/dagger/dagger/blob/82ba2681dbe30d3547a1dc50ea495900ab5b6047/engine/snapshots/manager.go#L394-L430","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before: leaked lock on error\nmr, _ := cm.GetMutable(ctx, id)\nif err := write(mr); err != nil {\n    return err // mr never released -> stays locked\n}\n// after\nmr, err := cm.GetMutable(ctx, id)\nif err != nil { return err }\ndefer mr.Release()","handlingStrategy":"try-catch","validationCode":"// only attempt acquisition if your ownership table says the ref is free\nif refLocked(id) { return ErrBusy }","typeGuard":null,"tryCatchPattern":"mr, err := cm.GetMutable(ctx, id)\nif err != nil && errors.Is(err, ErrLocked) {\n    return fmt.Errorf(\"ref %s in use by another consumer; retry later\", id)\n}","preventionTips":["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"],"tags":["locking","cache","concurrency"],"backgroundTag":"ref-already-locked","analyzedSha":"82ba2681dbe30d3547a1dc50ea495900ab5b6047","analyzedAt":"2026-09-05T07:21:37.930Z","contentChangedAt":"2026-09-05T07:21:37.930Z","schemaVersion":2},"datasetVersion":"2026-09-12T12:17:11.808Z"}