Просмотр исходного кода

Fix race condition with multiple playlist tasks running

Previously, stopping a playlist only set flags but didn't cancel the
actual asyncio task. When starting a new playlist immediately after:
1. Old task continues running (just between patterns)
2. New playlist clears stop_requested
3. Old task sees stop_requested=False and continues!

Now:
- Track the playlist task in _current_playlist_task
- cancel_current_playlist() properly cancels and awaits the task
- stop_actions() calls cancel_current_playlist() when clearing playlist
- run_playlist() cancels any existing task before starting new one

This prevents "ghost" playlists from running in parallel.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 неделя назад
Родитель
Сommit
27b52408ad
2 измененных файлов с 29 добавлено и 1 удалено
  1. 4 0
      modules/core/pattern_manager.py
  2. 25 1
      modules/core/playlist_manager.py

+ 4 - 0
modules/core/pattern_manager.py

@@ -1369,6 +1369,10 @@ async def stop_actions(clear_playlist = True, wait_for_lock = True):
                 if progress_update_task and not progress_update_task.done():
                     progress_update_task.cancel()
 
+                # Cancel the playlist task itself (late import to avoid circular dependency)
+                from modules.core import playlist_manager
+                await playlist_manager.cancel_current_playlist()
+
             state.pause_condition.notify_all()
 
         # Also set the pause event to wake up any paused patterns

+ 25 - 1
modules/core/playlist_manager.py

@@ -3,6 +3,7 @@ import os
 import threading
 import logging
 import asyncio
+from typing import Optional
 from modules.core import pattern_manager
 from modules.core.state import state
 from fastapi import HTTPException
@@ -10,6 +11,9 @@ from fastapi import HTTPException
 # Configure logging
 logger = logging.getLogger(__name__)
 
+# Track the current playlist task so we can cancel it properly
+_current_playlist_task: Optional[asyncio.Task] = None
+
 # Global state
 PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
 
@@ -113,8 +117,28 @@ def rename_playlist(old_name, new_name):
     logger.info(f"Renamed playlist '{old_name}' to '{new_name}'")
     return True, f"Playlist renamed to '{new_name}'"
 
+async def cancel_current_playlist():
+    """Cancel the current playlist task if one is running."""
+    global _current_playlist_task
+    if _current_playlist_task and not _current_playlist_task.done():
+        logger.info("Cancelling existing playlist task...")
+        _current_playlist_task.cancel()
+        try:
+            await _current_playlist_task
+        except asyncio.CancelledError:
+            logger.info("Playlist task cancelled successfully")
+        except Exception as e:
+            logger.warning(f"Error while cancelling playlist task: {e}")
+        _current_playlist_task = None
+
 async def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run a playlist with the given options."""
+    global _current_playlist_task
+
+    # Cancel any existing playlist task first
+    await cancel_current_playlist()
+
+    # Also stop any running pattern
     if pattern_manager.get_pattern_lock().locked():
         logger.info("Another pattern is running, stopping it first...")
         await pattern_manager.stop_actions()
@@ -135,7 +159,7 @@ async def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode
         logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
         state.current_playlist = file_paths
         state.current_playlist_name = playlist_name
-        asyncio.create_task(
+        _current_playlist_task = asyncio.create_task(
             pattern_manager.run_theta_rho_files(
                 file_paths,
                 pause_time=pause_time,