decred.org/dcrdex@v1.0.5/dex/runner.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package dex
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"sync"
    11  	"sync/atomic"
    12  )
    13  
    14  // contextManager is used to manage a context and its cancellation function.
    15  type contextManager struct {
    16  	mtx    sync.RWMutex
    17  	cancel context.CancelFunc
    18  	ctx    context.Context
    19  }
    20  
    21  // init uses the passed context to create and save a child context and
    22  // its cancellation function.
    23  func (cm *contextManager) init(ctx context.Context) {
    24  	cm.mtx.Lock()
    25  	defer cm.mtx.Unlock()
    26  	cm.ctx, cm.cancel = context.WithCancel(ctx)
    27  }
    28  
    29  // On will be true until the context is canceled.
    30  func (cm *contextManager) On() bool {
    31  	cm.mtx.RLock()
    32  	defer cm.mtx.RUnlock()
    33  	return cm.ctx != nil && cm.ctx.Err() == nil
    34  }
    35  
    36  // Runner is satisfied by DEX subsystems, which must start any of their
    37  // goroutines via the Run method.
    38  type Runner interface {
    39  	Run(ctx context.Context)
    40  	// Ready() <-chan struct{}
    41  }
    42  
    43  // StartStopWaiter wraps a Runner, providing the non-blocking Start and Stop
    44  // methods, and the blocking WaitForShutdown method.
    45  type StartStopWaiter struct {
    46  	contextManager
    47  	wg     sync.WaitGroup
    48  	runner Runner
    49  }
    50  
    51  // NewStartStopWaiter creates a StartStopWaiter from a Runner.
    52  func NewStartStopWaiter(runner Runner) *StartStopWaiter {
    53  	return &StartStopWaiter{
    54  		runner: runner,
    55  	}
    56  }
    57  
    58  // Start launches the Runner in a goroutine. Start will return immediately. Use
    59  // Stop to signal the Runner to stop, followed by WaitForShutdown to allow
    60  // shutdown to complete.
    61  func (ssw *StartStopWaiter) Start(ctx context.Context) {
    62  	ssw.init(ctx)
    63  	ssw.wg.Add(1)
    64  	go func() {
    65  		ssw.runner.Run(ssw.ctx)
    66  		ssw.cancel() // in case it stopped on its own
    67  		ssw.wg.Done()
    68  	}()
    69  	// TODO: do <-ssw.runner.Ready()
    70  }
    71  
    72  // WaitForShutdown blocks until the Runner has returned in response to Stop.
    73  func (ssw *StartStopWaiter) WaitForShutdown() {
    74  	ssw.wg.Wait()
    75  }
    76  
    77  // Stop cancels the context.
    78  func (ssw *StartStopWaiter) Stop() {
    79  	ssw.mtx.RLock()
    80  	ssw.cancel()
    81  	ssw.mtx.RUnlock()
    82  }
    83  
    84  // Connector is any type that implements the Connect method, which will return
    85  // a connection error, and a WaitGroup that can be waited on at Disconnection.
    86  type Connector interface {
    87  	Connect(ctx context.Context) (*sync.WaitGroup, error)
    88  }
    89  
    90  // ConnectionMaster manages a Connector.
    91  type ConnectionMaster struct {
    92  	connector Connector
    93  	cancel    context.CancelFunc
    94  	done      atomic.Value // chan struct{}
    95  }
    96  
    97  // NewConnectionMaster creates a new ConnectionMaster. The Connect method should
    98  // be used before Disconnect. The On, Done, and Wait methods may be used at any
    99  // time. However, prior to Connect, Wait and Done immediately return and signal
   100  // completion, respectively.
   101  func NewConnectionMaster(c Connector) *ConnectionMaster {
   102  	return &ConnectionMaster{
   103  		connector: c,
   104  	}
   105  }
   106  
   107  // Connect connects the Connector, and returns any initial connection error. Use
   108  // Disconnect to shut down the Connector. Even if Connect returns a non-nil
   109  // error, On may report true until Disconnect is called. You would use Connect
   110  // if the wrapped Connector has a reconnect loop to continually attempt to
   111  // establish a connection even if the initial attempt fails. Use ConnectOnce if
   112  // the Connector should be given one chance to connect before being considered
   113  // not to be "on". If the ConnectionMaster is discarded on error, it is not
   114  // important which method is used.
   115  func (c *ConnectionMaster) Connect(ctx context.Context) (err error) {
   116  	if c.On() { // probably a bug in the consumer
   117  		return errors.New("already running")
   118  	}
   119  
   120  	// Attempt to start the Connector.
   121  	ctx, cancel := context.WithCancel(ctx)
   122  	wg, err := c.connector.Connect(ctx)
   123  	if err != nil {
   124  		cancel() // no context leak
   125  		return fmt.Errorf("connect failure: %w", err)
   126  	}
   127  	// NOTE: A non-nil error currently does not indicate that the Connector is
   128  	// not running, only that the initial connection attempt has failed. As long
   129  	// as the WaitGroup is non-nil we need to wait on it. We return the error so
   130  	// that the caller may decide to stop it or wait (see ConnectOnce).
   131  
   132  	// It's running, enable Disconnect.
   133  	c.cancel = cancel // caller should synchronize Connect/Disconnect calls
   134  
   135  	// Done and On may be checked at any time.
   136  	done := make(chan struct{})
   137  	c.done.Store(done)
   138  
   139  	go func() { // capture the local variables
   140  		wg.Wait()
   141  		cancel() // if the Connector just died on its own, don't leak the context
   142  		close(done)
   143  	}()
   144  
   145  	return err
   146  }
   147  
   148  // ConnectOnce is like Connect, but on error the internal status is updated so
   149  // that the On method returns false. This method may be used if an error from
   150  // the Connector is terminal. The caller may also use Connect if they cancel the
   151  // parent context or call Disconnect.
   152  func (c *ConnectionMaster) ConnectOnce(ctx context.Context) (err error) {
   153  	if err = c.Connect(ctx); err != nil {
   154  		// If still "On", disconnect.
   155  		// c.Disconnect() // no-op if not "On"
   156  		if c.cancel != nil {
   157  			c.cancel()
   158  			<-c.done.Load().(chan struct{}) // wait for Connector
   159  		}
   160  	}
   161  	return err
   162  }
   163  
   164  // Done returns a channel that is closed when the Connector's WaitGroup is done.
   165  // If called before Connect, a closed channel is returned.
   166  func (c *ConnectionMaster) Done() <-chan struct{} {
   167  	done, ok := c.done.Load().(chan struct{})
   168  	if ok {
   169  		return done
   170  	}
   171  	done = make(chan struct{})
   172  	close(done)
   173  	return done
   174  }
   175  
   176  // On indicates if the Connector is running. This returns false if never
   177  // connected, or if the Connector has completed shut down.
   178  func (c *ConnectionMaster) On() bool {
   179  	select {
   180  	case <-c.Done():
   181  		return false
   182  	default:
   183  		return true
   184  	}
   185  }
   186  
   187  // Wait waits for the Connector to shut down. It returns immediately if
   188  // Connect has not been called yet.
   189  func (c *ConnectionMaster) Wait() {
   190  	<-c.Done() // let the anon goroutine from Connect return
   191  }
   192  
   193  // Disconnect closes the connection and waits for shutdown. This must not be
   194  // used before or concurrently with Connect.
   195  func (c *ConnectionMaster) Disconnect() {
   196  	if !c.On() {
   197  		return
   198  	}
   199  	c.cancel()
   200  	c.Wait()
   201  }