Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 177 additions & 33 deletions backend/routes/tasks.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,108 @@
from flask import Blueprint, jsonify, request
from flask import Blueprint, jsonify, request, current_app
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: current_app is imported but never referenced in this module. Please remove it to avoid linting/maintenance noise.

Suggested change
from flask import Blueprint, jsonify, request, current_app
from flask import Blueprint, jsonify, request

Copilot uses AI. Check for mistakes.
from datetime import datetime, timezone
import uuid
import json
import time
import requests
import shlex
import logging
from services.task_service import get_submitted_tasks, add_task, update_single_task_status
from utils.tes_utils import load_tes_instances
from utils.auth_utils import get_instance_credentials

logger = logging.getLogger(__name__)

tasks_bp = Blueprint('tasks', __name__)

def build_failed_task(tes_task, tes_url, tes_name, tes_endpoint=None,
error_message=None, error_type=None, error_code=None,
error_reason=None, http_status_code=None):
"""
Construct a standardized failed task dictionary.

Args:
tes_task: The TES task specification dict
tes_url: TES instance URL
tes_name: TES instance name
tes_endpoint: Optional TES endpoint used (if reached that stage)
error_message: Human-readable error description
error_type: Category of error (e.g., 'connection_error', 'bad_request')
error_code: Machine-readable error code
error_reason: Detailed reason for the error
http_status_code: HTTP status code if applicable

Returns:
Dict containing failed task with SUBMISSION_FAILED state
"""
now_iso = datetime.now(timezone.utc).isoformat()

failed_task = {
'id': str(uuid.uuid4()),
'task_id': 'N/A',
'name': tes_task['name'],
'task_name': tes_task['name'],
'description': tes_task['description'],
'state': 'SUBMISSION_FAILED',
Comment on lines +39 to +45
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

build_failed_task hard-codes task_id to 'N/A' while generating a unique UUID in id. The frontend (taskService.listTasks) prefers task.task_id || task.id, so all failed submissions end up with the same displayed id ('N/A'), causing React key collisions and making task details/logs resolve to the wrong failed task. Make the failed submission identifier unique (e.g., set task_id to the generated UUID, or drop task_id and update the frontend mapping), and keep the TES task id as a separate nullable field if needed.

Copilot uses AI. Check for mistakes.
'status': 'SUBMISSION_FAILED',
'creation_time': now_iso,
'submitted_at': now_iso,
'start_time': None,
'end_time': None,
'tes_url': tes_url,
'tes_name': tes_name,
'tes_endpoint': tes_endpoint,
'inputs': tes_task.get('inputs', []),
'outputs': tes_task.get('outputs', []),
'resources': tes_task.get('resources', {}),
'executors': tes_task.get('executors', []),
'volumes': [],
'tags': {}
}

# Add error details if provided
if error_message:
failed_task['error_message'] = error_message
if error_type:
failed_task['error_type'] = error_type
if error_code:
failed_task['error_code'] = error_code
if error_reason:
failed_task['error_reason'] = error_reason
if http_status_code:
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

http_status_code is added to the failed task only when truthy (if http_status_code:). For consistency with build_error_response (which checks is not None) and to avoid accidentally dropping valid falsy values, check http_status_code is not None instead.

Suggested change
if http_status_code:
if http_status_code is not None:

Copilot uses AI. Check for mistakes.
failed_task['http_status_code'] = http_status_code

return failed_task

def build_error_response(success=False, error=None, error_type=None, error_code=None,
reason=None, dashboard_task_id=None, tes_url=None, tes_name=None,
tes_endpoint=None, status_code=None):
"""
Construct a standardized error response.

Args:
dashboard_task_id: The local dashboard task record ID (UUID) for failed submissions
status_code: HTTP status code from TES response (if applicable)

Returns:
Dict containing consistent error response structure
"""
response = {
'success': success,
'error': error,
'error_type': error_type,
'error_code': error_code,
'reason': reason,
'dashboard_task_id': dashboard_task_id, # Local dashboard record ID (not TES task_id)
'tes_url': tes_url,
'tes_name': tes_name,
'tes_endpoint': tes_endpoint
}

if status_code is not None:
response['status_code'] = status_code

return response

@tasks_bp.route('/api/tasks', methods=['GET'])
def get_tasks():
return jsonify(get_submitted_tasks())
Expand All @@ -36,9 +129,33 @@ def submit_task():
tes_name = inst['name']
break

# bug: demo python script task does not complete #12
# Parse command - handle both list and string formats with proper shell-like quoting
command_input = data.get('command')
if isinstance(command_input, list):
command = command_input
elif isinstance(command_input, str) and command_input:
try:
# Use shlex.split() to properly handle quoted arguments
command = shlex.split(command_input)
except ValueError as e:
# Log parsing error and return 400 for invalid command syntax
error_msg = f"Invalid command syntax: {str(e)}. Command contains unmatched quotes or invalid escape sequences."
logger.error(f"Command parsing failed for '{command_input}': {e}")
return jsonify({
'success': False,
'error': error_msg,
'error_type': 'invalid_command',
'error_code': 'INVALID_COMMAND_SYNTAX',
'reason': 'The command string contains syntax errors. Check for unmatched quotes or invalid escape sequences.',
'invalid_command': command_input
}), 400
else:
command = ['echo', 'Hello World']

