github.com/decred/dcrlnd@v0.7.6/batch/scheduler.go (about)

     1  package batch
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	"github.com/decred/dcrlnd/kvdb"
     8  )
     9  
    10  // TimeScheduler is a batching engine that executes requests within a fixed
    11  // horizon. When the first request is received, a TimeScheduler waits a
    12  // configurable duration for other concurrent requests to join the batch. Once
    13  // this time has elapsed, the batch is closed and executed. Subsequent requests
    14  // are then added to a new batch which undergoes the same process.
    15  type TimeScheduler struct {
    16  	db       kvdb.Backend
    17  	locker   sync.Locker
    18  	duration time.Duration
    19  
    20  	mu sync.Mutex
    21  	b  *batch
    22  }
    23  
    24  // NewTimeScheduler initializes a new TimeScheduler with a fixed duration at
    25  // which to schedule batches. If the operation needs to modify a higher-level
    26  // cache, the cache's lock should be provided to so that external consistency
    27  // can be maintained, as successful db operations will cause a request's
    28  // OnCommit method to be executed while holding this lock.
    29  func NewTimeScheduler(db kvdb.Backend, locker sync.Locker,
    30  	duration time.Duration) *TimeScheduler {
    31  
    32  	return &TimeScheduler{
    33  		db:       db,
    34  		locker:   locker,
    35  		duration: duration,
    36  	}
    37  }
    38  
    39  // Execute schedules the provided request for batch execution along with other
    40  // concurrent requests. The request will be executed within a fixed horizon,
    41  // parameterizeed by the duration of the scheduler. The error from the
    42  // underlying operation is returned to the caller.
    43  //
    44  // NOTE: Part of the Scheduler interface.
    45  func (s *TimeScheduler) Execute(r *Request) error {
    46  	req := request{
    47  		Request: r,
    48  		errChan: make(chan error, 1),
    49  	}
    50  
    51  	// Add the request to the current batch. If the batch has been cleared
    52  	// or no batch exists, create a new one.
    53  	s.mu.Lock()
    54  	if s.b == nil {
    55  		s.b = &batch{
    56  			db:     s.db,
    57  			clear:  s.clear,
    58  			locker: s.locker,
    59  		}
    60  		time.AfterFunc(s.duration, s.b.trigger)
    61  	}
    62  	s.b.reqs = append(s.b.reqs, &req)
    63  
    64  	// If this is a non-lazy request, we'll execute the batch immediately.
    65  	if !r.lazy {
    66  		go s.b.trigger()
    67  	}
    68  
    69  	s.mu.Unlock()
    70  
    71  	// Wait for the batch to process the request. If the batch didn't
    72  	// ask us to execute the request individually, simply return the error.
    73  	err := <-req.errChan
    74  	if err != errSolo {
    75  		return err
    76  	}
    77  
    78  	// Obtain exclusive access to the cache if this scheduler needs to
    79  	// modify the cache in OnCommit.
    80  	if s.locker != nil {
    81  		s.locker.Lock()
    82  		defer s.locker.Unlock()
    83  	}
    84  
    85  	// Otherwise, run the request on its own.
    86  	commitErr := kvdb.Update(s.db, req.Update, func() {
    87  		if req.Reset != nil {
    88  			req.Reset()
    89  		}
    90  	})
    91  
    92  	// Finally, return the commit error directly or execute the OnCommit
    93  	// closure with the commit error if present.
    94  	if req.OnCommit != nil {
    95  		return req.OnCommit(commitErr)
    96  	}
    97  
    98  	return commitErr
    99  }
   100  
   101  // clear resets the scheduler's batch to nil so that no more requests can be
   102  // added.
   103  func (s *TimeScheduler) clear(b *batch) {
   104  	s.mu.Lock()
   105  	if s.b == b {
   106  		s.b = nil
   107  	}
   108  	s.mu.Unlock()
   109  }