431 lines
10 KiB
Go
431 lines
10 KiB
Go
package solver
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/moby/buildkit/cache/instructioncache"
|
|
"github.com/moby/buildkit/client"
|
|
"github.com/moby/buildkit/session"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/progress"
|
|
digest "github.com/opencontainers/go-digest"
|
|
opentracing "github.com/opentracing/opentracing-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type jobKeyT string
|
|
|
|
var jobKey = jobKeyT("buildkit/solver/job")
|
|
|
|
type jobList struct {
|
|
mu sync.RWMutex
|
|
refs map[string]*job
|
|
updateCond *sync.Cond
|
|
actives map[digest.Digest]*state
|
|
}
|
|
|
|
type state struct {
|
|
jobs map[*job]*vertex
|
|
solver VertexSolver
|
|
mpw *progress.MultiWriter
|
|
}
|
|
|
|
func newJobList() *jobList {
|
|
jl := &jobList{
|
|
refs: make(map[string]*job),
|
|
actives: make(map[digest.Digest]*state),
|
|
}
|
|
jl.updateCond = sync.NewCond(jl.mu.RLocker())
|
|
return jl
|
|
}
|
|
|
|
// jobInstructionCache implements InstructionCache.
|
|
// jobInstructionCache is instantiated for each of job instances rather than jobList or solver instances.
|
|
// Lookup for objects with IgnoreCache fail until Set is called.
|
|
type jobInstructionCache struct {
|
|
mu sync.RWMutex
|
|
instructioncache.InstructionCache
|
|
ignoreCache map[digest.Digest]struct{}
|
|
setInThisJob map[digest.Digest]struct{}
|
|
}
|
|
|
|
// Probe implements InstructionCache
|
|
func (jic *jobInstructionCache) Probe(ctx context.Context, key digest.Digest) (bool, error) {
|
|
jic.mu.RLock()
|
|
defer jic.mu.RUnlock()
|
|
_, ignoreCache := jic.ignoreCache[key]
|
|
_, setInThisJob := jic.setInThisJob[key]
|
|
if ignoreCache && !setInThisJob {
|
|
return false, nil
|
|
}
|
|
return jic.InstructionCache.Probe(ctx, key)
|
|
}
|
|
|
|
// Lookup implements InstructionCache
|
|
func (jic *jobInstructionCache) Lookup(ctx context.Context, key digest.Digest, msg string) (interface{}, error) {
|
|
jic.mu.RLock()
|
|
defer jic.mu.RUnlock()
|
|
_, ignoreCache := jic.ignoreCache[key]
|
|
_, setInThisJob := jic.setInThisJob[key]
|
|
if ignoreCache && !setInThisJob {
|
|
return nil, nil
|
|
}
|
|
return jic.InstructionCache.Lookup(ctx, key, msg)
|
|
}
|
|
|
|
// Set implements InstructionCache
|
|
func (jic *jobInstructionCache) Set(key digest.Digest, ref interface{}) error {
|
|
jic.mu.Lock()
|
|
defer jic.mu.Unlock()
|
|
jic.setInThisJob[key] = struct{}{}
|
|
return jic.InstructionCache.Set(key, ref)
|
|
}
|
|
|
|
// SetIgnoreCache is jobInstructionCache-specific extension
|
|
func (jic *jobInstructionCache) SetIgnoreCache(key digest.Digest) {
|
|
jic.mu.Lock()
|
|
defer jic.mu.Unlock()
|
|
jic.ignoreCache[key] = struct{}{}
|
|
}
|
|
|
|
func newJobInstructionCache(base instructioncache.InstructionCache) *jobInstructionCache {
|
|
return &jobInstructionCache{
|
|
InstructionCache: base,
|
|
ignoreCache: make(map[digest.Digest]struct{}),
|
|
setInThisJob: make(map[digest.Digest]struct{}),
|
|
}
|
|
}
|
|
|
|
func (jl *jobList) new(ctx context.Context, id string, pr progress.Reader, cache instructioncache.InstructionCache) (context.Context, *job, error) {
|
|
jl.mu.Lock()
|
|
defer jl.mu.Unlock()
|
|
|
|
if _, ok := jl.refs[id]; ok {
|
|
return nil, nil, errors.Errorf("id %s exists", id)
|
|
}
|
|
|
|
pw, _, _ := progress.FromContext(ctx) // TODO: remove this
|
|
sid := session.FromContext(ctx)
|
|
span := opentracing.SpanFromContext(ctx)
|
|
|
|
// TODO(AkihiroSuda): find a way to integrate map[string]*cacheRecord to jobInstructionCache?
|
|
j := &job{l: jl, pr: progress.NewMultiReader(pr), pw: pw, session: sid, cache: newJobInstructionCache(cache), cached: map[string]*cacheRecord{}, traceSpan: span}
|
|
jl.refs[id] = j
|
|
jl.updateCond.Broadcast()
|
|
go func() {
|
|
<-ctx.Done()
|
|
jl.mu.Lock()
|
|
defer jl.mu.Unlock()
|
|
delete(jl.refs, id)
|
|
}()
|
|
|
|
return context.WithValue(ctx, jobKey, jl.refs[id]), jl.refs[id], nil
|
|
}
|
|
|
|
func (jl *jobList) get(id string) (*job, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
jl.updateCond.Broadcast()
|
|
}()
|
|
|
|
jl.mu.RLock()
|
|
defer jl.mu.RUnlock()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, errors.Errorf("no such job %s", id)
|
|
default:
|
|
}
|
|
j, ok := jl.refs[id]
|
|
if !ok {
|
|
jl.updateCond.Wait()
|
|
continue
|
|
}
|
|
return j, nil
|
|
}
|
|
}
|
|
|
|
type job struct {
|
|
l *jobList
|
|
pr *progress.MultiReader
|
|
pw progress.Writer
|
|
session string
|
|
cache *jobInstructionCache
|
|
cached map[string]*cacheRecord
|
|
|
|
traceSpan opentracing.Span // TODO(tonistiigi): temporary until shared tracers support. Do not change until solver refactoring in merged.
|
|
}
|
|
|
|
type cacheRecord struct {
|
|
VertexSolver
|
|
index Index
|
|
ref Ref
|
|
}
|
|
|
|
func (j *job) load(def *pb.Definition, resolveOp ResolveOpFunc) (*Input, error) {
|
|
j.l.mu.Lock()
|
|
defer j.l.mu.Unlock()
|
|
|
|
return j.loadInternal(def, resolveOp)
|
|
}
|
|
|
|
func (j *job) loadInternal(def *pb.Definition, resolveOp ResolveOpFunc) (*Input, error) {
|
|
vtx, idx, err := loadLLB(def, func(dgst digest.Digest, pbOp *pb.Op, load func(digest.Digest) (interface{}, error)) (interface{}, error) {
|
|
if st, ok := j.l.actives[dgst]; ok {
|
|
if vtx, ok := st.jobs[j]; ok {
|
|
return vtx, nil
|
|
}
|
|
}
|
|
opMetadata := def.Metadata[dgst]
|
|
vtx, err := newVertex(dgst, pbOp, &opMetadata, load)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
st, ok := j.l.actives[dgst]
|
|
if !ok {
|
|
st = &state{
|
|
jobs: map[*job]*vertex{},
|
|
mpw: progress.NewMultiWriter(progress.WithMetadata("vertex", dgst)),
|
|
}
|
|
op, err := resolveOp(vtx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx := progress.WithProgress(context.Background(), st.mpw)
|
|
ctx = session.NewContext(ctx, j.session) // TODO: support multiple
|
|
|
|
if j.traceSpan != nil {
|
|
ctx = opentracing.ContextWithSpan(ctx, j.traceSpan)
|
|
}
|
|
|
|
s, err := newVertexSolver(ctx, vtx, op, j.cache, j.getSolver)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i, input := range pbOp.Inputs {
|
|
if inputMetadata := def.Metadata[input.Digest]; inputMetadata.IgnoreCache {
|
|
k, err := s.CacheKey(ctx, Index(i))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
j.cache.SetIgnoreCache(k)
|
|
}
|
|
}
|
|
st.solver = s
|
|
|
|
j.l.actives[dgst] = st
|
|
}
|
|
if _, ok := st.jobs[j]; !ok {
|
|
j.pw.Write(vtx.Digest().String(), vtx.clientVertex)
|
|
st.mpw.Add(j.pw)
|
|
st.jobs[j] = vtx
|
|
}
|
|
return vtx, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Input{Vertex: vtx.(*vertex), Index: Index(idx)}, nil
|
|
}
|
|
|
|
func (j *job) discard() {
|
|
j.l.mu.Lock()
|
|
defer j.l.mu.Unlock()
|
|
|
|
j.pw.Close()
|
|
|
|
for k, st := range j.l.actives {
|
|
if _, ok := st.jobs[j]; ok {
|
|
delete(st.jobs, j)
|
|
}
|
|
if len(st.jobs) == 0 {
|
|
go st.solver.Release()
|
|
delete(j.l.actives, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (j *job) getSolver(dgst digest.Digest) (VertexSolver, error) {
|
|
st, ok := j.l.actives[dgst]
|
|
if !ok {
|
|
return nil, errors.Errorf("vertex %v not found", dgst)
|
|
}
|
|
return st.solver, nil
|
|
}
|
|
|
|
func (j *job) getRef(ctx context.Context, cv client.Vertex, index Index) (Ref, error) {
|
|
s, err := j.getSolver(cv.Digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ctx = progress.WithProgress(ctx, j.pw)
|
|
ref, err := getRef(ctx, s, cv, index, j.cache)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
j.keepCacheRef(s, index, ref)
|
|
return ref, nil
|
|
}
|
|
|
|
func (j *job) keepCacheRef(s VertexSolver, index Index, ref Ref) {
|
|
immutable, ok := ToImmutableRef(ref)
|
|
if ok {
|
|
j.cached[immutable.ID()] = &cacheRecord{s, index, ref}
|
|
}
|
|
}
|
|
|
|
func (j *job) cacheExporter(ref Ref) (CacheExporter, error) {
|
|
immutable, ok := ToImmutableRef(ref)
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid reference")
|
|
}
|
|
cr, ok := j.cached[immutable.ID()]
|
|
if !ok {
|
|
return nil, errors.Errorf("invalid cache exporter")
|
|
}
|
|
return cr.Cache(cr.index, cr.ref), nil
|
|
}
|
|
|
|
func getRef(ctx context.Context, s VertexSolver, cv client.Vertex, index Index, cache instructioncache.InstructionCache) (Ref, error) {
|
|
k, err := s.CacheKey(ctx, index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ref, err := cache.Lookup(ctx, k, s.(*vertexSolver).v.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ref != nil {
|
|
markCached(ctx, cv)
|
|
return ref.(Ref), nil
|
|
}
|
|
|
|
ev, err := s.OutputEvaluator(index)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer ev.Cancel()
|
|
|
|
for {
|
|
r, err := ev.Next(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if r.CacheKey != "" {
|
|
ref, err := cache.Lookup(ctx, r.CacheKey, s.(*vertexSolver).v.Name())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ref != nil {
|
|
markCached(ctx, cv)
|
|
return ref.(Ref), nil
|
|
}
|
|
continue
|
|
}
|
|
return r.Reference, nil
|
|
}
|
|
}
|
|
|
|
func (j *job) pipe(ctx context.Context, ch chan *client.SolveStatus) error {
|
|
vs := &vertexStream{cache: map[digest.Digest]*client.Vertex{}}
|
|
pr := j.pr.Reader(ctx)
|
|
defer func() {
|
|
if enc := vs.encore(); len(enc) > 0 {
|
|
ch <- &client.SolveStatus{Vertexes: enc}
|
|
}
|
|
}()
|
|
for {
|
|
p, err := pr.Read(ctx)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
ss := &client.SolveStatus{}
|
|
for _, p := range p {
|
|
switch v := p.Sys.(type) {
|
|
case client.Vertex:
|
|
ss.Vertexes = append(ss.Vertexes, vs.append(v)...)
|
|
|
|
case progress.Status:
|
|
vtx, ok := p.Meta("vertex")
|
|
if !ok {
|
|
logrus.Warnf("progress %s status without vertex info", p.ID)
|
|
continue
|
|
}
|
|
vs := &client.VertexStatus{
|
|
ID: p.ID,
|
|
Vertex: vtx.(digest.Digest),
|
|
Name: v.Action,
|
|
Total: int64(v.Total),
|
|
Current: int64(v.Current),
|
|
Timestamp: p.Timestamp,
|
|
Started: v.Started,
|
|
Completed: v.Completed,
|
|
}
|
|
ss.Statuses = append(ss.Statuses, vs)
|
|
case client.VertexLog:
|
|
vtx, ok := p.Meta("vertex")
|
|
if !ok {
|
|
logrus.Warnf("progress %s log without vertex info", p.ID)
|
|
continue
|
|
}
|
|
v.Vertex = vtx.(digest.Digest)
|
|
v.Timestamp = p.Timestamp
|
|
ss.Logs = append(ss.Logs, &v)
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case ch <- ss:
|
|
}
|
|
}
|
|
}
|
|
|
|
type vertexStream struct {
|
|
cache map[digest.Digest]*client.Vertex
|
|
}
|
|
|
|
func (vs *vertexStream) append(v client.Vertex) []*client.Vertex {
|
|
var out []*client.Vertex
|
|
vs.cache[v.Digest] = &v
|
|
if v.Cached {
|
|
for _, inp := range v.Inputs {
|
|
if inpv, ok := vs.cache[inp]; ok {
|
|
if !inpv.Cached && inpv.Completed == nil {
|
|
inpv.Cached = true
|
|
inpv.Started = v.Completed
|
|
inpv.Completed = v.Completed
|
|
}
|
|
delete(vs.cache, inp)
|
|
out = append(out, vs.append(*inpv)...)
|
|
}
|
|
}
|
|
}
|
|
vcopy := v
|
|
return append(out, &vcopy)
|
|
}
|
|
|
|
func (vs *vertexStream) encore() []*client.Vertex {
|
|
var out []*client.Vertex
|
|
for _, v := range vs.cache {
|
|
if v.Started != nil && v.Completed == nil {
|
|
now := time.Now()
|
|
v.Completed = &now
|
|
v.Error = context.Canceled.Error()
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|