本文是胡涛大佬所出版的《Kubernetes Operator 进阶开发》,强烈推荐各位阅读原书,本文仅仅留作个人心得,如有侵权立马删除。
1 源码解析
让我们首先看看 DelayingQueue
的定义:
go
type DelayingInterface interface {
Interface
// AddAfter adds an item to the workqueue after the indicated duration has passed
AddAfter(item interface{}, duration time.Duration)
}
也就是在 Interface
之上多加了一个 AddAfter
的方法用来处理延迟。 具体实现如下:
go
// delayingType wraps an Interface and provides delayed re-enquing
type delayingType struct {
Interface
// clock tracks time for delayed firing
clock clock.Clock
// stopCh lets us signal a shutdown to the waiting loop
stopCh chan struct{}
// stopOnce guarantees we only signal shutdown a single time
stopOnce sync.Once
// heartbeat ensures we wait no more than maxWait before firing
heartbeat clock.Ticker
// waitingForAddCh is a buffered channel that feeds waitingForAdd
waitingForAddCh chan *waitFor
// metrics counts the number of retries
metrics retryMetrics
}
然后个人觉得这个结构中最重要的应该是关注这个 waitingForAddCh chan *waitFor
这个字段,因此我们来追踪一下这个字段的作用。
go
// 首先什么时候往这个 chan 添加元素
func (q *delayingType) AddAfter(item interface{}, duration time.Duration) {
// don't add if we're already shutting down
if q.ShuttingDown() {
return
}
q.metrics.retry()
// immediately add things with no delay
if duration <= 0 {
q.Add(item)
return
}
select {
case <-q.stopCh:
// unblock if ShutDown() is called
case q.waitingForAddCh <- &waitFor{data: item, readyAt: q.clock.Now().Add(duration)}:
}
}
这个大写的 AddAfter()
说明了这个就是给用户调用的,专门用来添加元素的,也就是用户首先将需要执行的 item
放入这个 waitFor
里面。
然后我们再来看这个 waitFor
什么时候会被消费,消费的场景是在这个叫做 waitingLoop
中间:
go
func (q *delayingType) waitingLoop() {
defer utilruntime.HandleCrash()
// Make a placeholder channel to use when there are no items in our list
never := make(<-chan time.Time)
// Make a timer that expires when the item at the head of the waiting queue is ready
var nextReadyAtTimer clock.Timer
waitingForQueue := &waitForPriorityQueue{}
heap.Init(waitingForQueue)
waitingEntryByData := map[t]*waitFor{}
for {
if q.Interface.ShuttingDown() {
return
}
now := q.clock.Now()
// Add ready entries
for waitingForQueue.Len() > 0 {
entry := waitingForQueue.Peek().(*waitFor)
if entry.readyAt.After(now) {
break
}
entry = heap.Pop(waitingForQueue).(*waitFor)
q.Add(entry.data)
delete(waitingEntryByData, entry.data)
}
// Set up a wait for the first item's readyAt (if one exists)
nextReadyAt := never
if waitingForQueue.Len() > 0 {
if nextReadyAtTimer != nil {
nextReadyAtTimer.Stop()
}
entry := waitingForQueue.Peek().(*waitFor)
nextReadyAtTimer = q.clock.NewTimer(entry.readyAt.Sub(now))
nextReadyAt = nextReadyAtTimer.C()
}
select {
case <-q.stopCh:
return
case <-q.heartbeat.C():
// continue the loop, which will add ready items
case <-nextReadyAt:
// continue the loop, which will add ready items
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
drained := false
for !drained {
select {
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
default:
drained = true
}
}
}
}
}
我们先看看这个代码片段:
go
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
drained := false
for !drained {
select {
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
default:
drained = true
}
}
这个代码也很好理解,也就是如果当前的 task
还没有超时,那么就按照时间要求将其放入 waitForQueue
中间,否则就赶快放入 queue
中间,让下游去执行;至于其中的 drained
就表示要把当前 chan
中的 task
全部处理完。
那么这个 waitingForQueue
是什么呢?我们接着看代码:
go
// waitFor holds the data to add and the time it should be added
type waitFor struct {
data t
readyAt time.Time
// index in the priority queue (heap)
index int
}
从注释来看,这里说的是 waitFor
说明的具体需要添加的元素,以及需要添加的具体时间。
然后在下面就又实现了一个优先级队列:
go
type waitForPriorityQueue []*waitFor
也就是本质上就是一个按照时间先后实现的优先级队列。我们再来追踪一下这个结构体的数据的来源和去向,其中数据来源就是开始看到的片段,那么数据是如何被消费的呢?
go
// Add ready entries
for waitingForQueue.Len() > 0 {
entry := waitingForQueue.Peek().(*waitFor)
if entry.readyAt.After(now) {
break
}
entry = heap.Pop(waitingForQueue).(*waitFor)
q.Add(entry.data)
delete(waitingEntryByData, entry.data)
}
这里就是在 waitingLoop
中对这个数据的消费情况,其实也非常的简单,就是将 waitFor
中的所有时间到了的元素统统加到 Queue
中,从而让下游去执行。
2 使用场景
我们首先需要明白 DelayingQueue
的功能和场景:
- 功能:添加一个
item
并制定其在特定的时间再加入队列中 - 场景:在处理某一个
task
的时候发生了意外,因此需要过一段时间再去处理
然后我么来看看官方给出的测试例子:
go
func TestSimpleQueue(t *testing.T) {
fakeClock := testingclock.NewFakeClock(time.Now())
q := NewDelayingQueueWithConfig(DelayingQueueConfig{Clock: fakeClock})
first := "foo"
q.AddAfter(first, 50*time.Millisecond)
if err := waitForWaitingQueueToFill(q); err != nil {
t.Fatalf("unexpected err: %v", err)
}
if q.Len() != 0 {
t.Errorf("should not have added")
}
fakeClock.Step(60 * time.Millisecond)
if err := waitForAdded(q, 1); err != nil {
t.Errorf("should have added")
} else {
t.Logf("should have added, len(): %v ", q.Len())
}
item, _ := q.Get()
t.Logf("item: %v", item)
q.Done(item)
// step past the next heartbeat
fakeClock.Step(10 * time.Second)
err := wait.Poll(1*time.Millisecond, 30*time.Millisecond, func() (done bool, err error) {
if q.Len() > 0 {
return false, fmt.Errorf("added to queue")
}
return false, nil
})
if err != wait.ErrWaitTimeout {
t.Errorf("expected timeout, got: %v", err)
}
if q.Len() != 0 {
t.Errorf("should not have added")
}
}
其中需要注意的两个函数:这两个函数第一个是为了检查 waitingForAddCh
是否已经把元素交给了 waitingForQueue
(就是那个优先级队列);第二个函数则是为了检查是否已经在时间到了之后,是否从 waitingForQueue
中将数据给到了底层的 queue
,为什么要这么写呢,因为在测试中,程序猿使用的是FakeTime
而不是真实的time
,而且测试时间也很短,在源码中我们知道心跳时间是10s
。
go
func waitForWaitingQueueToFill(q DelayingInterface) error {
return wait.Poll(1*time.Millisecond, 10*time.Second, func() (done bool, err error) {
if len(q.(*delayingType).waitingForAddCh) == 0 {
return true, nil
}
return false, nil
})
}
func waitForAdded(q DelayingInterface, depth int) error {
return wait.Poll(1*time.Millisecond, 10*time.Second, func() (done bool, err error) {
if q.Len() == depth {
return true, nil
}
return false, nil
})
}
然后个人觉得这个
fakeClock
非常的有意思,以后写延迟相关的东西都可以用来用。
所以整个例子就显得非常的明了了。