executor = {
"image": docker_image,
"command": data.get('command') if isinstance(data.get('command'), list) else (data.get('command', '').split() if data.get('command') else ['echo', 'Hello World']),
"command": command,
"workdir": data.get('workdir', '/tmp')
}

Expand Down Expand Up @@ -176,26 +293,37 @@ def submit_task():

if not service_is_reachable:
print(f"❌ All service-info endpoints failed for {tes_name}")
if connectivity_error_info:
return jsonify({
'success': False,
'error': connectivity_error_info['message'],
'error_type': connectivity_error_info['error_type'],
'error_code': connectivity_error_info['error_code'],
'reason': connectivity_error_info['reason'],
'tes_url': tes_url,
'tes_name': tes_name
}), 503
else:
return jsonify({
'success': False,
'error': 'Could not reach TES instance',
'error_type': 'service_unavailable',
'error_code': 'SERVICE_UNAVAILABLE',
'reason': 'None of the service-info endpoints responded',
'tes_url': tes_url,
'tes_name': tes_name
}), 503

# feat: add failed submission tasks to task management #11
error_message = connectivity_error_info['message'] if connectivity_error_info else 'Could not reach TES instance'
error_type = connectivity_error_info['error_type'] if connectivity_error_info else 'service_unavailable'
error_code = connectivity_error_info['error_code'] if connectivity_error_info else 'SERVICE_UNAVAILABLE'
error_reason = connectivity_error_info['reason'] if connectivity_error_info else 'None of the service-info endpoints responded'

failed_task = build_failed_task(
tes_task=tes_task,
tes_url=tes_url,
tes_name=tes_name,
tes_endpoint=None,
error_message=error_message,
error_type=error_type,
error_code=error_code,
error_reason=error_reason
)

add_task(failed_task)

return jsonify(build_error_response(
success=False,
error=error_message,
error_type=error_type,
error_code=error_code,
reason=error_reason,
dashboard_task_id=failed_task['id'],
tes_url=tes_url,
tes_name=tes_name,
tes_endpoint=None
)), 503

tes_endpoint = working_endpoint
print(f"🚀 Submitting task to {tes_endpoint}")
Expand Down Expand Up @@ -331,17 +459,33 @@ def submit_task():
response_text = response.text[:200] if response.text else 'No error details provided'
error_msg = f'{error_msg}: {response_text}'

return jsonify({
'success': False,
'error': error_msg,
'error_type': error_info['error_type'],
'error_code': error_info['error_code'],
'reason': error_info['reason'],
'tes_endpoint': tes_endpoint,
'tes_url': tes_url,
'tes_name': tes_name,
'status_code': response.status_code
}), 400
# feat: add failed submission tasks to task management #11
failed_task = build_failed_task(
tes_task=tes_task,
tes_url=tes_url,
tes_name=tes_name,
tes_endpoint=tes_endpoint,
error_message=error_msg,
error_type=error_info['error_type'],
error_code=error_info['error_code'],
error_reason=error_info['reason'],
http_status_code=response.status_code
)

add_task(failed_task)

return jsonify(build_error_response(
success=False,
error=error_msg,
error_type=error_info['error_type'],
error_code=error_info['error_code'],
reason=error_info['reason'],
dashboard_task_id=failed_task['id'],
tes_url=tes_url,
tes_name=tes_name,
tes_endpoint=tes_endpoint,
status_code=response.status_code
)), response.status_code

except Exception as e:
import traceback
Expand Down
2 changes: 1 addition & 1 deletion backend/services/task_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def update_single_task_status(task):
return False

def update_task_statuses():
terminal_states = ['COMPLETE', 'CANCELED', 'SYSTEM_ERROR', 'EXECUTOR_ERROR', 'PREEMPTED']
terminal_states = ['COMPLETE', 'CANCELED', 'SYSTEM_ERROR', 'EXECUTOR_ERROR', 'PREEMPTED', 'SUBMISSION_FAILED']
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

terminal_states is now defined in update_task_statuses, but a separate hard-coded terminal-state list still exists in update_single_task_status (used for the "Verified task in terminal state" branch). Consider centralizing terminal states in a single constant so these lists don’t drift over time.

Copilot uses AI. Check for mistakes.

while True:
try:
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/pages/Tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,7 @@ const Tasks = () => {
return task &&
task.id &&
task.tes_url &&
task.state &&
task.state !== 'ERROR' &&
task.state !== 'SYSTEM_ERROR' &&
task.state !== 'EXECUTOR_ERROR' &&
!task.error_prone_instance;
task.state;
});

if (!searchTerm) {
Expand Down
Loading