go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/async/latch.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package async
     9  
    10  import (
    11  	"errors"
    12  	"sync"
    13  	"sync/atomic"
    14  )
    15  
    16  // NewLatch creates a new latch.
    17  //
    18  // It is _highly_ recommended to use this constructor.
    19  func NewLatch() *Latch {
    20  	l := new(Latch)
    21  	l.Reset()
    22  	return l
    23  }
    24  
    25  // Latch states
    26  //
    27  // They are int32 to support atomic Load/Store calls.
    28  const (
    29  	LatchStopped  int32 = 0
    30  	LatchStarting int32 = 1
    31  	LatchResuming int32 = 2
    32  	LatchStarted  int32 = 3
    33  	LatchActive   int32 = 4
    34  	LatchPausing  int32 = 5
    35  	LatchPaused   int32 = 6
    36  	LatchStopping int32 = 7
    37  )
    38  
    39  // Errors
    40  var (
    41  	ErrCannotStart               = errors.New("cannot start; already started")
    42  	ErrCannotStop                = errors.New("cannot stop; already stopped")
    43  	ErrCannotCancel              = errors.New("cannot cancel; already canceled")
    44  	ErrCannotStartActionRequired = errors.New("cannot start; action is required")
    45  )
    46  
    47  /*
    48  Latch is a helper to coordinate goroutine lifecycles, specifically waiting for goroutines to start and end.
    49  
    50  The lifecycle is generally as follows:
    51  
    52  	0 - stopped - goto 1
    53  	1 - starting - goto 2
    54  	2 - started - goto 3
    55  	3 - stopping - goto 0
    56  
    57  Control flow is coordinated with chan struct{}, which acts as a semaphore but can only
    58  alert (1) listener as it is buffered.
    59  
    60  In order to start a `stopped` latch, you must call `.Reset()` first to initialize channels.
    61  */
    62  type Latch struct {
    63  	sync.Mutex
    64  
    65  	state int32
    66  
    67  	starting chan struct{}
    68  	started  chan struct{}
    69  	stopping chan struct{}
    70  	stopped  chan struct{}
    71  }
    72  
    73  // Reset resets the latch.
    74  func (l *Latch) Reset() {
    75  	l.Lock()
    76  	atomic.StoreInt32(&l.state, LatchStopped)
    77  	l.starting = make(chan struct{}, 1)
    78  	l.started = make(chan struct{}, 1)
    79  	l.stopping = make(chan struct{}, 1)
    80  	l.stopped = make(chan struct{}, 1)
    81  	l.Unlock()
    82  }
    83  
    84  // CanStart returns if the latch can start.
    85  func (l *Latch) CanStart() bool {
    86  	return atomic.LoadInt32(&l.state) == LatchStopped
    87  }
    88  
    89  // CanStop returns if the latch can stop.
    90  func (l *Latch) CanStop() bool {
    91  	return atomic.LoadInt32(&l.state) == LatchStarted
    92  }
    93  
    94  // IsStarting returns if the latch state is LatchStarting
    95  func (l *Latch) IsStarting() bool {
    96  	return atomic.LoadInt32(&l.state) == LatchStarting
    97  }
    98  
    99  // IsStarted returns if the latch state is LatchStarted.
   100  func (l *Latch) IsStarted() bool {
   101  	return atomic.LoadInt32(&l.state) == LatchStarted
   102  }
   103  
   104  // IsStopping returns if the latch state is LatchStopping.
   105  func (l *Latch) IsStopping() bool {
   106  	return atomic.LoadInt32(&l.state) == LatchStopping
   107  }
   108  
   109  // IsStopped returns if the latch state is LatchStopped.
   110  func (l *Latch) IsStopped() bool {
   111  	return atomic.LoadInt32(&l.state) == LatchStopped
   112  }
   113  
   114  // NotifyStarting returns the starting signal.
   115  // It is used to coordinate the transition from stopped -> starting.
   116  // There can only be (1) effective listener at a time for these events.
   117  func (l *Latch) NotifyStarting() (notifyStarting <-chan struct{}) {
   118  	l.Lock()
   119  	notifyStarting = l.starting
   120  	l.Unlock()
   121  	return
   122  }
   123  
   124  // NotifyStarted returns the started signal.
   125  // It is used to coordinate the transition from starting -> started.
   126  // There can only be (1) effective listener at a time for these events.
   127  func (l *Latch) NotifyStarted() (notifyStarted <-chan struct{}) {
   128  	l.Lock()
   129  	notifyStarted = l.started
   130  	l.Unlock()
   131  	return
   132  }
   133  
   134  // NotifyStopping returns the should stop signal.
   135  // It is used to trigger the transition from running -> stopping -> stopped.
   136  // There can only be (1) effective listener at a time for these events.
   137  func (l *Latch) NotifyStopping() (notifyStopping <-chan struct{}) {
   138  	l.Lock()
   139  	notifyStopping = l.stopping
   140  	l.Unlock()
   141  	return
   142  }
   143  
   144  // NotifyStopped returns the stopped signal.
   145  // It is used to coordinate the transition from stopping -> stopped.
   146  // There can only be (1) effective listener at a time for these events.
   147  func (l *Latch) NotifyStopped() (notifyStopped <-chan struct{}) {
   148  	l.Lock()
   149  	notifyStopped = l.stopped
   150  	l.Unlock()
   151  	return
   152  }
   153  
   154  // Starting signals the latch is starting.
   155  // This is typically done before you kick off a goroutine.
   156  func (l *Latch) Starting() {
   157  	if l.IsStarting() {
   158  		return
   159  	}
   160  	atomic.StoreInt32(&l.state, LatchStarting)
   161  	l.starting <- struct{}{}
   162  }
   163  
   164  // Started signals that the latch is started and has entered
   165  // the `IsStarted` state.
   166  func (l *Latch) Started() {
   167  	if l.IsStarted() {
   168  		return
   169  	}
   170  	atomic.StoreInt32(&l.state, LatchStarted)
   171  	l.started <- struct{}{}
   172  }
   173  
   174  // Stopping signals the latch to stop.
   175  // It could also be thought of as `SignalStopping`.
   176  func (l *Latch) Stopping() {
   177  	if l.IsStopping() {
   178  		return
   179  	}
   180  	atomic.StoreInt32(&l.state, LatchStopping)
   181  	l.stopping <- struct{}{}
   182  }
   183  
   184  // Stopped signals the latch has stopped.
   185  func (l *Latch) Stopped() {
   186  	if l.IsStopped() {
   187  		return
   188  	}
   189  	atomic.StoreInt32(&l.state, LatchStopped)
   190  	l.stopped <- struct{}{}
   191  }
   192  
   193  // WaitStarted triggers `Starting` and waits for the `Started` signal.
   194  func (l *Latch) WaitStarted() {
   195  	if !l.CanStart() {
   196  		return
   197  	}
   198  	started := l.NotifyStarted()
   199  	l.Starting()
   200  	<-started
   201  }
   202  
   203  // WaitStopped triggers `Stopping` and waits for the `Stopped` signal.
   204  func (l *Latch) WaitStopped() {
   205  	if !l.CanStop() {
   206  		return
   207  	}
   208  	stopped := l.NotifyStopped()
   209  	l.Stopping()
   210  	<-stopped
   211  }