aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:34:42 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:34:42 +0100
commit0d2f471e9a868518899be4d51cb3029f01079f2e (patch)
treeee92e7528ac3e8c31fc5a76014b055209de4b5c7
parentdocs: sync with current behavior (diff)
downloadsenator-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.rs218
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`].
110pub struct MusicState { 113pub 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 }