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  }