mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-11-17 20:32:48 +00:00
Heart: Migrated Voice to PacemakerLoop
This commit is contained in:
parent
4cfde32531
commit
c0c17085ba
245
utils/heart/heart.go
Normal file
245
utils/heart/heart.go
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
// Package heart implements a general purpose pacemaker.
|
||||||
|
package heart
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/diamondburned/arikawa/utils/wsutil"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Debug is the default logger that Pacemaker uses.
|
||||||
|
var Debug = func(v ...interface{}) {}
|
||||||
|
|
||||||
|
var ErrDead = errors.New("no heartbeat replied")
|
||||||
|
|
||||||
|
// AtomicTime is a thread-safe UnixNano timestamp guarded by atomic.
|
||||||
|
type AtomicTime struct {
|
||||||
|
unixnano int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AtomicTime) Get() int64 {
|
||||||
|
return atomic.LoadInt64(&t.unixnano)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AtomicTime) Set(time time.Time) {
|
||||||
|
atomic.StoreInt64(&t.unixnano, time.UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AtomicTime) Time() time.Time {
|
||||||
|
return time.Unix(0, t.Get())
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pacemaker struct {
|
||||||
|
// Heartrate is the received duration between heartbeats.
|
||||||
|
Heartrate time.Duration
|
||||||
|
|
||||||
|
// Time in nanoseconds, guarded by atomic read/writes.
|
||||||
|
SentBeat AtomicTime
|
||||||
|
EchoBeat AtomicTime
|
||||||
|
|
||||||
|
// Any callback that returns an error will stop the pacer.
|
||||||
|
Pace func() error
|
||||||
|
|
||||||
|
stop chan struct{}
|
||||||
|
death chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPacemaker(heartrate time.Duration, pacer func() error) *Pacemaker {
|
||||||
|
return &Pacemaker{
|
||||||
|
Heartrate: heartrate,
|
||||||
|
Pace: pacer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pacemaker) Echo() {
|
||||||
|
// Swap our received heartbeats
|
||||||
|
// p.LastBeat[0], p.LastBeat[1] = time.Now(), p.LastBeat[0]
|
||||||
|
p.EchoBeat.Set(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dead, if true, will have Pace return an ErrDead.
|
||||||
|
func (p *Pacemaker) Dead() bool {
|
||||||
|
/* Deprecated
|
||||||
|
if p.LastBeat[0].IsZero() || p.LastBeat[1].IsZero() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.LastBeat[0].Sub(p.LastBeat[1]) > p.Heartrate*2
|
||||||
|
*/
|
||||||
|
|
||||||
|
var (
|
||||||
|
echo = p.EchoBeat.Get()
|
||||||
|
sent = p.SentBeat.Get()
|
||||||
|
)
|
||||||
|
|
||||||
|
if echo == 0 || sent == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return sent-echo > int64(p.Heartrate)*2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pacemaker) Stop() {
|
||||||
|
if p.stop != nil {
|
||||||
|
p.stop <- struct{}{}
|
||||||
|
Debug("(*Pacemaker).stop was sent a stop signal.")
|
||||||
|
} else {
|
||||||
|
Debug("(*Pacemaker).stop is nil, skipping.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pacemaker) start() error {
|
||||||
|
log.Println("HR:", p.Heartrate)
|
||||||
|
tick := time.NewTicker(p.Heartrate)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
// Echo at least once
|
||||||
|
p.Echo()
|
||||||
|
|
||||||
|
for {
|
||||||
|
Debug("Pacemaker loop restarted.")
|
||||||
|
|
||||||
|
if err := p.Pace(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug("Paced.")
|
||||||
|
|
||||||
|
// Paced, save:
|
||||||
|
p.SentBeat.Set(time.Now())
|
||||||
|
|
||||||
|
if p.Dead() {
|
||||||
|
return ErrDead
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.stop:
|
||||||
|
Debug("Received stop signal.")
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case <-tick.C:
|
||||||
|
Debug("Ticked. Restarting.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartAsync starts the pacemaker asynchronously. The WaitGroup is optional.
|
||||||
|
func (p *Pacemaker) StartAsync(wg *sync.WaitGroup) (death chan error) {
|
||||||
|
p.death = make(chan error)
|
||||||
|
p.stop = make(chan struct{})
|
||||||
|
|
||||||
|
if wg != nil {
|
||||||
|
wg.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
p.death <- p.start()
|
||||||
|
// Debug.
|
||||||
|
Debug("Pacemaker returned.")
|
||||||
|
// Mark the stop channel as nil, so later Close() calls won't block forever.
|
||||||
|
p.stop = nil
|
||||||
|
|
||||||
|
// Mark the pacemaker loop as done.
|
||||||
|
if wg != nil {
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return p.death
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO API
|
||||||
|
type EventLoop interface {
|
||||||
|
Heartbeat() error
|
||||||
|
HandleEvent(ev wsutil.Event) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacemakerLoop provides an event loop with a pacemaker.
|
||||||
|
type PacemakerLoop struct {
|
||||||
|
pacemaker *Pacemaker // let's not copy this
|
||||||
|
pacedeath chan error
|
||||||
|
|
||||||
|
events <-chan wsutil.Event
|
||||||
|
handler func(wsutil.Event) error
|
||||||
|
|
||||||
|
ErrorLog func(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLoop(heartrate time.Duration, evs <-chan wsutil.Event, evl EventLoop) *PacemakerLoop {
|
||||||
|
pacemaker := NewPacemaker(heartrate, evl.Heartbeat)
|
||||||
|
|
||||||
|
return &PacemakerLoop{
|
||||||
|
pacemaker: pacemaker,
|
||||||
|
events: evs,
|
||||||
|
handler: evl.HandleEvent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacemakerLoop) errorLog(err error) {
|
||||||
|
if p.ErrorLog == nil {
|
||||||
|
Debug("Uncaught error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ErrorLog(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacemakerLoop) Echo() {
|
||||||
|
p.pacemaker.Echo()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacemakerLoop) Stop() {
|
||||||
|
p.pacemaker.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacemakerLoop) Stopped() bool {
|
||||||
|
return p.pacedeath == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacemakerLoop) Run() error {
|
||||||
|
// If the event loop is already running.
|
||||||
|
if p.pacedeath != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// callers should explicitly handle waitgroups.
|
||||||
|
p.pacedeath = p.pacemaker.StartAsync(nil)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// mark pacedeath once done
|
||||||
|
p.pacedeath = nil
|
||||||
|
|
||||||
|
Debug("Pacemaker loop has exited.")
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-p.pacedeath:
|
||||||
|
// Got a paceDeath, we're exiting from here on out.
|
||||||
|
p.pacedeath = nil // mark
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
// No error, just exit normally.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Wrap(err, "Pacemaker died, reconnecting")
|
||||||
|
|
||||||
|
case ev, ok := <-p.events:
|
||||||
|
if !ok {
|
||||||
|
// Events channel is closed. Kill the pacemaker manually and
|
||||||
|
// die.
|
||||||
|
p.pacemaker.Stop()
|
||||||
|
return <-p.pacedeath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the event
|
||||||
|
if err := p.handler(ev); err != nil {
|
||||||
|
p.errorLog(errors.Wrap(err, "WS handler error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import (
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
|
"github.com/diamondburned/arikawa/utils/heart"
|
||||||
"github.com/diamondburned/arikawa/utils/json"
|
"github.com/diamondburned/arikawa/utils/json"
|
||||||
"github.com/diamondburned/arikawa/utils/wsutil"
|
"github.com/diamondburned/arikawa/utils/wsutil"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -48,7 +49,8 @@ type Connection struct {
|
||||||
WS *wsutil.Websocket
|
WS *wsutil.Websocket
|
||||||
WSTimeout time.Duration
|
WSTimeout time.Duration
|
||||||
|
|
||||||
Pacemaker *gateway.Pacemaker
|
EventLoop *heart.PacemakerLoop
|
||||||
|
// Pacemaker *gateway.Pacemaker
|
||||||
|
|
||||||
udpConn *net.UDPConn
|
udpConn *net.UDPConn
|
||||||
OpusSend chan []byte
|
OpusSend chan []byte
|
||||||
|
@ -174,27 +176,35 @@ func (c *Connection) start() error {
|
||||||
// Make a new WaitGroup for use in background loops:
|
// Make a new WaitGroup for use in background loops:
|
||||||
c.waitGroup = new(sync.WaitGroup)
|
c.waitGroup = new(sync.WaitGroup)
|
||||||
|
|
||||||
// Start the websocket handler.
|
|
||||||
go c.handleWS()
|
|
||||||
|
|
||||||
// Wait for hello.
|
// Wait for hello.
|
||||||
WSDebug("Waiting for Hello..")
|
WSDebug("Waiting for Hello..")
|
||||||
<-c.helloChan
|
|
||||||
|
_, err := AssertEvent(c, <-c.WS.Listen(), HelloOP, &c.hello)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "Error at Hello")
|
||||||
|
}
|
||||||
|
|
||||||
WSDebug("Received Hello")
|
WSDebug("Received Hello")
|
||||||
|
|
||||||
// Start the pacemaker with the heartrate received from Hello, after
|
// // Start the pacemaker with the heartrate received from Hello, after
|
||||||
// initializing everything. This ensures we only heartbeat if the websocket
|
// // initializing everything. This ensures we only heartbeat if the websocket
|
||||||
// is authenticated.
|
// // is authenticated.
|
||||||
c.Pacemaker = &gateway.Pacemaker{
|
// c.Pacemaker = &gateway.Pacemaker{
|
||||||
Heartrate: time.Duration(int(c.hello.HeartbeatInterval)) * time.Millisecond,
|
// Heartrate: time.Duration(int(c.hello.HeartbeatInterval)) * time.Millisecond,
|
||||||
Pace: c.Heartbeat,
|
// Pace: c.Heartbeat,
|
||||||
}
|
// }
|
||||||
// Pacemaker dies here, only when it's fatal.
|
// // Pacemaker dies here, only when it's fatal.
|
||||||
c.paceDeath = c.Pacemaker.StartAsync(c.waitGroup)
|
// c.paceDeath = c.Pacemaker.StartAsync(c.waitGroup)
|
||||||
|
|
||||||
// Start the event handler, which also handles the pacemaker death signal.
|
// Start the event handler, which also handles the pacemaker death signal.
|
||||||
c.waitGroup.Add(1)
|
c.waitGroup.Add(1)
|
||||||
|
|
||||||
|
// Calculate the heartrate.
|
||||||
|
heartrate := time.Duration(int(c.hello.HeartbeatInterval)) * time.Millisecond
|
||||||
|
|
||||||
|
// Start the websocket handler.
|
||||||
|
go c.handleWS(heart.NewLoop(heartrate, c.WS.Listen(), c))
|
||||||
|
|
||||||
WSDebug("Started successfully.")
|
WSDebug("Started successfully.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -220,7 +230,8 @@ func (c *Connection) Close() error {
|
||||||
WSDebug("Stopping pacemaker...")
|
WSDebug("Stopping pacemaker...")
|
||||||
|
|
||||||
// Stop the pacemaker and the event handler
|
// Stop the pacemaker and the event handler
|
||||||
c.Pacemaker.Stop()
|
// c.Pacemaker.Stop()
|
||||||
|
c.EventLoop.Stop()
|
||||||
|
|
||||||
WSDebug("Stopped pacemaker.")
|
WSDebug("Stopped pacemaker.")
|
||||||
}
|
}
|
||||||
|
@ -288,9 +299,15 @@ func (c *Connection) Disconnect(g *gateway.Gateway) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Connection) HandleEvent(ev wsutil.Event) error {
|
||||||
|
return HandleEvent(c, ev)
|
||||||
|
}
|
||||||
|
|
||||||
// handleWS .
|
// handleWS .
|
||||||
func (c *Connection) handleWS() {
|
func (c *Connection) handleWS(evl *heart.PacemakerLoop) {
|
||||||
err := c.eventLoop()
|
c.EventLoop = evl
|
||||||
|
err := c.EventLoop.Run()
|
||||||
|
|
||||||
c.waitGroup.Done() // mark so Close() can exit.
|
c.waitGroup.Done() // mark so Close() can exit.
|
||||||
WSDebug("Event loop stopped.")
|
WSDebug("Event loop stopped.")
|
||||||
|
|
||||||
|
@ -301,32 +318,32 @@ func (c *Connection) handleWS() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eventLoop .
|
// // eventLoop .
|
||||||
func (c *Connection) eventLoop() error {
|
// func (c *Connection) eventLoop() error {
|
||||||
ch := c.WS.Listen()
|
// ch := c.WS.Listen()
|
||||||
|
|
||||||
for {
|
// for {
|
||||||
select {
|
// select {
|
||||||
case err := <-c.paceDeath:
|
// case err := <-c.paceDeath:
|
||||||
// Got a paceDeath, we're exiting from here on out.
|
// // Got a paceDeath, we're exiting from here on out.
|
||||||
c.paceDeath = nil // mark
|
// c.paceDeath = nil // mark
|
||||||
|
|
||||||
if err == nil {
|
// if err == nil {
|
||||||
WSDebug("Pacemaker stopped without errors.")
|
// WSDebug("Pacemaker stopped without errors.")
|
||||||
// No error, just exit normally.
|
// // No error, just exit normally.
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
return errors.Wrap(err, "Pacemaker died, reconnecting")
|
// return errors.Wrap(err, "Pacemaker died, reconnecting")
|
||||||
|
|
||||||
case ev := <-ch:
|
// case ev := <-ch:
|
||||||
// Handle the event
|
// // Handle the event
|
||||||
if err := HandleEvent(c, ev); err != nil {
|
// if err := HandleEvent(c, ev); err != nil {
|
||||||
c.ErrorLog(errors.Wrap(err, "WS handler error"))
|
// c.ErrorLog(errors.Wrap(err, "WS handler error"))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Send .
|
// Send .
|
||||||
func (c *Connection) Send(code OPCode, v interface{}) error {
|
func (c *Connection) Send(code OPCode, v interface{}) error {
|
||||||
|
|
22
voice/op.go
22
voice/op.go
|
@ -40,6 +40,26 @@ func HandleEvent(c *Connection, ev wsutil.Event) error {
|
||||||
return HandleOP(c, o)
|
return HandleOP(c, o)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AssertEvent(driver json.Driver, ev wsutil.Event, code OPCode, v interface{}) (*OP, error) {
|
||||||
|
op, err := DecodeOP(driver, ev)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if op.Code != code {
|
||||||
|
return op, fmt.Errorf(
|
||||||
|
"Unexpected OP Code: %d, expected %d (%s)",
|
||||||
|
op.Code, code, op.Data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := driver.Unmarshal(op.Data, v); err != nil {
|
||||||
|
return op, errors.Wrap(err, "Failed to decode data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return op, nil
|
||||||
|
}
|
||||||
|
|
||||||
func DecodeOP(driver json.Driver, ev wsutil.Event) (*OP, error) {
|
func DecodeOP(driver json.Driver, ev wsutil.Event) (*OP, error) {
|
||||||
if ev.Error != nil {
|
if ev.Error != nil {
|
||||||
return nil, ev.Error
|
return nil, ev.Error
|
||||||
|
@ -81,7 +101,7 @@ func HandleOP(c *Connection, op *OP) error {
|
||||||
|
|
||||||
// Heartbeat response from the server
|
// Heartbeat response from the server
|
||||||
case HeartbeatAckOP:
|
case HeartbeatAckOP:
|
||||||
c.Pacemaker.Echo()
|
c.EventLoop.Echo()
|
||||||
|
|
||||||
// Hello server, we hear you! :)
|
// Hello server, we hear you! :)
|
||||||
case HelloOP:
|
case HelloOP:
|
||||||
|
|
Loading…
Reference in a new issue