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) <noreply@anthropic.com>
This commit is contained in:
nickviljoen 2026-04-16 15:20:56 +02:00
parent d0d7110836
commit 8a7d477c86
4 changed files with 35 additions and 27 deletions

View file

@ -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

View file

@ -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

View file

@ -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}")

View file

@ -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()