diff options
| author | salmonik <raisersoft@proton.me> | 2026-05-21 16:45:31 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-05-21 16:45:31 +0200 |
| commit | 35545e30303faa517a98ec18c660e5f6c063c912 (patch) | |
| tree | 36f5cf41b1dd1cf34f377804f20b76a462fcd219 /cogs/voice.py | |
| parent | make the assistant simpler (diff) | |
| download | senator-sperma-35545e30303faa517a98ec18c660e5f6c063c912.tar.gz senator-sperma-35545e30303faa517a98ec18c660e5f6c063c912.zip | |
Fix voice reconnect race: reuse VoiceClient instead of destroying it
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.
Diffstat (limited to 'cogs/voice.py')
| -rw-r--r-- | cogs/voice.py | 54 |
1 files 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: | |||
| 80 | logger.info(f"Already connected to {channel}") | 80 | logger.info(f"Already connected to {channel}") |
| 81 | return vc | 81 | return vc |
| 82 | logger.warning(f"Stale connection to {channel}, reconnecting") | 82 | logger.warning(f"Stale connection to {channel}, reconnecting") |
| 83 | # Disconnect resets the state machine to 'disconnected' | ||
| 84 | # but keeps the VoiceClient alive. Reusing it avoids a | ||
| 85 | # race where Discord sends VOICE_SERVER_UPDATE between | ||
| 86 | # the destroy and the fresh-connect below, causing the | ||
| 87 | # new client to timeout waiting for the event (since the | ||
| 88 | # old one already consumed it). | ||
| 83 | try: | 89 | try: |
| 84 | await vc.disconnect() | 90 | await vc.disconnect() |
| 85 | except Exception: | 91 | except Exception: |
| 86 | pass | 92 | pass |
| 87 | # Forcefully clear stale internal state so the fresh | ||
| 88 | # connect doesn't hang for 30s waiting for Discord. | ||
| 89 | try: | 93 | try: |
| 90 | channel._state._remove_voice_client(guild.id) | 94 | await vc.connect( |
| 91 | except Exception: | 95 | reconnect=False, timeout=10.0, self_deaf=False, self_mute=False |
| 92 | pass | 96 | ) |
| 93 | vc = None | 97 | logger.info(f"Reconnected to voice channel: {channel}") |
| 98 | return vc | ||
| 99 | except Exception as e: | ||
| 100 | logger.error(f"Failed to reconnect stale voice client: {e}") | ||
| 101 | try: | ||
| 102 | channel._state._remove_voice_client(guild.id) | ||
| 103 | except Exception: | ||
| 104 | pass | ||
| 105 | vc = None | ||
| 94 | else: | 106 | else: |
| 95 | try: | 107 | try: |
| 96 | await vc.move_to(channel) | 108 | await vc.move_to(channel) |
| @@ -102,18 +114,38 @@ class VoiceManager: | |||
| 102 | await vc.disconnect() | 114 | await vc.disconnect() |
| 103 | except Exception: | 115 | except Exception: |
| 104 | pass | 116 | pass |
| 117 | try: | ||
| 118 | await vc.connect( | ||
| 119 | reconnect=False, timeout=10.0, self_deaf=False, self_mute=False | ||
| 120 | ) | ||
| 121 | logger.info(f"Reconnected after failed move: {channel}") | ||
| 122 | return vc | ||
| 123 | except Exception: | ||
| 124 | pass | ||
| 125 | try: | ||
| 126 | channel._state._remove_voice_client(guild.id) | ||
| 127 | except Exception: | ||
| 128 | pass | ||
| 105 | vc = None | 129 | vc = None |
| 106 | else: | 130 | else: |
| 107 | logger.info("Voice client exists but disconnected, cleaning up stale entry") | 131 | logger.info("Voice client exists but disconnected, reconnecting") |
| 108 | try: | 132 | try: |
| 109 | await vc.disconnect() | 133 | await vc.disconnect() |
| 110 | except Exception: | 134 | except Exception: |
| 111 | pass | 135 | pass |
| 112 | try: | 136 | try: |
| 113 | channel._state._remove_voice_client(guild.id) | 137 | await vc.connect( |
| 114 | except Exception: | 138 | reconnect=False, timeout=10.0, self_deaf=False, self_mute=False |
| 115 | pass | 139 | ) |
| 116 | vc = None | 140 | logger.info(f"Reconnected disconnected client: {channel}") |
| 141 | return vc | ||
| 142 | except Exception as e: | ||
| 143 | logger.error(f"Failed to reconnect disconnected client: {e}") | ||
| 144 | try: | ||
| 145 | channel._state._remove_voice_client(guild.id) | ||
| 146 | except Exception: | ||
| 147 | pass | ||
| 148 | vc = None | ||
| 117 | 149 | ||
| 118 | # --- create fresh connection -------------------------------------- | 150 | # --- create fresh connection -------------------------------------- |
| 119 | self._connecting.add(guild_id) | 151 | self._connecting.add(guild_id) |
