mirror of
https://github.com/diamondburned/arikawa.git
synced 2024-12-02 20:02:53 +00:00
diamondburned
6c332ac145
This commit fixes race conditions in both package voice, package voicegateway and package gateway. Originally, several race conditions exist when both the user's and the pacemaker's goroutines both want to do several things to the websocket connection. For example, the user's goroutine could be writing, and the pacemaker's goroutine could trigger a reconnection. This is racey. This issue is partially fixed by removing the pacer loop from package heart and combining the ticker into the event (pacemaker) loop itself. Technically, a race condition could still be triggered with care, but the API itself never guaranteed any of those. As events are handled using an internal loop into a channel, a race condition will not be triggered just by handling events and writing to the websocket.
117 lines
2.9 KiB
Go
117 lines
2.9 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/diamondburned/arikawa/utils/json"
|
|
"github.com/diamondburned/arikawa/utils/wsutil"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
type OPCode = wsutil.OPCode
|
|
|
|
const (
|
|
DispatchOP OPCode = 0 // recv
|
|
HeartbeatOP OPCode = 1 // send/recv
|
|
IdentifyOP OPCode = 2 // send...
|
|
StatusUpdateOP OPCode = 3 //
|
|
VoiceStateUpdateOP OPCode = 4 //
|
|
VoiceServerPingOP OPCode = 5 //
|
|
ResumeOP OPCode = 6 //
|
|
ReconnectOP OPCode = 7 // recv
|
|
RequestGuildMembersOP OPCode = 8 // send
|
|
InvalidSessionOP OPCode = 9 // recv...
|
|
HelloOP OPCode = 10
|
|
HeartbeatAckOP OPCode = 11
|
|
CallConnectOP OPCode = 13
|
|
GuildSubscriptionsOP OPCode = 14
|
|
)
|
|
|
|
// ErrReconnectRequest is returned by HandleOP if a ReconnectOP is given. This
|
|
// is used mostly internally to signal the heartbeat loop to reconnect, if
|
|
// needed. It is not a fatal error.
|
|
var ErrReconnectRequest = errors.New("ReconnectOP received")
|
|
|
|
func (g *Gateway) HandleOP(op *wsutil.OP) error {
|
|
switch op.Code {
|
|
case HeartbeatAckOP:
|
|
// Heartbeat from the server?
|
|
g.PacerLoop.Echo()
|
|
|
|
case HeartbeatOP:
|
|
ctx, cancel := context.WithTimeout(context.Background(), g.WSTimeout)
|
|
defer cancel()
|
|
|
|
// Server requesting a heartbeat.
|
|
if err := g.PacerLoop.Pace(ctx); err != nil {
|
|
return wsutil.ErrBrokenConnection(errors.Wrap(err, "failed to pace"))
|
|
}
|
|
|
|
case ReconnectOP:
|
|
// Server requests to reconnect, die and retry.
|
|
wsutil.WSDebug("ReconnectOP received.")
|
|
|
|
// Exit with the ReconnectOP error to force the heartbeat event loop to
|
|
// reconnect synchronously. Not really a fatal error.
|
|
return wsutil.ErrBrokenConnection(ErrReconnectRequest)
|
|
|
|
case InvalidSessionOP:
|
|
// Discord expects us to sleep for no reason
|
|
time.Sleep(time.Duration(rand.Intn(5)+1) * time.Second)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), g.WSTimeout)
|
|
defer cancel()
|
|
|
|
// Invalid session, try and Identify.
|
|
if err := g.IdentifyCtx(ctx); err != nil {
|
|
// Can't identify, reconnect.
|
|
return wsutil.ErrBrokenConnection(ErrReconnectRequest)
|
|
}
|
|
|
|
return nil
|
|
|
|
case HelloOP:
|
|
return nil
|
|
|
|
case DispatchOP:
|
|
// Set the sequence
|
|
if op.Sequence > 0 {
|
|
g.Sequence.Set(op.Sequence)
|
|
}
|
|
|
|
// Check if we know the event
|
|
fn, ok := EventCreator[op.EventName]
|
|
if !ok {
|
|
return fmt.Errorf(
|
|
"unknown event %s: %s",
|
|
op.EventName, string(op.Data),
|
|
)
|
|
}
|
|
|
|
// Make a new pointer to the event
|
|
var ev = fn()
|
|
|
|
// Try and parse the event
|
|
if err := json.Unmarshal(op.Data, ev); err != nil {
|
|
return errors.Wrap(err, "failed to parse event "+op.EventName)
|
|
}
|
|
|
|
// If the event is a ready, we'll want its sessionID
|
|
if ev, ok := ev.(*ReadyEvent); ok {
|
|
g.SessionID = ev.SessionID
|
|
}
|
|
|
|
// Throw the event into a channel; it's valid now.
|
|
g.Events <- ev
|
|
return nil
|
|
|
|
default:
|
|
return fmt.Errorf("unknown OP code %d (event %s)", op.Code, op.EventName)
|
|
}
|
|
|
|
return nil
|
|
}
|