这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战
scheduler.build
现在有两个Edge,Scheduler会怎么编排任务呢? Scheduler开始构建的地方:
// build evaluates edge into a result
func (s *scheduler) build(ctx context.Context, edge Edge) (CachedResult, error) {
s.mu.Lock()
e := s.ef.getEdge(edge)
...
wait := make(chan struct{})
var p *pipe.Pipe
p = s.newPipe(e, nil, pipe.Request{Payload: &edgeRequest{desiredState: edgeStatusComplete}})
p.OnSendCompletion = func() {
p.Receiver.Receive()
if p.Receiver.Status().Completed {
close(wait)
}
}
...
<-wait
...
}
可以看到,最先获取edge,然后创建EdgePipe来处理Edge,这里的onSendCompletion里最终要完成的任务就是close(wait),就是说我们这是最后一个Edge,当OnSendCompletion完成后,也就意味着这一次的构建全部完成。
scheduler newPipe
再来看看newPipe:
// newPipe creates a new request pipe between two edges
func (s *scheduler) newPipe(target, from *edge, req pipe.Request) *pipe.Pipe {
p := &edgePipe{
Pipe: pipe.New(req),
Target: target,
From: from,
}
s.signal(target)
if from != nil {
p.OnSendCompletion = func() {
p.mu.Lock()
defer p.mu.Unlock()
s.signal(p.From)
}
s.outgoing[from] = append(s.outgoing[from], p)
}
s.incoming[target] = append(s.incoming[target], p)
p.OnReceiveCompletion = func() {
p.mu.Lock()
defer p.mu.Unlock()
s.signal(p.Target)
}
return p.Pipe
}
从参数可以看出,有两个edge传入,一个是target,一个是from,也就是第一个是待构建的edge,而from则是来源于哪个edge构建的请求,从build里的调用可以看出,from为nil,也就是第一构建的edge就是最后一个edge。
当from不为空时,OnSendCompletion会调用s.signal(p.From),发送from也就是源的信号。
OnReceiveCompletion则会发送target的信号。
同时将from,target分别添加到s.outgoign, s.incoming数组中。
scheduler.signal
// signal notifies that an edge needs to be processed again
func (s *scheduler) signal(e *edge) {
s.muQ.Lock()
if _, ok := s.waitq[e]; !ok {
d := &dispatcher{e: e}
if s.last == nil {
s.next = d
} else {
s.last.next = d
}
s.last = d
s.waitq[e] = struct{}{}
s.cond.Signal()
}
s.muQ.Unlock()
}
根据信号edge创建新的&dispatcher,加入到s.last,并触发信号。 那发出的信号被谁接收到了,又做了什么样的处理呢?
func (s *scheduler) loop() {
defer func() {
close(s.closed)
}()
go func() {
<-s.stopped
s.mu.Lock()
s.cond.Signal()
s.mu.Unlock()
}()
s.mu.Lock()
for {
select {
case <-s.stopped:
s.mu.Unlock()
return
default:
}
s.muQ.Lock()
l := s.next
if l != nil {
if l == s.last {
s.last = nil
}
s.next = l.next
delete(s.waitq, l.e)
}
s.muQ.Unlock()
if l == nil {
s.cond.Wait()
continue
}
s.dispatch(l.e)
}
}
记得scheduler在被jobs初始化的时候,就触发了这个loop,会一直监听最新的dispatch事件,最终分发事件s.dispatch(l.e):
// dispatch schedules an edge to be processed
func (s *scheduler) dispatch(e *edge) {
...
pf := &pipeFactory{s: s, e: e}
// unpark the edge
e.unpark(inc, updates, out, pf)
...
}
也就是在分发edge的时候,调用了e.unpark,这就是真正处理edge的地方:
func (e *edge) unpark(incoming []pipe.Sender, updates, allPipes []pipe.Receiver, f *pipeFactory) {
...
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
}
...
if e.execReq == nil {
if added := e.createInputRequests(desiredState, f, false); !added && !e.hasActiveOutgoing && !cacheMapReq {
bklog.G(context.TODO()).Errorf("buildkit scheluding error: leaving incoming open. forcing solve. Please report this with BUILDKIT_SCHEDULER_DEBUG=1")
debugSchedulerPreUnpark(e, incoming, updates, allPipes)
e.createInputRequests(desiredState, f, true)
}
}
}
在这里:
desiredState, done := e.respondToIncoming(incoming, allPipes),判断edge构建是否完成e.cacheMapReq = f.NewFuncRequest(...),创建获取op.CacheMap的操作,可以看到这里又创建了新的pipeadded := e.createInputRequests(...),如果是可执行的Op,要看看依赖有没有准备好,也就是自己的Inputs,又会创建新的pipe。
这一环环的梳理下来后,如何更直观的理解呢?
- 从Edge0开始构建,创建了一个只关心
close(wait)的pipe0 - 进入到
unpark后,会检查自己Op的CacheMap,也就是这里的第二步,创建的pipe1,pipe1是从pipe0来,所以发送完成的时候会发送edge0信号,这样再次分发的dispatch事件就可以让edge0的pipe获取所需要内容,看看是否已经完成 - edge0是有inputs()的,也就是edge1,所以也要为edge1创建构建事件,创建了pipe2,也就是步骤3
- 在edge1开始构建后,和edge1一样,先看看自己的Op是否在CacheMap中,创建了pipe3,也就是第4步,但这里pipe3发送完成的时候是要通知edge1的
可以看出这也是一个递归,而pipe的作用就是将大家都关联起来,可以触发回调事件,这里是s.dispatch,s.loop就像事件总线,负责监听新事件,并发送。
既然是递归,那就有了口,那edge的出口又在哪儿呢,是怎么判断自己完成了呢?
还记得s.incoming, s.outgoing数组吗?
里面存储了所有的pipe,而edge递归构建的出口就在:
desiredState, done := e.respondToIncoming(incoming, allPipes)
在这里和e0相关的pipe有p0, p1, p2,也就是说这三个pipe都完成后,e0才算构建完成。 同理,和e1相关的有p2, p3,只有p2, p3都完成了才算构建完成。
原来如此! Scheduler真是一位好管家,将所有的事情管理的有条不紊。