From 35545e30303faa517a98ec18c660e5f6c063c912 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 21 May 2026 16:45:31 +0200 Subject: Fix voice reconnect race: reuse VoiceClient instead of destroying it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale-connection path was calling _remove_voice_client() then creating a brand-new VoiceClient via channel.connect(). This created a race: VOICE_SERVER_UPDATE (triggered by Discord when the user joins voice) arrives between the destroy and the fresh-connect, gets consumed by the old client, and the new client never receives it — causing a 10s timeout waiting for got_both_voice_updates. Fix: after disconnect(), call vc.connect() on the same VoiceClient instance. The disconnect resets the state machine to 'disconnected' but the same VoiceConnectionState processes any events that arrive between disconnect and reconnect, so the state machine correctly reaches got_both_voice_updates. --- cogs/voice.py | 54 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/cogs/voice.py b/cogs/voice.py index f079c6a..a224336 100644 --- a/cogs/voice.py +++ b/cogs/voice.py @@ -80,17 +80,29 @@ class VoiceManager: logger.info(f"Already connected to {channel}") return vc logger.warning(f"Stale connection to {channel}, reconnecting") + # Disconnect resets the state machine to 'disconnected' + # but keeps the VoiceClient alive. Reusing it avoids a + # race where Discord sends VOICE_SERVER_UPDATE between + # the destroy and the fresh-connect below, causing the + # new client to timeout waiting for the event (since the + # old one already consumed it). try: await vc.disconnect() except Exception: pass - # Forcefully clear stale internal state so the fresh - # connect doesn't hang for 30s waiting for Discord. try: - channel._state._remove_voice_client(guild.id) - except Exception: - pass - vc = None + await vc.connect( + reconnect=False, timeout=10.0, self_deaf=False, self_mute=False + ) + logger.info(f"Reconnected to voice channel: {channel}") + return vc + except Exception as e: + logger.error(f"Failed to reconnect stale voice client: {e}") + try: + channel._state._remove_voice_client(guild.id) + except Exception: + pass + vc = None else: try: await vc.move_to(channel) @@ -102,18 +114,38 @@ class VoiceManager: await vc.disconnect() except Exception: pass + try: + await vc.connect( + reconnect=False, timeout=10.0, self_deaf=False, self_mute=False + ) + logger.info(f"Reconnected after failed move: {channel}") + return vc + except Exception: + pass + try: + channel._state._remove_voice_client(guild.id) + except Exception: + pass vc = None else: - logger.info("Voice client exists but disconnected, cleaning up stale entry") + logger.info("Voice client exists but disconnected, reconnecting") try: await vc.disconnect() except Exception: pass try: - channel._state._remove_voice_client(guild.id) - except Exception: - pass - vc = None + await vc.connect( + reconnect=False, timeout=10.0, self_deaf=False, self_mute=False + ) + logger.info(f"Reconnected disconnected client: {channel}") + return vc + except Exception as e: + logger.error(f"Failed to reconnect disconnected client: {e}") + try: + channel._state._remove_voice_client(guild.id) + except Exception: + pass + vc = None # --- create fresh connection -------------------------------------- self._connecting.add(guild_id) -- cgit v1.2.3