github.com/decred/dcrlnd@v0.7.6/watchtower/wtclient/task_pipeline.go (about) 1 package wtclient 2 3 import ( 4 "container/list" 5 "sync" 6 "time" 7 8 "github.com/decred/slog" 9 ) 10 11 // taskPipeline implements a reliable, in-order queue that ensures its queue 12 // fully drained before exiting. Stopping the taskPipeline prevents the pipeline 13 // from accepting any further tasks, and will cause the pipeline to exit after 14 // all updates have been delivered to the downstream receiver. If this process 15 // hangs and is unable to make progress, users can optionally call ForceQuit to 16 // abandon the reliable draining of the queue in order to permit shutdown. 17 type taskPipeline struct { 18 started sync.Once 19 stopped sync.Once 20 forced sync.Once 21 22 log slog.Logger 23 24 queueMtx sync.Mutex 25 queueCond *sync.Cond 26 queue *list.List 27 28 newBackupTasks chan *backupTask 29 30 quit chan struct{} 31 forceQuit chan struct{} 32 shutdown chan struct{} 33 } 34 35 // newTaskPipeline initializes a new taskPipeline. 36 func newTaskPipeline(log slog.Logger) *taskPipeline { 37 rq := &taskPipeline{ 38 log: log, 39 queue: list.New(), 40 newBackupTasks: make(chan *backupTask), 41 quit: make(chan struct{}), 42 forceQuit: make(chan struct{}), 43 shutdown: make(chan struct{}), 44 } 45 rq.queueCond = sync.NewCond(&rq.queueMtx) 46 47 return rq 48 } 49 50 // Start spins up the taskPipeline, making it eligible to begin receiving backup 51 // tasks and deliver them to the receiver of NewBackupTasks. 52 func (q *taskPipeline) Start() { 53 q.started.Do(func() { 54 go q.queueManager() 55 }) 56 } 57 58 // Stop begins a graceful shutdown of the taskPipeline. This method returns once 59 // all backupTasks have been delivered via NewBackupTasks, or a ForceQuit causes 60 // the delivery of pending tasks to be interrupted. 61 func (q *taskPipeline) Stop() { 62 q.stopped.Do(func() { 63 q.log.Debugf("Stopping task pipeline") 64 65 close(q.quit) 66 q.signalUntilShutdown() 67 68 // Skip log if we also force quit. 69 select { 70 case <-q.forceQuit: 71 default: 72 q.log.Debugf("Task pipeline stopped successfully") 73 } 74 }) 75 } 76 77 // ForceQuit signals the taskPipeline to immediately exit, dropping any 78 // backupTasks that have not been delivered via NewBackupTasks. 79 func (q *taskPipeline) ForceQuit() { 80 q.forced.Do(func() { 81 q.log.Infof("Force quitting task pipeline") 82 83 close(q.forceQuit) 84 q.signalUntilShutdown() 85 86 q.log.Infof("Task pipeline unclean shutdown complete") 87 }) 88 } 89 90 // NewBackupTasks returns a read-only channel for enqueue backupTasks. The 91 // channel will be closed after a call to Stop and all pending tasks have been 92 // delivered, or if a call to ForceQuit is called before the pending entries 93 // have been drained. 94 func (q *taskPipeline) NewBackupTasks() <-chan *backupTask { 95 return q.newBackupTasks 96 } 97 98 // QueueBackupTask enqueues a backupTask for reliable delivery to the consumer 99 // of NewBackupTasks. If the taskPipeline is shutting down, ErrClientExiting is 100 // returned. Otherwise, if QueueBackupTask returns nil it is guaranteed to be 101 // delivered via NewBackupTasks unless ForceQuit is called before completion. 102 func (q *taskPipeline) QueueBackupTask(task *backupTask) error { 103 q.queueCond.L.Lock() 104 select { 105 106 // Reject new tasks after quit has been signaled. 107 case <-q.quit: 108 q.queueCond.L.Unlock() 109 return ErrClientExiting 110 111 // Reject new tasks after force quit has been signaled. 112 case <-q.forceQuit: 113 q.queueCond.L.Unlock() 114 return ErrClientExiting 115 116 default: 117 } 118 119 // Queue the new task and signal the queue's condition variable to wake up 120 // the queueManager for processing. 121 q.queue.PushBack(task) 122 q.queueCond.L.Unlock() 123 124 q.queueCond.Signal() 125 126 return nil 127 } 128 129 // queueManager processes all incoming backup requests that get added via 130 // QueueBackupTask. The manager will exit 131 // 132 // NOTE: This method MUST be run as a goroutine. 133 func (q *taskPipeline) queueManager() { 134 defer close(q.shutdown) 135 defer close(q.newBackupTasks) 136 137 for { 138 q.queueCond.L.Lock() 139 for q.queue.Front() == nil { 140 q.queueCond.Wait() 141 142 select { 143 case <-q.quit: 144 // Exit only after the queue has been fully drained. 145 if q.queue.Len() == 0 { 146 q.queueCond.L.Unlock() 147 q.log.Debugf("Revoked state pipeline flushed.") 148 return 149 } 150 151 case <-q.forceQuit: 152 q.queueCond.L.Unlock() 153 q.log.Debugf("Revoked state pipeline force quit.") 154 return 155 156 default: 157 } 158 } 159 160 // Pop the first element from the queue. 161 e := q.queue.Front() 162 task := q.queue.Remove(e).(*backupTask) 163 q.queueCond.L.Unlock() 164 165 select { 166 167 // Backup task submitted to dispatcher. We don't select on quit to 168 // ensure that we still drain tasks while shutting down. 169 case q.newBackupTasks <- task: 170 171 // Force quit, return immediately to allow the client to exit. 172 case <-q.forceQuit: 173 q.log.Debugf("Revoked state pipeline force quit.") 174 return 175 } 176 } 177 } 178 179 // signalUntilShutdown strobes the queue's condition variable to ensure the 180 // queueManager reliably unblocks to check for the exit condition. 181 func (q *taskPipeline) signalUntilShutdown() { 182 for { 183 select { 184 case <-time.After(time.Millisecond): 185 q.queueCond.Signal() 186 case <-q.shutdown: 187 return 188 } 189 } 190 }