github.com/blend/go-sdk@v1.20220411.3/async/latch.go (about)

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