diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 01:34:42 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 01:34:42 +0100 |
| commit | 0d2f471e9a868518899be4d51cb3029f01079f2e (patch) | |
| tree | ee92e7528ac3e8c31fc5a76014b055209de4b5c7 | |
| parent | docs: sync with current behavior (diff) | |
| download | senator-sperma-0d2f471e9a868518899be4d51cb3029f01079f2e.tar.gz senator-sperma-0d2f471e9a868518899be4d51cb3029f01079f2e.zip | |
music: keep preparation out of the guild lock and requeue failed connects
| -rw-r--r-- | src/music.rs | 218 |
1 files changed, 119 insertions, 99 deletions
diff --git a/src/music.rs b/src/music.rs index 0b907d3..f1c0ad9 100644 --- a/src/music.rs +++ b/src/music.rs | |||
| @@ -104,13 +104,17 @@ struct GuildMusic { | |||
| 104 | voice: serenity::ChannelId, | 104 | voice: serenity::ChannelId, |
| 105 | /// Text channel where auto-advance announcements are posted. | 105 | /// Text channel where auto-advance announcements are posted. |
| 106 | announce: Option<serenity::ChannelId>, | 106 | announce: Option<serenity::ChannelId>, |
| 107 | /// Bumped by interrupts (`now`, skip, stop). In-flight preparation for an | ||
| 108 | /// older generation is discarded instead of starting over the new track. | ||
| 109 | generation: u64, | ||
| 107 | } | 110 | } |
| 108 | 111 | ||
| 109 | /// Shared, per-guild music state. Stored in [`crate::Data`]. | 112 | /// Shared, per-guild music state. Stored in [`crate::Data`]. |
| 110 | pub struct MusicState { | 113 | pub struct MusicState { |
| 111 | guilds: Mutex<HashMap<u64, GuildMusic>>, | 114 | guilds: Mutex<HashMap<u64, GuildMusic>>, |
| 112 | /// One transition lock per guild. Held across the whole stop/prepare/play | 115 | /// One state lock per guild. Held only for state mutations; downloads, |
| 113 | /// sequence so concurrent `/music` commands cannot interleave. | 116 | /// compression, and voice connects happen outside it so one slow track |
| 117 | /// cannot stall every other `/music` command in the guild. | ||
| 114 | locks: Mutex<HashMap<u64, Arc<Mutex<()>>>>, | 118 | locks: Mutex<HashMap<u64, Arc<Mutex<()>>>>, |
| 115 | voice: VoiceManager, | 119 | voice: VoiceManager, |
| 116 | http: Arc<serenity::Http>, | 120 | http: Arc<serenity::Http>, |
| @@ -150,55 +154,54 @@ impl MusicState { | |||
| 150 | position: QueuePosition, | 154 | position: QueuePosition, |
| 151 | ) -> Result<EnqueueOutcome> { | 155 | ) -> Result<EnqueueOutcome> { |
| 152 | let lock = self.guild_lock(guild).await; | 156 | let lock = self.guild_lock(guild).await; |
| 153 | let _transition = lock.lock().await; | 157 | let (was_playing, queued_position, interrupted) = { |
| 154 | 158 | let _transition = lock.lock().await; | |
| 155 | let was_playing; | ||
| 156 | { | ||
| 157 | let mut map = self.guilds.lock().await; | 159 | let mut map = self.guilds.lock().await; |
| 158 | let gm = map.entry(guild).or_insert_with(|| GuildMusic { | 160 | let gm = map.entry(guild).or_insert_with(|| GuildMusic { |
| 159 | current: None, | 161 | current: None, |
| 160 | queue: VecDeque::new(), | 162 | queue: VecDeque::new(), |
| 161 | voice: voice_channel, | 163 | voice: voice_channel, |
| 162 | announce, | 164 | announce, |
| 165 | generation: 0, | ||
| 163 | }); | 166 | }); |
| 164 | gm.voice = voice_channel; | 167 | gm.voice = voice_channel; |
| 165 | gm.announce = announce; | 168 | gm.announce = announce; |
| 166 | was_playing = gm.current.is_some(); | 169 | let was_playing = gm.current.is_some(); |
| 167 | 170 | ||
| 168 | match position { | 171 | match position { |
| 169 | QueuePosition::Now | QueuePosition::Next => gm.queue.push_front(request), | 172 | QueuePosition::Now => gm.queue.push_front(request), |
| 173 | QueuePosition::Next => gm.queue.push_front(request), | ||
| 170 | QueuePosition::Last => gm.queue.push_back(request), | 174 | QueuePosition::Last => gm.queue.push_back(request), |
| 171 | } | 175 | } |
| 172 | } | 176 | let queued_position = match position { |
| 177 | QueuePosition::Next => 1, | ||
| 178 | QueuePosition::Last => gm.queue.len(), | ||
| 179 | QueuePosition::Now => 1, | ||
| 180 | }; | ||
| 173 | 181 | ||
| 174 | if was_playing && position != QueuePosition::Now { | 182 | let interrupted = if was_playing && position == QueuePosition::Now { |
| 175 | let position = { | 183 | gm.generation += 1; |
| 176 | let map = self.guilds.lock().await; | 184 | gm.current.take() |
| 177 | let len = map.get(&guild).map(|gm| gm.queue.len()).unwrap_or(1); | 185 | } else { |
| 178 | if position == QueuePosition::Next { | 186 | None |
| 179 | 1 | ||
| 180 | } else { | ||
| 181 | len | ||
| 182 | } | ||
| 183 | }; | 187 | }; |
| 184 | return Ok(EnqueueOutcome::Queued { position }); | 188 | (was_playing, queued_position, interrupted) |
| 189 | }; | ||
| 190 | |||
| 191 | if let Some(current) = interrupted { | ||
| 192 | let _ = current.handle.stop(); | ||
| 185 | } | 193 | } |
| 186 | 194 | ||
| 187 | if was_playing { | 195 | if was_playing && position != QueuePosition::Now { |
| 188 | // `Now`: drop the current track. The new request is at the front of | 196 | return Ok(EnqueueOutcome::Queued { |
| 189 | // the queue; play_next_locked will pick it up. | 197 | position: queued_position, |
| 190 | let current = { | 198 | }); |
| 191 | let mut map = self.guilds.lock().await; | ||
| 192 | map.get_mut(&guild).and_then(|gm| gm.current.take()) | ||
| 193 | }; | ||
| 194 | if let Some(current) = current { | ||
| 195 | let _ = current.handle.stop(); | ||
| 196 | } | ||
| 197 | } | 199 | } |
| 198 | 200 | ||
| 199 | // The command layer posts the "now playing" message for direct | 201 | // The command layer posts the "now playing" message for direct |
| 200 | // enqueues, so don't announce here as well. | 202 | // enqueues, so don't announce here as well. Preparation happens |
| 201 | let played = self.play_next_locked(guild, false).await?; | 203 | // outside the guild lock. |
| 204 | let played = self.play_next(guild, false).await?; | ||
| 202 | Ok(if played { | 205 | Ok(if played { |
| 203 | EnqueueOutcome::Started | 206 | EnqueueOutcome::Started |
| 204 | } else { | 207 | } else { |
| @@ -210,35 +213,30 @@ impl MusicState { | |||
| 210 | /// Returns `true` when a track started, `false` when the queue was empty. | 213 | /// Returns `true` when a track started, `false` when the queue was empty. |
| 211 | pub async fn skip(self: &Arc<Self>, guild: u64) -> Result<bool> { | 214 | pub async fn skip(self: &Arc<Self>, guild: u64) -> Result<bool> { |
| 212 | let lock = self.guild_lock(guild).await; | 215 | let lock = self.guild_lock(guild).await; |
| 213 | let _transition = lock.lock().await; | 216 | let (current, has_queue) = { |
| 214 | 217 | let _transition = lock.lock().await; | |
| 215 | let current = { | ||
| 216 | let mut map = self.guilds.lock().await; | 218 | let mut map = self.guilds.lock().await; |
| 217 | map.get_mut(&guild).and_then(|gm| gm.current.take()) | 219 | let Some(gm) = map.get_mut(&guild) else { |
| 220 | return Ok(false); | ||
| 221 | }; | ||
| 222 | gm.generation += 1; | ||
| 223 | (gm.current.take(), !gm.queue.is_empty()) | ||
| 218 | }; | 224 | }; |
| 219 | if let Some(current) = current { | 225 | if let Some(current) = current { |
| 220 | let _ = current.handle.stop(); | 226 | let _ = current.handle.stop(); |
| 221 | } | 227 | } |
| 222 | |||
| 223 | let has_queue = { | ||
| 224 | let map = self.guilds.lock().await; | ||
| 225 | map.get(&guild) | ||
| 226 | .map(|gm| !gm.queue.is_empty()) | ||
| 227 | .unwrap_or(false) | ||
| 228 | }; | ||
| 229 | if !has_queue { | 228 | if !has_queue { |
| 230 | return Ok(false); | 229 | return Ok(false); |
| 231 | } | 230 | } |
| 232 | 231 | ||
| 233 | self.play_next_locked(guild, true).await | 232 | self.play_next(guild, true).await |
| 234 | } | 233 | } |
| 235 | 234 | ||
| 236 | /// Stop playback and drop the queue (does not disconnect from voice). | 235 | /// Stop playback and drop the queue (does not disconnect from voice). |
| 237 | pub async fn stop(self: &Arc<Self>, guild: u64) { | 236 | pub async fn stop(self: &Arc<Self>, guild: u64) { |
| 238 | let lock = self.guild_lock(guild).await; | 237 | let lock = self.guild_lock(guild).await; |
| 239 | let _transition = lock.lock().await; | ||
| 240 | |||
| 241 | let current = { | 238 | let current = { |
| 239 | let _transition = lock.lock().await; | ||
| 242 | let mut map = self.guilds.lock().await; | 240 | let mut map = self.guilds.lock().await; |
| 243 | map.remove(&guild).and_then(|gm| gm.current) | 241 | map.remove(&guild).and_then(|gm| gm.current) |
| 244 | }; | 242 | }; |
| @@ -258,24 +256,17 @@ impl MusicState { | |||
| 258 | request: TrackRequest, | 256 | request: TrackRequest, |
| 259 | ) -> Result<()> { | 257 | ) -> Result<()> { |
| 260 | let lock = self.guild_lock(guild).await; | 258 | let lock = self.guild_lock(guild).await; |
| 261 | let _transition = lock.lock().await; | ||
| 262 | |||
| 263 | let current = { | 259 | let current = { |
| 264 | let mut map = self.guilds.lock().await; | 260 | let _transition = lock.lock().await; |
| 265 | map.get_mut(&guild).and_then(|gm| gm.current.take()) | ||
| 266 | }; | ||
| 267 | if let Some(current) = current { | ||
| 268 | let _ = current.handle.stop(); | ||
| 269 | } | ||
| 270 | |||
| 271 | { | ||
| 272 | let mut map = self.guilds.lock().await; | 261 | let mut map = self.guilds.lock().await; |
| 273 | let gm = map.entry(guild).or_insert_with(|| GuildMusic { | 262 | let gm = map.entry(guild).or_insert_with(|| GuildMusic { |
| 274 | current: None, | 263 | current: None, |
| 275 | queue: VecDeque::new(), | 264 | queue: VecDeque::new(), |
| 276 | voice: voice_channel, | 265 | voice: voice_channel, |
| 277 | announce, | 266 | announce, |
| 267 | generation: 0, | ||
| 278 | }); | 268 | }); |
| 269 | gm.generation += 1; | ||
| 279 | gm.voice = voice_channel; | 270 | gm.voice = voice_channel; |
| 280 | // Keep the last text channel when the caller has nowhere to | 271 | // Keep the last text channel when the caller has nowhere to |
| 281 | // announce (cron's channel is a voice channel). | 272 | // announce (cron's channel is a voice channel). |
| @@ -283,9 +274,13 @@ impl MusicState { | |||
| 283 | gm.announce = announce; | 274 | gm.announce = announce; |
| 284 | } | 275 | } |
| 285 | gm.queue.push_front(request); | 276 | gm.queue.push_front(request); |
| 277 | gm.current.take() | ||
| 278 | }; | ||
| 279 | if let Some(current) = current { | ||
| 280 | let _ = current.handle.stop(); | ||
| 286 | } | 281 | } |
| 287 | 282 | ||
| 288 | if self.play_next_locked(guild, true).await? { | 283 | if self.play_next(guild, true).await? { |
| 289 | Ok(()) | 284 | Ok(()) |
| 290 | } else { | 285 | } else { |
| 291 | Err(Error::Custom(lang::L_MUSIC_QUEUE_EMPTY.into())) | 286 | Err(Error::Custom(lang::L_MUSIC_QUEUE_EMPTY.into())) |
| @@ -314,22 +309,28 @@ impl MusicState { | |||
| 314 | } | 309 | } |
| 315 | } | 310 | } |
| 316 | 311 | ||
| 317 | /// Pop and play the next playable track. Caller must hold the guild | 312 | /// Pop and play the next playable track. |
| 318 | /// transition lock. | 313 | /// |
| 319 | async fn play_next_locked(self: &Arc<Self>, guild: u64, announce: bool) -> Result<bool> { | 314 | /// Input preparation and the voice connect happen outside the guild state |
| 315 | /// lock, so a slow download cannot block other commands. A generation | ||
| 316 | /// token makes an in-flight track abort if it was interrupted meanwhile. | ||
| 317 | async fn play_next(self: &Arc<Self>, guild: u64, announce: bool) -> Result<bool> { | ||
| 320 | let mut last_error: Option<Error> = None; | 318 | let mut last_error: Option<Error> = None; |
| 321 | loop { | 319 | loop { |
| 322 | let request = { | 320 | let (request, voice_channel, announce_channel, generation) = { |
| 321 | let lock = self.guild_lock(guild).await; | ||
| 322 | let _transition = lock.lock().await; | ||
| 323 | let mut map = self.guilds.lock().await; | 323 | let mut map = self.guilds.lock().await; |
| 324 | match map.get_mut(&guild).and_then(|gm| gm.queue.pop_front()) { | 324 | let Some(gm) = map.get_mut(&guild) else { |
| 325 | Some(request) => request, | 325 | return Ok(false); |
| 326 | None => { | 326 | }; |
| 327 | return match last_error { | 327 | let Some(request) = gm.queue.pop_front() else { |
| 328 | Some(e) => Err(e), | 328 | return match last_error { |
| 329 | None => Ok(false), | 329 | Some(e) => Err(e), |
| 330 | }; | 330 | None => Ok(false), |
| 331 | } | 331 | }; |
| 332 | } | 332 | }; |
| 333 | (request, gm.voice, gm.announce, gm.generation) | ||
| 333 | }; | 334 | }; |
| 334 | 335 | ||
| 335 | let input = match build_input(&request, &self.reqwest).await { | 336 | let input = match build_input(&request, &self.reqwest).await { |
| @@ -345,38 +346,58 @@ impl MusicState { | |||
| 345 | } | 346 | } |
| 346 | }; | 347 | }; |
| 347 | 348 | ||
| 348 | let (voice_channel, announce_channel) = { | ||
| 349 | let map = self.guilds.lock().await; | ||
| 350 | match map.get(&guild) { | ||
| 351 | Some(gm) => (gm.voice, gm.announce), | ||
| 352 | None => return Ok(false), | ||
| 353 | } | ||
| 354 | }; | ||
| 355 | |||
| 356 | let call = match self.voice.get_call(serenity::GuildId::new(guild)) { | 349 | let call = match self.voice.get_call(serenity::GuildId::new(guild)) { |
| 357 | Some(call) => call, | 350 | Some(call) => call, |
| 358 | None => { | 351 | None => match self |
| 359 | self.voice | 352 | .voice |
| 360 | .connect(serenity::GuildId::new(guild), voice_channel) | 353 | .connect(serenity::GuildId::new(guild), voice_channel) |
| 361 | .await? | 354 | .await |
| 362 | } | 355 | { |
| 356 | Ok(call) => call, | ||
| 357 | Err(e) => { | ||
| 358 | // Put the track back unless an interrupt superseded it. | ||
| 359 | let lock = self.guild_lock(guild).await; | ||
| 360 | let _transition = lock.lock().await; | ||
| 361 | let mut map = self.guilds.lock().await; | ||
| 362 | if let Some(gm) = map.get_mut(&guild) | ||
| 363 | && gm.generation == generation | ||
| 364 | { | ||
| 365 | gm.queue.push_front(request); | ||
| 366 | } | ||
| 367 | return Err(e); | ||
| 368 | } | ||
| 369 | }, | ||
| 363 | }; | 370 | }; |
| 364 | 371 | ||
| 365 | let handle = { | 372 | let (handle, token) = { |
| 366 | let mut call = call.lock().await; | 373 | let lock = self.guild_lock(guild).await; |
| 367 | call.play_input(input) | 374 | let _transition = lock.lock().await; |
| 368 | }; | ||
| 369 | let token = Uuid::new_v4(); | ||
| 370 | { | ||
| 371 | let mut map = self.guilds.lock().await; | 375 | let mut map = self.guilds.lock().await; |
| 372 | if let Some(gm) = map.get_mut(&guild) { | 376 | let Some(gm) = map.get_mut(&guild) else { |
| 373 | gm.current = Some(CurrentTrack { | 377 | return Ok(false); |
| 374 | token, | 378 | }; |
| 375 | handle: handle.clone(), | 379 | if gm.generation != generation { |
| 376 | request: request.clone(), | 380 | // Interrupted while preparing; the replacement request is |
| 377 | }); | 381 | // already at the front of the queue. |
| 382 | continue; | ||
| 378 | } | 383 | } |
| 379 | } | 384 | if gm.current.is_some() { |
| 385 | // Another preparation won the race; let it handle this. | ||
| 386 | gm.queue.push_front(request); | ||
| 387 | return Ok(false); | ||
| 388 | } | ||
| 389 | let handle = { | ||
| 390 | let mut call = call.lock().await; | ||
| 391 | call.play_input(input) | ||
| 392 | }; | ||
| 393 | let token = Uuid::new_v4(); | ||
| 394 | gm.current = Some(CurrentTrack { | ||
| 395 | token, | ||
| 396 | handle: handle.clone(), | ||
| 397 | request: request.clone(), | ||
| 398 | }); | ||
| 399 | (handle, token) | ||
| 400 | }; | ||
| 380 | 401 | ||
| 381 | let _ = handle.add_event( | 402 | let _ = handle.add_event( |
| 382 | songbird::Event::Track(songbird::TrackEvent::End), | 403 | songbird::Event::Track(songbird::TrackEvent::End), |
| @@ -405,10 +426,9 @@ impl MusicState { | |||
| 405 | /// Called from the `End` event of a track. Only advances if the track that | 426 | /// Called from the `End` event of a track. Only advances if the track that |
| 406 | /// ended is still the current one. | 427 | /// ended is still the current one. |
| 407 | async fn on_track_end(self: Arc<Self>, guild: u64, token: Uuid) { | 428 | async fn on_track_end(self: Arc<Self>, guild: u64, token: Uuid) { |
| 408 | let lock = self.guild_lock(guild).await; | ||
| 409 | let _transition = lock.lock().await; | ||
| 410 | |||
| 411 | let is_current = { | 429 | let is_current = { |
| 430 | let lock = self.guild_lock(guild).await; | ||
| 431 | let _transition = lock.lock().await; | ||
| 412 | let mut map = self.guilds.lock().await; | 432 | let mut map = self.guilds.lock().await; |
| 413 | match map.get_mut(&guild) { | 433 | match map.get_mut(&guild) { |
| 414 | Some(gm) => match &gm.current { | 434 | Some(gm) => match &gm.current { |
| @@ -423,7 +443,7 @@ impl MusicState { | |||
| 423 | }; | 443 | }; |
| 424 | 444 | ||
| 425 | if is_current { | 445 | if is_current { |
| 426 | if let Err(e) = self.play_next_locked(guild, true).await { | 446 | if let Err(e) = self.play_next(guild, true).await { |
| 427 | error!(guild, error = %e, "failed to advance music queue"); | 447 | error!(guild, error = %e, "failed to advance music queue"); |
| 428 | } | 448 | } |
| 429 | } | 449 | } |
