1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2024-12-02 20:02:53 +00:00
arikawa/gateway/op.go
diamondburned 6c332ac145 {Voice,}Gateway: Fixed various race conditions
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.
2020-10-22 10:47:27 -07:00

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
}