diff --git a/solver/jobs.go b/solver/jobs.go index 932d9d0f..67f1a30f 100644 --- a/solver/jobs.go +++ b/solver/jobs.go @@ -659,6 +659,7 @@ func (s *sharedOp) CalcSlowCache(ctx context.Context, index Index, p PreprocessF case <-ctx.Done(): if strings.Contains(err.Error(), context.Canceled.Error()) { complete = false + releaseError(err) err = errors.Wrap(ctx.Err(), err.Error()) } default: @@ -717,6 +718,7 @@ func (s *sharedOp) CacheMap(ctx context.Context, index int) (resp *cacheMapResp, case <-ctx.Done(): if strings.Contains(err.Error(), context.Canceled.Error()) { complete = false + releaseError(err) err = errors.Wrap(ctx.Err(), err.Error()) } default: @@ -774,6 +776,7 @@ func (s *sharedOp) Exec(ctx context.Context, inputs []Result) (outputs []Result, case <-ctx.Done(): if strings.Contains(err.Error(), context.Canceled.Error()) { complete = false + releaseError(err) err = errors.Wrap(ctx.Err(), err.Error()) } default: @@ -911,3 +914,15 @@ func WrapSlowCache(err error, index Index, res Result) error { } return &SlowCacheError{Index: index, Result: res, error: err} } + +func releaseError(err error) { + if err == nil { + return + } + if re, ok := err.(interface { + Release() error + }); ok { + re.Release() + } + releaseError(errors.Unwrap(err)) +} diff --git a/solver/llbsolver/bridge.go b/solver/llbsolver/bridge.go index 30d92320..e6a5850f 100644 --- a/solver/llbsolver/bridge.go +++ b/solver/llbsolver/bridge.go @@ -167,6 +167,8 @@ func newResultProxy(b *llbBridge, req frontend.SolveRequest) *resultProxy { rp.errResults = append(rp.errResults, res) return nil }) + // acquire ownership so ExecError finalizer doesn't attempt to release as well + ee.OwnerBorrowed = true } return res, err } diff --git a/solver/llbsolver/errdefs/exec.go b/solver/llbsolver/errdefs/exec.go index 0a41792d..f6075710 100644 --- a/solver/llbsolver/errdefs/exec.go +++ b/solver/llbsolver/errdefs/exec.go @@ -1,14 +1,19 @@ package errdefs import ( + "context" + "runtime" + "github.com/moby/buildkit/solver" + "github.com/sirupsen/logrus" ) // ExecError will be returned when an error is encountered when evaluating an op. type ExecError struct { error - Inputs []solver.Result - Mounts []solver.Result + Inputs []solver.Result + Mounts []solver.Result + OwnerBorrowed bool } func (e *ExecError) Unwrap() error { @@ -35,13 +40,35 @@ func (e *ExecError) EachRef(fn func(solver.Result) error) (err error) { return err } +func (e *ExecError) Release() error { + if e.OwnerBorrowed { + return nil + } + err := e.EachRef(func(r solver.Result) error { + r.Release(context.TODO()) + return nil + }) + e.OwnerBorrowed = true + return err +} + func WithExecError(err error, inputs, mounts []solver.Result) error { if err == nil { return nil } - return &ExecError{ + ee := &ExecError{ error: err, Inputs: inputs, Mounts: mounts, } + runtime.SetFinalizer(ee, func(e *ExecError) { + if !e.OwnerBorrowed { + e.EachRef(func(r solver.Result) error { + logrus.Warn("leaked execError detected and released") + r.Release(context.TODO()) + return nil + }) + } + }) + return ee }