diff --git a/ui/pages/__pycache__/downloads.cpython-310.pyc b/ui/pages/__pycache__/downloads.cpython-310.pyc index 0498c913..eaee0250 100644 Binary files a/ui/pages/__pycache__/downloads.cpython-310.pyc and b/ui/pages/__pycache__/downloads.cpython-310.pyc differ diff --git a/ui/pages/__pycache__/downloads.cpython-312.pyc b/ui/pages/__pycache__/downloads.cpython-312.pyc index a1efd24c..47816505 100644 Binary files a/ui/pages/__pycache__/downloads.cpython-312.pyc and b/ui/pages/__pycache__/downloads.cpython-312.pyc differ diff --git a/ui/pages/downloads.py b/ui/pages/downloads.py index a745a00d..b80981ea 100644 --- a/ui/pages/downloads.py +++ b/ui/pages/downloads.py @@ -9612,6 +9612,7 @@ class DownloadsPage(QWidget): def handle_status_update_v2(transfers_data): """Enhanced status update handler with robust state management""" + import time try: if not transfers_data: return @@ -9647,6 +9648,13 @@ class DownloadsPage(QWidget): # Update UI counters self.download_queue.update_tab_counts() + + # Enhanced logging: periodic queue health summary + if not hasattr(self, '_last_queue_summary_time'): + self._last_queue_summary_time = 0 + if time.time() - self._last_queue_summary_time > 30: # Every 30 seconds + self._log_queue_health_summary(download_items) + self._last_queue_summary_time = time.time() except Exception as e: print(f"[ERROR] Status update v2 failed: {e}") @@ -9669,6 +9677,45 @@ class DownloadsPage(QWidget): except Exception as e: print(f"[ERROR] Failed to start status update thread v2: {e}") + def _log_queue_health_summary(self, download_items): + """Log periodic queue health summary for debugging stuck downloads""" + import time + + try: + if not download_items: + return + + # Count downloads by status + status_counts = {} + stuck_downloads = [] + + for item in download_items: + status = item.status.lower() + status_counts[status] = status_counts.get(status, 0) + 1 + + # Check for potentially stuck downloads + if hasattr(item, 'queue_start_time') and item.queue_start_time: + queue_age = time.time() - item.queue_start_time + if queue_age > 15: # More than 15 seconds in queue + stuck_downloads.append((item.title, queue_age)) + + # Check for items with high API missing counts + if hasattr(item, 'api_missing_count_v2') and item.api_missing_count_v2 > 1: + stuck_downloads.append((item.title, f"API missing {item.api_missing_count_v2} cycles")) + + # Log summary + print(f"📊 Queue Health: {status_counts}") + if stuck_downloads: + print(f"⚠️ Potentially stuck downloads: {len(stuck_downloads)}") + for title, issue in stuck_downloads[:3]: # Show first 3 + if isinstance(issue, str): + print(f" - {title}: {issue}") + else: + print(f" - {title}: queued for {issue:.1f}s") + + except Exception as e: + pass # Silent logging failures + def _find_matching_transfer_v2(self, download_item, all_transfers, matched_ids): """Enhanced transfer matching with better ID tracking""" # Primary: ID-based matching @@ -9694,10 +9741,16 @@ class DownloadsPage(QWidget): return None def _process_transfer_match_v2(self, download_item, transfer): - """Process matched transfer with atomic state updates""" + """Process matched transfer with atomic state updates and progressive timeout logic""" + import time + state = transfer.get('state', 'Unknown') progress = min(100, max(0, transfer.get('percentComplete', 0))) + # Initialize queue tracking if needed + if not hasattr(download_item, 'queue_start_time'): + download_item.queue_start_time = None + # Determine new status if 'Completed' in state or 'Succeeded' in state: new_status = 'completed' @@ -9711,6 +9764,16 @@ class DownloadsPage(QWidget): else: new_status = 'queued' + # Track queue state transitions for progressive timeout + if new_status in ['queued', 'initializing'] and download_item.queue_start_time is None: + download_item.queue_start_time = time.time() + print(f"🕐 Download entered queue: {download_item.title}") + elif new_status in ['downloading', 'completed', 'cancelled', 'failed']: + if download_item.queue_start_time: + queue_duration = time.time() - download_item.queue_start_time + print(f"⏱️ Download '{download_item.title}' was in queue for {queue_duration:.1f}s") + download_item.queue_start_time = None # Reset queue timer + # Atomic status update self._queue_manager.atomic_state_transition( download_item, new_status, @@ -9725,6 +9788,11 @@ class DownloadsPage(QWidget): file_path=transfer.get('filename', download_item.file_path) ) + # Reset API missing count when transfer is found and add enhanced logging + if hasattr(download_item, 'api_missing_count_v2') and download_item.api_missing_count_v2 > 0: + print(f"✅ Download reconnected to API after {download_item.api_missing_count_v2} missing cycles: {download_item.title}") + download_item.api_missing_count_v2 = 0 + # Update ID mapping if needed transfer_id = transfer.get('id') if transfer_id and (not hasattr(download_item, 'download_id') or download_item.download_id != transfer_id): @@ -9751,14 +9819,40 @@ class DownloadsPage(QWidget): self._schedule_fallback_cleanup_check() def _handle_missing_transfer_v2(self, download_item): - """Handle downloads missing from API with improved grace period""" + """Handle downloads missing from API with improved grace period and queue age tracking""" + import time + + # Initialize tracking attributes if not hasattr(download_item, 'api_missing_count_v2'): download_item.api_missing_count_v2 = 0 + if not hasattr(download_item, 'queue_start_time'): + download_item.queue_start_time = None + + # Track when downloads first enter queued state + current_status = download_item.status.lower() + if current_status in ['queued', 'initializing'] and download_item.queue_start_time is None: + download_item.queue_start_time = time.time() + print(f"🕐 Started tracking queue time for: {download_item.title}") + elif current_status not in ['queued', 'initializing']: + download_item.queue_start_time = None # Reset if no longer queued download_item.api_missing_count_v2 += 1 - # Longer grace period for stability - if download_item.api_missing_count_v2 >= 5: # 5 polling cycles + # Enhanced timeout logic with queue age consideration + queue_timeout_exceeded = False + if download_item.queue_start_time: + queue_age = time.time() - download_item.queue_start_time + queue_timeout = 180.0 # 3 minutes max in queue + if queue_age > queue_timeout: + queue_timeout_exceeded = True + print(f"⏰ Queue timeout exceeded: {download_item.title} stuck in queue for {queue_age:.1f}s") + + # Fail download if API missing for 3 cycles OR queue timeout exceeded + if download_item.api_missing_count_v2 >= 3 or queue_timeout_exceeded: + if queue_timeout_exceeded: + print(f"⚠️ Download failed due to queue timeout: {download_item.title}") + else: + print(f"⚠️ Download missing from API for {download_item.api_missing_count_v2} cycles: {download_item.title}") self._queue_manager.atomic_state_transition(download_item, 'failed') self.download_queue.move_to_finished(download_item)