From 8a7d477c86595bce141b1eeff20c0f716ed2ea7b Mon Sep 17 00:00:00 2001 From: nickviljoen Date: Thu, 16 Apr 2026 15:20:56 +0200 Subject: [PATCH] Fix batch QC: add Flask app context to ThreadPoolExecutor child threads ThreadPoolExecutor workers don't inherit the parent thread's Flask app context, causing "Working outside of application context" errors during batch QC execution. Pass the app instance into BatchQCExecutor and wrap each child thread's work with app.app_context(). Also ensure the progress_sessions table is created on fresh databases. Co-Authored-By: Claude Opus 4.6 (1M context) --- core/models/database.py | 1 + core/utils/progress_tracker.py | 1 - modules/hm_qc/batch_executor.py | 53 +++++++++++++++++++-------------- modules/hm_qc/routes.py | 7 +++-- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/core/models/database.py b/core/models/database.py index 15327ea..936c26b 100644 --- a/core/models/database.py +++ b/core/models/database.py @@ -22,6 +22,7 @@ def init_db(app): from . import qc_report from . import usage_log from . import campaign_presentation + from core.utils.progress_tracker import ProgressSession # noqa: F401 # Create all tables (if they don't exist) # Tables are created automatically on first access diff --git a/core/utils/progress_tracker.py b/core/utils/progress_tracker.py index 1532734..bc37efb 100644 --- a/core/utils/progress_tracker.py +++ b/core/utils/progress_tracker.py @@ -8,7 +8,6 @@ import json import time from datetime import datetime from typing import Optional, Dict, Any -from flask import session from core.models.database import db diff --git a/modules/hm_qc/batch_executor.py b/modules/hm_qc/batch_executor.py index 6b8806f..c59e0b2 100644 --- a/modules/hm_qc/batch_executor.py +++ b/modules/hm_qc/batch_executor.py @@ -37,7 +37,8 @@ class BatchQCExecutor: job_number: str = None, batch_size: int = DEFAULT_BATCH_SIZE, campaign_id: str = None, - batch_id: str = None + batch_id: str = None, + app=None ): """ Initialize batch executor. @@ -50,6 +51,7 @@ class BatchQCExecutor: batch_size: Number of files per batch (default 10) campaign_id: Optional campaign ID to load presentation guidelines batch_id: Optional batch ID for grouping reports from the same upload + app: Flask app instance (required for ThreadPoolExecutor child threads) """ self.session_id = session_id self.file_paths = file_paths[:MAX_FILES] @@ -58,6 +60,7 @@ class BatchQCExecutor: self.campaign_id = campaign_id self.batch_id = batch_id self.batch_size = batch_size + self.app = app self.progress = UnifiedProgressTracker(session_id) self.results = [] @@ -165,6 +168,9 @@ class BatchQCExecutor: """ Process a single file using QCExecutor. + Runs inside a ThreadPoolExecutor child thread, so we must push + an app context explicitly (child threads don't inherit it). + Errors are captured and returned, not raised. Args: @@ -177,31 +183,32 @@ class BatchQCExecutor: """ filename = os.path.basename(file_path) try: - # Create a per-file executor with a sub-session ID - # We don't use its progress tracker—we track at the batch level - file_session_id = f"{self.session_id}_file_{completed}" + with self.app.app_context(): + # Create a per-file executor with a sub-session ID + # We don't use its progress tracker—we track at the batch level + file_session_id = f"{self.session_id}_file_{completed}" - executor = QCExecutor( - session_id=file_session_id, - file_path=file_path, - profile=self.profile, - job_number=self.job_number, - campaign_id=self.campaign_id, - batch_id=self.batch_id - ) + executor = QCExecutor( + session_id=file_session_id, + file_path=file_path, + profile=self.profile, + job_number=self.job_number, + campaign_id=self.campaign_id, + batch_id=self.batch_id + ) - result = executor.execute() + result = executor.execute() - return { - 'filename': filename, - 'file_path': file_path, - 'success': result.get('success', False), - 'score': result.get('overall_score'), - 'status': result.get('overall_status', 'error'), - 'report_path': result.get('report_path'), - 'report_id': result.get('report_id'), - 'error': result.get('error') - } + return { + 'filename': filename, + 'file_path': file_path, + 'success': result.get('success', False), + 'score': result.get('overall_score'), + 'status': result.get('overall_status', 'error'), + 'report_path': result.get('report_path'), + 'report_id': result.get('report_id'), + 'error': result.get('error') + } except Exception as e: logger.error(f"Error processing file {filename}: {e}") diff --git a/modules/hm_qc/routes.py b/modules/hm_qc/routes.py index fbf0cde..6cb5708 100644 --- a/modules/hm_qc/routes.py +++ b/modules/hm_qc/routes.py @@ -317,17 +317,18 @@ def execute_batch(): campaign_id = data.get('campaign_id') batch_id = str(uuid.uuid4()) + app = current_app._get_current_object() + batch_executor = BatchQCExecutor( session_id=session_id, file_paths=file_paths, profile=profile, job_number=job_number, campaign_id=campaign_id, - batch_id=batch_id + batch_id=batch_id, + app=app ) - app = current_app._get_current_object() - def run_batch(): with app.app_context(): result = batch_executor.execute()