深入理解Moby Buildkit系列 #24 - 神奇的Pipe

210 阅读3分钟

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

龙飞介绍完scheduler后,袁小白感觉是懂非懂。 说懂呢,因为大概了解到scheduler是如何将edge进行构建的,是如何拆解的,等等。 但要说到底是如何工作的,却又理不清,道不明。 这不,pipe就是其中之一。 在好奇心驱使下,袁小白开始分析起了pipe。

pipe使用场景之Receive

func TestPipe(t *testing.T) {
   t.Parallel()

   runCh := make(chan struct{})
   f := func(ctx context.Context) (interface{}, error) {
      select {
      case <-ctx.Done():
         return nil, ctx.Err()
      case <-runCh:
         return "res0", nil
      }
   }

   waitSignal := make(chan struct{}, 10)
   signalled := 0
   signal := func() {
      signalled++
      waitSignal <- struct{}{}
   }

   p, start := NewWithFunction(f)
   p.OnSendCompletion = signal
   go start()
   require.Equal(t, false, p.Receiver.Receive())

   st := p.Receiver.Status()
   require.Equal(t, st.Completed, false)
   require.Equal(t, st.Canceled, false)
   require.Nil(t, st.Value)
   require.Equal(t, signalled, 0)

   close(runCh)
   <-waitSignal

   p.Receiver.Receive()
   st = p.Receiver.Status()
   require.Equal(t, st.Completed, true)
   require.Equal(t, st.Canceled, false)
   require.NoError(t, st.Err)
   require.Equal(t, st.Value.(string), "res0")
}

可以看出如果我想通过pipe执行一个有阻塞的功能,如:

f := func(ctx context.Context) (interface{}, error) {
   select {
   case <-ctx.Done():
      return nil, ctx.Err()
   case <-runCh:
      return "res0", nil
   }
}

这种场景很常见,比如HTTP网络请求,要等到runCh收到消息才能返回结果。 我们在使用NewWithFunction创建pipe的同时,还返回了一个start函数,同时我们可以设置pipe的OnSendCompletion事件,这样我们就可以在send动作完成的时候,做我们想做的事了,这里想做的事是signal,让计数器signalled累加一,然后写入waitSignal通道信息。 可以理解为当send操作完成时,通过waitSignal信道,进行通知。

pipe执行go start(),Goroutine基本单元创建。 这时去检查pipe的Receiver的Status时,发现,并没有真正执行:

require.Equal(t, false, p.Receiver.Receive())

st := p.Receiver.Status()
require.Equal(t, st.Completed, false)
require.Equal(t, st.Canceled, false)
require.Nil(t, st.Value)
require.Equal(t, signalled, 0)

关闭runCh通道,触发了用户函数最终结果的返回。 但此时仍需等待Goroutine go start()真正的运行,用<-waitSignal进行等待。 等到pipe的OnSendCompletion被调用时,也就意味着收到了waitSignal信道的消息,可以继续执行下去了。 再来看对应的状态,也就发生了期待中的变化:

p.Receiver.Receive()
st = p.Receiver.Status()
require.Equal(t, st.Completed, true)
require.Equal(t, st.Canceled, false)
require.NoError(t, st.Err)
require.Equal(t, st.Value.(string), "res0")

pipe使用场景之Cancel

和Receiver正常工作不同的是,Cancel取消了请求:

p.Receiver.Cancel()
<-waitSignal

我们期待的结果,请求真正的被取消了:

p.Receiver.Receive()
st = p.Receiver.Status()
require.Equal(t, st.Completed, true)
require.Equal(t, st.Canceled, true)
require.Error(t, st.Err)
require.Equal(t, st.Err, context.Canceled)

通过管道pipe的使用场景,我们可以了解到设计初衷。 希望像linux上的管道一样,在一端输入请求,在另一端接收结果。 这里的请求可以是一个函数-f,并且可以是异步操作,在请求真正被发送的时候,我们有回调函数,可以让使用者接着处理或触发进一步的操作。 最后通过Receiver的状态,接收返回状态和返回结果,这个结果是真正的用户请求的结果。

再试想一下,如果多个管道连接在一起会是什么样? 比如总共有三个管道,彼此互相联接,前一个管理的结果想要传递给后一个时,可以让后一个管理持有前一个管道的Receiver,并在前一个管道的OnSendCompletion事件中注册触发事件,那这样是不是就可以把依赖顺序设定好,并且接计划执行了?!

那这个神奇的pipe到底是怎么实现的呢? 好奇心又让袁小白忍不住接着往下看了。 Pipe - v2.jpg

原来pipe是由这些组件组成的,那他们又是怎么互相协作,才能变出这么神奇的魔术的呢?

下一篇:深入理解Moby Buildkit系列 #25 - Pipe设计原理