mirror of
https://github.com/moby/buildkit.git
synced 2026-08-04 06:40:22 +00:00
Many of the functions in the solver that operated on the edges were very large which obscured their usage and the overall flow of the scheduler. This change refactors those functions into separate smaller functions to make it easier to follow the overall flow of the scheduler. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
1062 lines
29 KiB
Go
1062 lines
29 KiB
Go
package solver
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/moby/buildkit/solver/internal/pipe"
|
|
"github.com/moby/buildkit/util/bklog"
|
|
digest "github.com/opencontainers/go-digest"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type edgeStatusType int
|
|
|
|
const (
|
|
edgeStatusInitial edgeStatusType = iota
|
|
edgeStatusCacheFast
|
|
edgeStatusCacheSlow
|
|
edgeStatusComplete
|
|
)
|
|
|
|
func (t edgeStatusType) String() string {
|
|
return []string{"initial", "cache-fast", "cache-slow", "complete"}[t]
|
|
}
|
|
|
|
func newEdge(ed Edge, op activeOp, index *edgeIndex) *edge {
|
|
e := &edge{
|
|
edge: ed,
|
|
op: op,
|
|
depRequests: map[pipeReceiver]*dep{},
|
|
keyMap: map[string]struct{}{},
|
|
cacheRecords: map[string]*CacheRecord{},
|
|
cacheRecordsLoaded: map[string]struct{}{},
|
|
index: index,
|
|
}
|
|
e.debug = debugSchedulerCheckEdge(e)
|
|
return e
|
|
}
|
|
|
|
type edge struct {
|
|
edge Edge
|
|
op activeOp
|
|
|
|
edgeState
|
|
depRequests map[pipeReceiver]*dep
|
|
deps []*dep
|
|
|
|
cacheMapReq pipeReceiver
|
|
cacheMapDone bool
|
|
cacheMapIndex int
|
|
cacheMapDigests []digest.Digest
|
|
execReq pipeReceiver
|
|
execCacheLoad bool
|
|
err error
|
|
cacheRecords map[string]*CacheRecord
|
|
cacheRecordsLoaded map[string]struct{}
|
|
keyMap map[string]struct{}
|
|
|
|
noCacheMatchPossible bool
|
|
allDepsCompletedCacheFast bool
|
|
allDepsCompletedCacheSlow bool
|
|
allDepsStateCacheSlow bool
|
|
allDepsCompleted bool
|
|
hasActiveOutgoing bool
|
|
|
|
releaserCount int
|
|
owner *edge
|
|
keysDidChange bool
|
|
index *edgeIndex
|
|
|
|
secondaryExporters []expDep
|
|
|
|
failedOnce sync.Once
|
|
debug bool
|
|
}
|
|
|
|
// dep holds state for a dependant edge
|
|
type dep struct {
|
|
req pipeReceiver
|
|
edgeState
|
|
index Index
|
|
keyMap map[string]*CacheKey
|
|
slowCacheReq pipeReceiver
|
|
slowCacheComplete bool
|
|
slowCacheFoundKey bool
|
|
slowCacheKey *ExportableCacheKey
|
|
err error
|
|
}
|
|
|
|
// expDep holds secondary exporter info for dependency
|
|
type expDep struct {
|
|
index int
|
|
cacheKey CacheKeyWithSelector
|
|
}
|
|
|
|
func newDep(i Index) *dep {
|
|
return &dep{index: i, keyMap: map[string]*CacheKey{}}
|
|
}
|
|
|
|
// edgePipe is a pipe for requests between two edges
|
|
type edgePipe struct {
|
|
*pipe.Pipe[*edgeRequest, any]
|
|
From, Target *edge
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// edgeState hold basic mutable state info for an edge
|
|
type edgeState struct {
|
|
state edgeStatusType
|
|
result *SharedCachedResult
|
|
cacheMap *CacheMap
|
|
keys []ExportableCacheKey
|
|
}
|
|
|
|
type edgeRequest struct {
|
|
desiredState edgeStatusType
|
|
currentState edgeState
|
|
currentKeys int
|
|
}
|
|
|
|
// takeOwnership increases the number of times release needs to be
|
|
// called to release the edge. Called on merging edges.
|
|
func (e *edge) takeOwnership(old *edge) {
|
|
e.releaserCount += old.releaserCount + 1
|
|
old.owner = e
|
|
old.releaseResult()
|
|
}
|
|
|
|
// release releases the edge resources
|
|
func (e *edge) release() {
|
|
if e.releaserCount > 0 {
|
|
e.releaserCount--
|
|
return
|
|
}
|
|
e.releaseResult()
|
|
}
|
|
|
|
func (e *edge) releaseResult() {
|
|
e.index.Release(e)
|
|
if e.result != nil {
|
|
go e.result.Release(context.TODO())
|
|
}
|
|
}
|
|
|
|
// commitOptions returns parameters for the op execution
|
|
func (e *edge) commitOptions() ([]*CacheKey, []CachedResult) {
|
|
k := NewCacheKey(e.cacheMap.Digest, e.edge.Vertex.Digest(), e.edge.Index)
|
|
if len(e.deps) == 0 {
|
|
keys := make([]*CacheKey, 0, len(e.cacheMapDigests))
|
|
for _, dgst := range e.cacheMapDigests {
|
|
keys = append(keys, NewCacheKey(dgst, e.edge.Vertex.Digest(), e.edge.Index))
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
inputs := make([][]CacheKeyWithSelector, len(e.deps))
|
|
results := make([]CachedResult, len(e.deps))
|
|
for i, dep := range e.deps {
|
|
for _, k := range dep.result.CacheKeys() {
|
|
inputs[i] = append(inputs[i], CacheKeyWithSelector{CacheKey: k, Selector: e.cacheMap.Deps[i].Selector})
|
|
}
|
|
if dep.slowCacheKey != nil {
|
|
inputs[i] = append(inputs[i], CacheKeyWithSelector{CacheKey: *dep.slowCacheKey})
|
|
}
|
|
results[i] = dep.result
|
|
}
|
|
|
|
k.deps = inputs
|
|
return []*CacheKey{k}, results
|
|
}
|
|
|
|
// isComplete returns true if edge state is final and will never change
|
|
func (e *edge) isComplete() bool {
|
|
return e.err != nil || e.result != nil
|
|
}
|
|
|
|
// finishIncoming finalizes the incoming pipe request
|
|
func (e *edge) finishIncoming(req pipeSender) {
|
|
err := e.err
|
|
if req.Request().Canceled && err == nil {
|
|
err = context.Canceled
|
|
}
|
|
debugSchedulerFinishIncoming(e, err, req)
|
|
req.Finalize(&e.edgeState, err)
|
|
}
|
|
|
|
// updateIncoming updates the current value of incoming pipe request
|
|
func (e *edge) updateIncoming(req pipeSender) {
|
|
debugSchedulerUpdateIncoming(e, req)
|
|
req.Update(&e.edgeState)
|
|
}
|
|
|
|
// probeCache is called with unprocessed cache keys for dependency
|
|
// if the key could match the edge, the cacheRecords for dependency are filled
|
|
func (e *edge) probeCache(d *dep, depKeys []CacheKeyWithSelector) bool {
|
|
if len(depKeys) == 0 {
|
|
return false
|
|
}
|
|
if e.op.IgnoreCache() {
|
|
return false
|
|
}
|
|
keys, err := e.op.Cache().Query(depKeys, d.index, e.cacheMap.Digest, e.edge.Index)
|
|
if err != nil {
|
|
e.err = errors.Wrap(err, "error on cache query")
|
|
}
|
|
found := false
|
|
for _, k := range keys {
|
|
k.vtx = e.edge.Vertex.Digest()
|
|
if _, ok := d.keyMap[k.ID]; !ok {
|
|
d.keyMap[k.ID] = k
|
|
found = true
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
// checkDepMatchPossible checks if any cache matches are possible past this point
|
|
func (e *edge) checkDepMatchPossible(dep *dep) {
|
|
depHasSlowCache := e.cacheMap.Deps[dep.index].ComputeDigestFunc != nil
|
|
if !e.noCacheMatchPossible && (((!dep.slowCacheFoundKey && dep.slowCacheComplete && depHasSlowCache) || (!depHasSlowCache && dep.state >= edgeStatusCacheSlow)) && len(dep.keyMap) == 0) {
|
|
e.noCacheMatchPossible = true
|
|
}
|
|
}
|
|
|
|
// slowCacheFunc returns the result based cache func for dependency if it exists
|
|
func (e *edge) slowCacheFunc(dep *dep) ResultBasedCacheFunc {
|
|
if e.cacheMap == nil {
|
|
return nil
|
|
}
|
|
return e.cacheMap.Deps[int(dep.index)].ComputeDigestFunc
|
|
}
|
|
|
|
// preprocessFunc returns result based cache func
|
|
func (e *edge) preprocessFunc(dep *dep) PreprocessFunc {
|
|
if e.cacheMap == nil {
|
|
return nil
|
|
}
|
|
return e.cacheMap.Deps[int(dep.index)].PreprocessFunc
|
|
}
|
|
|
|
// allDepsHaveKeys checks if all dependencies have at least one key. used for
|
|
// determining if there is enough data for combining cache key for edge
|
|
func (e *edge) allDepsHaveKeys(matching bool) bool {
|
|
if e.cacheMap == nil {
|
|
return false
|
|
}
|
|
for _, d := range e.deps {
|
|
cond := len(d.keys) == 0
|
|
if matching {
|
|
cond = len(d.keyMap) == 0
|
|
}
|
|
if cond && d.slowCacheKey == nil && d.result == nil {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// depKeys returns all current dependency cache keys
|
|
func (e *edge) currentIndexKey() *CacheKey {
|
|
if e.cacheMap == nil {
|
|
return nil
|
|
}
|
|
|
|
keys := make([][]CacheKeyWithSelector, len(e.deps))
|
|
for i, d := range e.deps {
|
|
if len(d.keys) == 0 && d.result == nil {
|
|
return nil
|
|
}
|
|
for _, k := range d.keys {
|
|
keys[i] = append(keys[i], CacheKeyWithSelector{Selector: e.cacheMap.Deps[i].Selector, CacheKey: k})
|
|
}
|
|
if d.result != nil {
|
|
for _, rk := range d.result.CacheKeys() {
|
|
keys[i] = append(keys[i], CacheKeyWithSelector{Selector: e.cacheMap.Deps[i].Selector, CacheKey: rk})
|
|
}
|
|
if d.slowCacheKey != nil {
|
|
keys[i] = append(keys[i], CacheKeyWithSelector{CacheKey: ExportableCacheKey{CacheKey: d.slowCacheKey.CacheKey, Exporter: &exporter{k: d.slowCacheKey.CacheKey}}})
|
|
}
|
|
}
|
|
}
|
|
|
|
k := NewCacheKey(e.cacheMap.Digest, e.edge.Vertex.Digest(), e.edge.Index)
|
|
k.deps = keys
|
|
|
|
return k
|
|
}
|
|
|
|
// slow cache keys can be computed in 2 phases if there are multiple deps.
|
|
// first evaluate ones that didn't match any definition based keys
|
|
func (e *edge) skipPhase2SlowCache(dep *dep) bool {
|
|
isPhase1 := false
|
|
for _, dep := range e.deps {
|
|
if (!dep.slowCacheComplete && e.slowCacheFunc(dep) != nil || dep.state < edgeStatusCacheSlow) && len(dep.keyMap) == 0 {
|
|
isPhase1 = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isPhase1 && !dep.slowCacheComplete && e.slowCacheFunc(dep) != nil && len(dep.keyMap) > 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (e *edge) skipPhase2FastCache(dep *dep) bool {
|
|
isPhase1 := false
|
|
for _, dep := range e.deps {
|
|
if e.cacheMap == nil || len(dep.keyMap) == 0 && ((!dep.slowCacheComplete && e.slowCacheFunc(dep) != nil) || (dep.state < edgeStatusComplete && e.slowCacheFunc(dep) == nil)) {
|
|
isPhase1 = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if isPhase1 && len(dep.keyMap) > 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// unpark is called by the scheduler with incoming requests and updates for
|
|
// previous calls.
|
|
// To avoid deadlocks and resource leaks this function needs to follow
|
|
// following rules:
|
|
// 1. this function needs to return unclosed outgoing requests if some incoming
|
|
// requests were not completed
|
|
// 2. this function may not return outgoing requests if it has completed all
|
|
// incoming requests
|
|
func (e *edge) unpark(incoming []pipeSender, updates, allPipes []pipeReceiver, f *pipeFactory) {
|
|
// process all incoming changes
|
|
e.processUpdates(updates)
|
|
|
|
desiredState, done := e.respondToIncoming(incoming, allPipes)
|
|
if done {
|
|
return
|
|
}
|
|
|
|
cacheMapReq := false
|
|
// set up new outgoing requests if needed
|
|
if e.cacheMapReq == nil && (e.cacheMap == nil || len(e.cacheRecords) == 0) {
|
|
index := e.cacheMapIndex
|
|
e.cacheMapReq = f.NewFuncRequest(func(ctx context.Context) (interface{}, error) {
|
|
cm, err := e.op.CacheMap(ctx, index)
|
|
return cm, errors.Wrap(err, "failed to load cache key")
|
|
})
|
|
cacheMapReq = true
|
|
}
|
|
|
|
// execute op
|
|
if e.execReq == nil && desiredState == edgeStatusComplete {
|
|
if ok := e.execIfPossible(f); ok {
|
|
return
|
|
}
|
|
}
|
|
|
|
if e.execReq == nil {
|
|
if added := e.createInputRequests(desiredState, f, false); !added && !e.hasActiveOutgoing && !cacheMapReq {
|
|
bklog.G(context.TODO()).Errorf("buildkit scheduling error: leaving incoming open. forcing solve. Please report this with BUILDKIT_SCHEDULER_DEBUG=1")
|
|
debugSchedulerPreUnpark(e, incoming, updates, allPipes)
|
|
e.createInputRequests(desiredState, f, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *edge) makeExportable(k *CacheKey, records []*CacheRecord) ExportableCacheKey {
|
|
return ExportableCacheKey{
|
|
CacheKey: k,
|
|
Exporter: &exporter{k: k, records: records, override: e.edge.Vertex.Options().ExportCache},
|
|
}
|
|
}
|
|
|
|
func (e *edge) markFailed(f *pipeFactory, err error) {
|
|
e.err = err
|
|
e.failedOnce.Do(func() {
|
|
e.postpone(f)
|
|
})
|
|
}
|
|
|
|
func (e *edge) processUpdates(updates []pipe.Receiver[*edgeRequest, any]) {
|
|
depChanged := false
|
|
for _, upt := range updates {
|
|
depChanged = e.processUpdate(upt) || depChanged
|
|
}
|
|
|
|
if depChanged {
|
|
e.recalcCurrentState()
|
|
}
|
|
}
|
|
|
|
// processUpdate is called by unpark for every updated pipe request
|
|
func (e *edge) processUpdate(upt pipeReceiver) (depChanged bool) {
|
|
// response for cachemap request
|
|
if upt == e.cacheMapReq && upt.Status().Completed {
|
|
e.processCacheMapReq()
|
|
return true
|
|
}
|
|
|
|
// response for exec request
|
|
if upt == e.execReq && upt.Status().Completed {
|
|
e.processExecReq()
|
|
return true
|
|
}
|
|
|
|
// response for requests to dependencies
|
|
if dep, ok := e.depRequests[upt]; ok {
|
|
return e.processDepReq(dep)
|
|
}
|
|
|
|
// response for result based cache function
|
|
for i, dep := range e.deps {
|
|
if upt == dep.slowCacheReq && upt.Status().Completed {
|
|
e.processDepSlowCacheReq(i, dep)
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// recalcCurrentState is called by unpark to recompute internal state after
|
|
// the state of dependencies has changed
|
|
func (e *edge) recalcCurrentState() {
|
|
// TODO: fast pass to detect incomplete results
|
|
newKeys := map[string]*CacheKey{}
|
|
|
|
for i, dep := range e.deps {
|
|
if i == 0 {
|
|
for id, k := range dep.keyMap {
|
|
if _, ok := e.keyMap[id]; ok {
|
|
continue
|
|
}
|
|
newKeys[id] = k
|
|
}
|
|
} else {
|
|
for id := range newKeys {
|
|
if _, ok := dep.keyMap[id]; !ok {
|
|
delete(newKeys, id)
|
|
}
|
|
}
|
|
}
|
|
if len(newKeys) == 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
for key := range newKeys {
|
|
e.keyMap[key] = struct{}{}
|
|
}
|
|
|
|
for _, r := range newKeys {
|
|
// TODO: add all deps automatically
|
|
mergedKey := r.clone()
|
|
mergedKey.deps = make([][]CacheKeyWithSelector, len(e.deps))
|
|
for i, dep := range e.deps {
|
|
if dep.result != nil {
|
|
for _, dk := range dep.result.CacheKeys() {
|
|
mergedKey.deps[i] = append(mergedKey.deps[i], CacheKeyWithSelector{Selector: e.cacheMap.Deps[i].Selector, CacheKey: dk})
|
|
}
|
|
if dep.slowCacheKey != nil {
|
|
mergedKey.deps[i] = append(mergedKey.deps[i], CacheKeyWithSelector{CacheKey: *dep.slowCacheKey})
|
|
}
|
|
} else {
|
|
for _, k := range dep.keys {
|
|
mergedKey.deps[i] = append(mergedKey.deps[i], CacheKeyWithSelector{Selector: e.cacheMap.Deps[i].Selector, CacheKey: k})
|
|
}
|
|
}
|
|
}
|
|
|
|
records, err := e.op.Cache().Records(context.Background(), mergedKey)
|
|
if err != nil {
|
|
bklog.G(context.TODO()).Errorf("error receiving cache records: %v", err)
|
|
continue
|
|
}
|
|
|
|
for _, r := range records {
|
|
if _, ok := e.cacheRecordsLoaded[r.ID]; !ok {
|
|
e.cacheRecords[r.ID] = r
|
|
}
|
|
}
|
|
|
|
e.keys = append(e.keys, e.makeExportable(mergedKey, records))
|
|
}
|
|
|
|
// detect lower/upper bound for current state
|
|
allDepsCompletedCacheFast := e.cacheMap != nil
|
|
allDepsCompletedCacheSlow := e.cacheMap != nil
|
|
allDepsStateCacheSlow := true
|
|
allDepsCompleted := true
|
|
stLow := edgeStatusInitial // minimal possible state
|
|
stHigh := edgeStatusCacheSlow // maximum possible state
|
|
if e.cacheMap != nil {
|
|
for _, dep := range e.deps {
|
|
isSlowCacheIncomplete := e.slowCacheFunc(dep) != nil && (dep.state == edgeStatusCacheSlow || (dep.state == edgeStatusComplete && !dep.slowCacheComplete))
|
|
isSlowIncomplete := (e.slowCacheFunc(dep) != nil || e.preprocessFunc(dep) != nil) && (dep.state == edgeStatusCacheSlow || (dep.state == edgeStatusComplete && !dep.slowCacheComplete))
|
|
|
|
if dep.state > stLow && len(dep.keyMap) == 0 && !isSlowIncomplete {
|
|
stLow = dep.state
|
|
if stLow > edgeStatusCacheSlow {
|
|
stLow = edgeStatusCacheSlow
|
|
}
|
|
}
|
|
effectiveState := dep.state
|
|
if dep.state == edgeStatusCacheSlow && isSlowCacheIncomplete {
|
|
effectiveState = edgeStatusCacheFast
|
|
}
|
|
if dep.state == edgeStatusComplete && isSlowCacheIncomplete {
|
|
effectiveState = edgeStatusCacheFast
|
|
}
|
|
if effectiveState < stHigh {
|
|
stHigh = effectiveState
|
|
}
|
|
if isSlowIncomplete || dep.state < edgeStatusComplete {
|
|
allDepsCompleted = false
|
|
}
|
|
if dep.state < edgeStatusCacheFast {
|
|
allDepsCompletedCacheFast = false
|
|
}
|
|
if isSlowCacheIncomplete || dep.state < edgeStatusCacheSlow {
|
|
allDepsCompletedCacheSlow = false
|
|
}
|
|
if dep.state < edgeStatusCacheSlow && len(dep.keyMap) == 0 {
|
|
allDepsStateCacheSlow = false
|
|
}
|
|
}
|
|
if stLow > e.state {
|
|
e.state = stLow
|
|
}
|
|
if stHigh > e.state {
|
|
e.state = stHigh
|
|
}
|
|
if !e.cacheMapDone && len(e.keys) == 0 {
|
|
e.state = edgeStatusInitial
|
|
}
|
|
|
|
e.allDepsCompletedCacheFast = e.cacheMapDone && allDepsCompletedCacheFast
|
|
e.allDepsCompletedCacheSlow = e.cacheMapDone && allDepsCompletedCacheSlow
|
|
e.allDepsStateCacheSlow = e.cacheMapDone && allDepsStateCacheSlow
|
|
e.allDepsCompleted = e.cacheMapDone && allDepsCompleted
|
|
|
|
if e.allDepsStateCacheSlow && len(e.cacheRecords) > 0 && e.state == edgeStatusCacheFast {
|
|
openKeys := map[string]struct{}{}
|
|
for _, dep := range e.deps {
|
|
isSlowIncomplete := e.slowCacheFunc(dep) != nil && (dep.state == edgeStatusCacheSlow || (dep.state == edgeStatusComplete && !dep.slowCacheComplete))
|
|
if !isSlowIncomplete {
|
|
openDepKeys := map[string]struct{}{}
|
|
for key := range dep.keyMap {
|
|
if _, ok := e.keyMap[key]; !ok {
|
|
openDepKeys[key] = struct{}{}
|
|
}
|
|
}
|
|
if len(openKeys) != 0 {
|
|
for k := range openKeys {
|
|
if _, ok := openDepKeys[k]; !ok {
|
|
delete(openKeys, k)
|
|
}
|
|
}
|
|
} else {
|
|
openKeys = openDepKeys
|
|
}
|
|
if len(openKeys) == 0 {
|
|
e.state = edgeStatusCacheSlow
|
|
debugSchedulerUpgradeCacheSlow(e)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *edge) processCacheMapReq() {
|
|
upt := e.cacheMapReq
|
|
if err := upt.Status().Err; err != nil {
|
|
e.cacheMapReq = nil
|
|
if !upt.Status().Canceled && e.err == nil {
|
|
e.err = err
|
|
}
|
|
return
|
|
}
|
|
|
|
resp := upt.Status().Value.(*cacheMapResp)
|
|
e.cacheMap = resp.CacheMap
|
|
e.cacheMapDone = resp.complete
|
|
e.cacheMapIndex++
|
|
if len(e.deps) == 0 {
|
|
e.cacheMapDigests = append(e.cacheMapDigests, e.cacheMap.Digest)
|
|
if !e.op.IgnoreCache() {
|
|
keys, err := e.op.Cache().Query(nil, 0, e.cacheMap.Digest, e.edge.Index)
|
|
if err != nil {
|
|
bklog.G(context.TODO()).Error(errors.Wrap(err, "invalid query response")) // make the build fail for this error
|
|
} else {
|
|
for _, k := range keys {
|
|
k.vtx = e.edge.Vertex.Digest()
|
|
records, err := e.op.Cache().Records(context.Background(), k)
|
|
if err != nil {
|
|
bklog.G(context.TODO()).Errorf("error receiving cache records: %v", err)
|
|
continue
|
|
}
|
|
|
|
for _, r := range records {
|
|
e.cacheRecords[r.ID] = r
|
|
}
|
|
|
|
e.keys = append(e.keys, e.makeExportable(k, records))
|
|
}
|
|
}
|
|
}
|
|
e.state = edgeStatusCacheSlow
|
|
}
|
|
if e.allDepsHaveKeys(false) {
|
|
e.keysDidChange = true
|
|
}
|
|
// probe keys that were loaded before cache map
|
|
for i, dep := range e.deps {
|
|
e.probeCache(dep, withSelector(dep.keys, e.cacheMap.Deps[i].Selector))
|
|
e.checkDepMatchPossible(dep)
|
|
}
|
|
if !e.cacheMapDone {
|
|
e.cacheMapReq = nil
|
|
}
|
|
}
|
|
|
|
func (e *edge) processExecReq() {
|
|
upt := e.execReq
|
|
if err := upt.Status().Err; err != nil {
|
|
e.execReq = nil
|
|
if e.execCacheLoad {
|
|
for k := range e.cacheRecordsLoaded {
|
|
delete(e.cacheRecords, k)
|
|
}
|
|
} else if !upt.Status().Canceled && e.err == nil {
|
|
e.err = err
|
|
}
|
|
return
|
|
}
|
|
|
|
e.result = NewSharedCachedResult(upt.Status().Value.(CachedResult))
|
|
e.state = edgeStatusComplete
|
|
}
|
|
|
|
func (e *edge) processDepReq(dep *dep) (depChanged bool) {
|
|
upt := dep.req
|
|
if err := upt.Status().Err; !upt.Status().Canceled && upt.Status().Completed && err != nil {
|
|
if e.err == nil {
|
|
e.err = err
|
|
}
|
|
dep.err = err
|
|
}
|
|
|
|
if upt.Status().Value == nil {
|
|
return false
|
|
}
|
|
|
|
state, isEdgeState := upt.Status().Value.(*edgeState)
|
|
if !isEdgeState {
|
|
bklog.G(context.TODO()).Warnf("invalid edgeState value for update: %T", state)
|
|
return false
|
|
}
|
|
|
|
if len(dep.keys) < len(state.keys) {
|
|
newKeys := state.keys[len(dep.keys):]
|
|
if e.cacheMap != nil {
|
|
e.probeCache(dep, withSelector(newKeys, e.cacheMap.Deps[dep.index].Selector))
|
|
dep.edgeState.keys = state.keys
|
|
if e.allDepsHaveKeys(false) {
|
|
e.keysDidChange = true
|
|
}
|
|
}
|
|
depChanged = true
|
|
}
|
|
|
|
if dep.state != edgeStatusComplete && state.state == edgeStatusComplete {
|
|
e.keysDidChange = true
|
|
}
|
|
|
|
recheck := state.state != dep.state
|
|
|
|
dep.edgeState = *state
|
|
|
|
if recheck && e.cacheMap != nil {
|
|
e.checkDepMatchPossible(dep)
|
|
depChanged = true
|
|
}
|
|
return depChanged
|
|
}
|
|
|
|
func (e *edge) processDepSlowCacheReq(index int, dep *dep) {
|
|
upt := dep.slowCacheReq
|
|
if err := upt.Status().Err; err != nil {
|
|
dep.slowCacheReq = nil
|
|
if !upt.Status().Canceled && e.err == nil {
|
|
e.err = upt.Status().Err
|
|
}
|
|
} else if !dep.slowCacheComplete {
|
|
dgst := upt.Status().Value.(digest.Digest)
|
|
if e.cacheMap.Deps[int(dep.index)].ComputeDigestFunc != nil && dgst != "" {
|
|
k := NewCacheKey(dgst, "", -1)
|
|
dep.slowCacheKey = &ExportableCacheKey{CacheKey: k, Exporter: &exporter{k: k}}
|
|
slowKeyExp := CacheKeyWithSelector{CacheKey: *dep.slowCacheKey}
|
|
defKeys := make([]CacheKeyWithSelector, 0, len(dep.result.CacheKeys()))
|
|
for _, dk := range dep.result.CacheKeys() {
|
|
defKeys = append(defKeys, CacheKeyWithSelector{CacheKey: dk, Selector: e.cacheMap.Deps[index].Selector})
|
|
}
|
|
dep.slowCacheFoundKey = e.probeCache(dep, []CacheKeyWithSelector{slowKeyExp})
|
|
|
|
// connect def key to slow key
|
|
e.op.Cache().Query(append(defKeys, slowKeyExp), dep.index, e.cacheMap.Digest, e.edge.Index)
|
|
}
|
|
|
|
dep.slowCacheComplete = true
|
|
e.keysDidChange = true
|
|
e.checkDepMatchPossible(dep) // not matching key here doesn't set nocachematch possible to true
|
|
}
|
|
}
|
|
|
|
// respondToIncoming responds to all incoming requests. completing or
|
|
// updating them when possible
|
|
func (e *edge) respondToIncoming(incoming []pipeSender, allPipes []pipeReceiver) (edgeStatusType, bool) {
|
|
// detect the result state for the requests
|
|
allIncomingCanComplete := true
|
|
desiredState := e.state
|
|
allCanceled := true
|
|
|
|
// check incoming requests
|
|
// check if all requests can be either answered or canceled
|
|
if !e.isComplete() {
|
|
for _, req := range incoming {
|
|
if !req.Request().Canceled {
|
|
allCanceled = false
|
|
if r := req.Request().Payload; desiredState < r.desiredState {
|
|
desiredState = r.desiredState
|
|
if e.hasActiveOutgoing || r.desiredState == edgeStatusComplete || r.currentKeys == len(e.keys) {
|
|
allIncomingCanComplete = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// do not set allIncomingCanComplete if active ongoing can modify the state
|
|
if !allCanceled && e.state < edgeStatusComplete && len(e.keys) == 0 && e.hasActiveOutgoing {
|
|
allIncomingCanComplete = false
|
|
}
|
|
|
|
debugSchedulerRespondToIncomingStatus(e, allIncomingCanComplete)
|
|
|
|
if allIncomingCanComplete && e.hasActiveOutgoing {
|
|
// cancel all current requests
|
|
for _, p := range allPipes {
|
|
p.Cancel()
|
|
}
|
|
|
|
// can close all but one requests
|
|
var leaveOpen pipeSender
|
|
for _, req := range incoming {
|
|
if !req.Request().Canceled {
|
|
leaveOpen = req
|
|
break
|
|
}
|
|
}
|
|
for _, req := range incoming {
|
|
if leaveOpen == nil || leaveOpen == req {
|
|
leaveOpen = req
|
|
continue
|
|
}
|
|
e.finishIncoming(req)
|
|
}
|
|
return desiredState, true
|
|
}
|
|
|
|
// can complete, finish and return
|
|
if allIncomingCanComplete && !e.hasActiveOutgoing {
|
|
for _, req := range incoming {
|
|
e.finishIncoming(req)
|
|
}
|
|
return desiredState, true
|
|
}
|
|
|
|
// update incoming based on current state
|
|
for _, req := range incoming {
|
|
r := req.Request().Payload
|
|
if req.Request().Canceled {
|
|
e.finishIncoming(req)
|
|
} else if !e.hasActiveOutgoing && e.state >= r.desiredState {
|
|
e.finishIncoming(req)
|
|
} else if !isEqualState(r.currentState, e.edgeState) && !req.Request().Canceled {
|
|
e.updateIncoming(req)
|
|
}
|
|
}
|
|
return desiredState, false
|
|
}
|
|
|
|
// createInputRequests creates new requests for dependencies or async functions
|
|
// that need to complete to continue processing the edge
|
|
func (e *edge) createInputRequests(desiredState edgeStatusType, f *pipeFactory, force bool) (addedNew bool) {
|
|
// initialize deps state
|
|
e.ensureDepsInitialized()
|
|
|
|
// cycle all dependencies. set up outgoing requests if needed
|
|
for _, dep := range e.deps {
|
|
desiredStateDep := e.desiredStateDep(dep, desiredState, force)
|
|
|
|
// outgoing request is needed
|
|
if dep.state < desiredStateDep {
|
|
addedNew = e.createOutgoingRequest(dep, desiredStateDep, f) || addedNew
|
|
} else {
|
|
debugSchedulerSkipInputRequestBasedOnDepState(e, dep, desiredStateDep)
|
|
}
|
|
|
|
// initialize function to compute cache key based on dependency result
|
|
addedNew = e.computeCacheKeyFromDep(dep, f) || addedNew
|
|
}
|
|
return addedNew
|
|
}
|
|
|
|
func (e *edge) ensureDepsInitialized() {
|
|
if e.deps != nil {
|
|
return
|
|
}
|
|
|
|
e.depRequests = make(map[pipeReceiver]*dep)
|
|
e.deps = make([]*dep, 0, len(e.edge.Vertex.Inputs()))
|
|
for i := range e.edge.Vertex.Inputs() {
|
|
e.deps = append(e.deps, newDep(Index(i)))
|
|
}
|
|
}
|
|
|
|
func (e *edge) desiredStateDep(dep *dep, desiredState edgeStatusType, force bool) edgeStatusType {
|
|
if e.noCacheMatchPossible || force {
|
|
return edgeStatusComplete
|
|
}
|
|
|
|
if dep.state == edgeStatusInitial && desiredState > dep.state {
|
|
return edgeStatusCacheFast
|
|
}
|
|
|
|
if dep.state == edgeStatusCacheFast && desiredState > dep.state {
|
|
// wait all deps to complete cache fast before continuing with slow cache
|
|
if (e.allDepsCompletedCacheFast && len(e.keys) == 0) || len(dep.keyMap) == 0 || e.allDepsHaveKeys(true) {
|
|
if !e.skipPhase2FastCache(dep) && e.cacheMap != nil {
|
|
return edgeStatusCacheSlow
|
|
}
|
|
}
|
|
return dep.state
|
|
}
|
|
|
|
if e.cacheMap != nil && dep.state == edgeStatusCacheSlow && desiredState == edgeStatusComplete {
|
|
// if all deps have completed cache-slow or content based cache for input is available
|
|
if (len(dep.keyMap) == 0 || e.allDepsCompletedCacheSlow || (!e.skipPhase2FastCache(dep) && e.slowCacheFunc(dep) != nil)) && (len(e.cacheRecords) == 0) {
|
|
if len(dep.keyMap) == 0 || !e.skipPhase2SlowCache(dep) {
|
|
return edgeStatusComplete
|
|
}
|
|
}
|
|
return dep.state
|
|
}
|
|
|
|
if e.cacheMap != nil && dep.state == edgeStatusCacheSlow && e.slowCacheFunc(dep) != nil && desiredState == edgeStatusCacheSlow {
|
|
if len(dep.keyMap) == 0 || !e.skipPhase2SlowCache(dep) {
|
|
return edgeStatusComplete
|
|
}
|
|
return dep.state
|
|
}
|
|
|
|
return dep.state
|
|
}
|
|
|
|
func (e *edge) createOutgoingRequest(dep *dep, desiredStateDep edgeStatusType, f *pipeFactory) (addedNew bool) {
|
|
if dep.req != nil && !dep.req.Status().Completed {
|
|
if dep.req.Request().desiredState == desiredStateDep {
|
|
debugSchedulerSkipInputRequestBasedOnExistingRequest(e, dep, desiredStateDep)
|
|
return false
|
|
}
|
|
debugSchedulerCancelInputRequest(e, dep, desiredStateDep)
|
|
dep.req.Cancel()
|
|
}
|
|
|
|
debugSchedulerAddInputRequest(e, dep, desiredStateDep)
|
|
req := f.NewInputRequest(e.edge.Vertex.Inputs()[int(dep.index)], &edgeRequest{
|
|
currentState: dep.edgeState,
|
|
desiredState: desiredStateDep,
|
|
currentKeys: len(dep.keys),
|
|
})
|
|
e.depRequests[req] = dep
|
|
dep.req = req
|
|
return true
|
|
}
|
|
|
|
func (e *edge) computeCacheKeyFromDep(dep *dep, f *pipeFactory) (addedNew bool) {
|
|
if dep.state != edgeStatusComplete || dep.slowCacheReq != nil || e.cacheMap == nil {
|
|
return false
|
|
}
|
|
|
|
pfn := e.preprocessFunc(dep)
|
|
fn := e.slowCacheFunc(dep)
|
|
if pfn == nil && fn == nil {
|
|
return false
|
|
}
|
|
|
|
res := dep.result
|
|
index := dep.index
|
|
dep.slowCacheReq = f.NewFuncRequest(func(ctx context.Context) (interface{}, error) {
|
|
v, err := e.op.CalcSlowCache(ctx, index, pfn, fn, res)
|
|
return v, errors.Wrap(err, "failed to compute cache key")
|
|
})
|
|
return true
|
|
}
|
|
|
|
// execIfPossible creates a request for getting the edge result if there is
|
|
// enough state
|
|
func (e *edge) execIfPossible(f *pipeFactory) bool {
|
|
if len(e.cacheRecords) > 0 {
|
|
if e.keysDidChange {
|
|
e.postpone(f)
|
|
return true
|
|
}
|
|
e.execReq = f.NewFuncRequest(e.loadCache)
|
|
e.execCacheLoad = true
|
|
for req := range e.depRequests {
|
|
req.Cancel()
|
|
}
|
|
return true
|
|
} else if e.allDepsCompleted {
|
|
if e.keysDidChange {
|
|
e.postpone(f)
|
|
return true
|
|
}
|
|
e.execReq = f.NewFuncRequest(e.execOp)
|
|
e.execCacheLoad = false
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// postpone delays exec to next unpark invocation if we have unprocessed keys
|
|
func (e *edge) postpone(f *pipeFactory) {
|
|
f.NewFuncRequest(func(context.Context) (interface{}, error) {
|
|
return nil, nil
|
|
})
|
|
}
|
|
|
|
// loadCache creates a request to load edge result from cache
|
|
func (e *edge) loadCache(ctx context.Context) (interface{}, error) {
|
|
recs := make([]*CacheRecord, 0, len(e.cacheRecords))
|
|
for _, r := range e.cacheRecords {
|
|
recs = append(recs, r)
|
|
}
|
|
|
|
rec := getBestResult(recs)
|
|
e.cacheRecordsLoaded[rec.ID] = struct{}{}
|
|
|
|
bklog.G(ctx).Debugf("load cache for %s with %s", e.edge.Vertex.Name(), rec.ID)
|
|
res, err := e.op.LoadCache(ctx, rec)
|
|
if err != nil {
|
|
bklog.G(ctx).Debugf("load cache for %s err: %v", e.edge.Vertex.Name(), err)
|
|
return nil, errors.Wrap(err, "failed to load cache")
|
|
}
|
|
|
|
return NewCachedResult(res, []ExportableCacheKey{{CacheKey: rec.key, Exporter: &exporter{k: rec.key, record: rec, edge: e}}}), nil
|
|
}
|
|
|
|
// execOp creates a request to execute the vertex operation
|
|
func (e *edge) execOp(ctx context.Context) (interface{}, error) {
|
|
cacheKeys, inputs := e.commitOptions()
|
|
results, subExporters, err := e.op.Exec(ctx, toResultSlice(inputs))
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
index := e.edge.Index
|
|
if len(results) <= int(index) {
|
|
return nil, errors.Errorf("invalid response from exec need %d index but %d results received", index, len(results))
|
|
}
|
|
|
|
res := results[int(index)]
|
|
|
|
for i := range results {
|
|
if i != int(index) {
|
|
go results[i].Release(context.TODO())
|
|
}
|
|
}
|
|
|
|
var exporters []CacheExporter
|
|
|
|
for _, cacheKey := range cacheKeys {
|
|
ck, err := e.op.Cache().Save(cacheKey, res, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if exp, ok := ck.Exporter.(*exporter); ok {
|
|
exp.edge = e
|
|
}
|
|
|
|
exps := make([]CacheExporter, 0, len(subExporters))
|
|
for _, exp := range subExporters {
|
|
exps = append(exps, exp.Exporter)
|
|
}
|
|
|
|
exporters = append(exporters, ck.Exporter)
|
|
exporters = append(exporters, exps...)
|
|
}
|
|
|
|
ek := make([]ExportableCacheKey, 0, len(cacheKeys))
|
|
for _, ck := range cacheKeys {
|
|
ek = append(ek, ExportableCacheKey{
|
|
CacheKey: ck,
|
|
Exporter: &mergedExporter{exporters: exporters},
|
|
})
|
|
}
|
|
|
|
return NewCachedResult(res, ek), nil
|
|
}
|
|
|
|
func (e *edge) isDep(e2 *edge) bool {
|
|
return isDep(e.edge.Vertex, e2.edge.Vertex)
|
|
}
|
|
|
|
func isDep(vtx, vtx2 Vertex) bool {
|
|
if vtx.Digest() == vtx2.Digest() {
|
|
return true
|
|
}
|
|
for _, e := range vtx.Inputs() {
|
|
if isDep(e.Vertex, vtx2) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func toResultSlice(cres []CachedResult) (out []Result) {
|
|
out = make([]Result, len(cres))
|
|
for i := range cres {
|
|
out[i] = cres[i].(Result)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isEqualState(s1, s2 edgeState) bool {
|
|
if s1.state != s2.state || s1.result != s2.result || s1.cacheMap != s2.cacheMap || len(s1.keys) != len(s2.keys) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func withSelector(keys []ExportableCacheKey, selector digest.Digest) []CacheKeyWithSelector {
|
|
out := make([]CacheKeyWithSelector, len(keys))
|
|
for i, k := range keys {
|
|
out[i] = CacheKeyWithSelector{Selector: selector, CacheKey: k}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type (
|
|
pipeRequest = pipe.Request[*edgeRequest]
|
|
pipeSender = pipe.Sender[*edgeRequest, any]
|
|
pipeReceiver = pipe.Receiver[*edgeRequest, any]
|
|
)
|
|
|
|
func newPipe(req pipeRequest) *pipe.Pipe[*edgeRequest, any] {
|
|
return pipe.New[*edgeRequest, any](req)
|
|
}
|