Skip to content

Commit bcf8bc9

Browse files
committed
feat: gateway timeout
1 parent abc3489 commit bcf8bc9

1 file changed

Lines changed: 32 additions & 1 deletion

File tree

lib/discordrb/gateway.rb

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ class Gateway
152152
# - 4014: Use of disabled privileged intents.
153153
FATAL_CLOSE_CODES = [4003, 4004, 4011, 4014].freeze
154154

155+
# The timeout in seconds for responses from Discord when expecting one.
156+
# This is used when expecting a hello message after the initial connection
157+
# and when expecting a heartbeat ack after sending a heartbeat (if {#check_heartbeat_acks} is true).
158+
RESPONSE_TIMEOUT = 2
159+
155160
# Heartbeat ACKs are Discord's way of verifying on the client side whether the connection is still alive. If this is
156161
# set to true (default value) the gateway client will use that functionality to detect zombie connections and
157162
# reconnect in such a case; however it may lead to instability if there's some problem with the ACKs. If this occurs
@@ -267,6 +272,9 @@ def inject_error(e)
267272
# @see #send_heartbeat
268273
def heartbeat
269274
if check_heartbeat_acks
275+
# This check is still needed even though we have socket_timeout
276+
# because the Ruby version may be too old to support setting timeout on a socket
277+
# and because RESPONSE_TIMEOUT may be larger than the heartbeat interval.
270278
unless @last_heartbeat_acked
271279
# We're in a bad situation - apparently the last heartbeat wasn't ACK'd, which means the connection is likely
272280
# a zombie. Reconnect
@@ -279,6 +287,7 @@ def heartbeat
279287
end
280288

281289
@last_heartbeat_acked = false
290+
socket_timeout(RESPONSE_TIMEOUT)
282291
end
283292

284293
send_heartbeat(@session ? @session.sequence : 0)
@@ -453,10 +462,22 @@ def send_raw(data, type = :text)
453462

454463
private
455464

465+
def socket_timeout(duration)
466+
if @socket.respond_to?(:timeout=)
467+
@socket.timeout = duration
468+
LOGGER.debug("Set socket timeout to #{duration}")
469+
else # Ruby version < 3.4
470+
LOGGER.debug('Cannot set socket timeout because Ruby is too old')
471+
end
472+
end
473+
456474
def setup_heartbeats(interval)
457475
# Make sure to reset ACK handling, so we don't keep reconnecting
458476
@last_heartbeat_acked = true
459477

478+
# Socket timeout needs to be larger than heartbeat interval
479+
socket_timeout(interval + 1)
480+
460481
# We don't want to have redundant heartbeat threads, so if one already exists, don't start a new one
461482
return if @heartbeat_thread
462483

@@ -590,6 +611,9 @@ def connect
590611
@socket = obtain_socket(gateway_uri)
591612
LOGGER.debug('Obtained socket')
592613

614+
# A hello message should be received quickly after it is connected
615+
socket_timeout(RESPONSE_TIMEOUT)
616+
593617
# Initialise some properties
594618
@handshake = ::WebSocket::Handshake::Client.new(url: url) # Represents the handshake between us and the server
595619
@handshaked = false # Whether the handshake has finished yet
@@ -625,6 +649,10 @@ def websocket_loop
625649
@pipe_broken = true
626650
handle_internal_close('Socket EOF in websocket_loop')
627651
next
652+
rescue IO::TimeoutError
653+
@pipe_broken = true
654+
handle_internal_close('Socket did not receive a response in time')
655+
next
628656
end
629657

630658
# Check if we actually got data
@@ -803,7 +831,10 @@ def handle_hello(packet)
803831
# Op 11
804832
def handle_heartbeat_ack(packet)
805833
LOGGER.debug("Received heartbeat ack for packet: #{packet.inspect}")
806-
@last_heartbeat_acked = true if @check_heartbeat_acks
834+
835+
return unless @check_heartbeat_acks
836+
@last_heartbeat_acked = true
837+
socket_timeout(@heartbeat_interval + 1)
807838
end
808839

809840
# Called when the websocket has been disconnected in some way - say due to a pipe error while sending

0 commit comments

Comments
 (0)