github.com/diamondburned/arikawa/v2@v2.1.0/utils/wsutil/heart.go (about)

     1  package wsutil
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	"github.com/pkg/errors"
     8  
     9  	"github.com/diamondburned/arikawa/v2/internal/heart"
    10  )
    11  
    12  type errBrokenConnection struct {
    13  	underneath error
    14  }
    15  
    16  // Error formats the broken connection error with the message "explicit
    17  // connection break."
    18  func (err errBrokenConnection) Error() string {
    19  	return "explicit connection break: " + err.underneath.Error()
    20  }
    21  
    22  // Unwrap returns the underlying error.
    23  func (err errBrokenConnection) Unwrap() error {
    24  	return err.underneath
    25  }
    26  
    27  // ErrBrokenConnection marks the given error as a broken connection error. This
    28  // error will cause the pacemaker loop to break and return the error. The error,
    29  // when stringified, will say "explicit connection break."
    30  func ErrBrokenConnection(err error) error {
    31  	return errBrokenConnection{underneath: err}
    32  }
    33  
    34  // IsBrokenConnection returns true if the error is a broken connection error.
    35  func IsBrokenConnection(err error) bool {
    36  	var broken *errBrokenConnection
    37  	return errors.As(err, &broken)
    38  }
    39  
    40  // TODO API
    41  type EventLoopHandler interface {
    42  	EventHandler
    43  	HeartbeatCtx(context.Context) error
    44  }
    45  
    46  // PacemakerLoop provides an event loop with a pacemaker. A zero-value instance
    47  // is a valid instance only when RunAsync is called first.
    48  type PacemakerLoop struct {
    49  	heart.Pacemaker
    50  	Extras   ExtraHandlers
    51  	ErrorLog func(error)
    52  
    53  	events  <-chan Event
    54  	control chan func()
    55  	handler func(*OP) error
    56  }
    57  
    58  func (p *PacemakerLoop) errorLog(err error) {
    59  	if p.ErrorLog == nil {
    60  		WSDebug("Uncaught error:", err)
    61  		return
    62  	}
    63  
    64  	p.ErrorLog(err)
    65  }
    66  
    67  // Pace calls the pacemaker's Pace function.
    68  func (p *PacemakerLoop) Pace(ctx context.Context) error {
    69  	return p.Pacemaker.PaceCtx(ctx)
    70  }
    71  
    72  // StartBeating asynchronously starts the pacemaker loop.
    73  func (p *PacemakerLoop) StartBeating(pace time.Duration, evl EventLoopHandler, exit func(error)) {
    74  	WSDebug("Starting the pacemaker loop.")
    75  
    76  	p.Pacemaker = heart.NewPacemaker(pace, evl.HeartbeatCtx)
    77  	p.control = make(chan func())
    78  	p.handler = evl.HandleOP
    79  	p.events = nil // block forever
    80  
    81  	go func() { exit(p.startLoop()) }()
    82  }
    83  
    84  // Stop signals the pacemaker to stop. It does not wait for the pacer to stop.
    85  // The pacer will call the given callback with a nil error.
    86  func (p *PacemakerLoop) Stop() {
    87  	close(p.control)
    88  }
    89  
    90  // SetEventChannel sets the event channel inside the event loop. There is no
    91  // guarantee that the channel is set when the function returns. This function is
    92  // concurrently safe.
    93  func (p *PacemakerLoop) SetEventChannel(evCh <-chan Event) {
    94  	p.control <- func() { p.events = evCh }
    95  }
    96  
    97  // SetPace (re)sets the pace duration. As with SetEventChannel, there is no
    98  // guarantee that the pacer is reset when the function returns. This function is
    99  // concurrently safe.
   100  func (p *PacemakerLoop) SetPace(pace time.Duration) {
   101  	p.control <- func() { p.Pacemaker.SetPace(pace) }
   102  }
   103  
   104  func (p *PacemakerLoop) startLoop() error {
   105  	defer WSDebug("Pacemaker loop has exited.")
   106  	defer p.Pacemaker.StopTicker()
   107  
   108  	for {
   109  		select {
   110  		case <-p.Pacemaker.Ticks:
   111  			if err := p.Pacemaker.Pace(); err != nil {
   112  				return errors.Wrap(err, "pace failed, reconnecting")
   113  			}
   114  
   115  		case fn, ok := <-p.control:
   116  			if !ok { // Intentional stop at p.Close().
   117  				WSDebug("Pacemaker intentionally stopped using p.control.")
   118  				return nil
   119  			}
   120  
   121  			fn()
   122  
   123  		case ev, ok := <-p.events:
   124  			if !ok {
   125  				WSDebug("Events channel closed, stopping pacemaker.")
   126  				return nil
   127  			}
   128  
   129  			if ev.Error != nil {
   130  				return errors.Wrap(ev.Error, "event returned error")
   131  			}
   132  
   133  			o, err := DecodeOP(ev)
   134  			if err != nil {
   135  				return errors.Wrap(err, "failed to decode OP")
   136  			}
   137  
   138  			// Check the events before handling.
   139  			p.Extras.Check(o)
   140  
   141  			// Handle the event
   142  			if err := p.handler(o); err != nil {
   143  				if IsBrokenConnection(err) {
   144  					return errors.Wrap(err, "handler failed")
   145  				}
   146  
   147  				p.errorLog(err)
   148  			}
   149  		}
   150  	}
   151  }