from client. Tasks.py remains the task implementations running on the celery worker. In particular, move status message generation out of task thread to client side.
208 lines
8.6 KiB
Python
208 lines
8.6 KiB
Python
import json
|
|
import logging
|
|
from django.http import HttpResponse
|
|
|
|
from celery.result import AsyncResult
|
|
from celery.states import READY_STATES
|
|
|
|
from courseware.models import CourseTaskLog
|
|
from courseware.tasks import regrade_problem_for_all_students
|
|
from xmodule.modulestore.django import modulestore
|
|
|
|
|
|
# define different loggers for use within tasks and on client side
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def get_running_course_tasks(course_id):
|
|
course_tasks = CourseTaskLog.objects.filter(course_id=course_id)
|
|
# exclude(task_state='SUCCESS').exclude(task_state='FAILURE').exclude(task_state='REVOKED')
|
|
for state in READY_STATES:
|
|
course_tasks = course_tasks.exclude(task_state=state)
|
|
return course_tasks
|
|
|
|
|
|
def _task_is_running(course_id, task_name, task_args, student=None):
|
|
runningTasks = CourseTaskLog.objects.filter(course_id=course_id, task_name=task_name, task_args=task_args)
|
|
if student is not None:
|
|
runningTasks = runningTasks.filter(student=student)
|
|
for state in READY_STATES:
|
|
runningTasks = runningTasks.exclude(task_state=state)
|
|
return len(runningTasks) > 0
|
|
|
|
|
|
def submit_regrade_problem_for_all_students(request, course_id, problem_url):
|
|
# check arguments: in particular, make sure that problem_url is defined
|
|
# (since that's currently typed in). If the corresponding module descriptor doesn't exist,
|
|
# an exception should be raised. Let it continue to the caller.
|
|
modulestore().get_instance(course_id, problem_url)
|
|
|
|
# TODO: adjust transactions so that one request will not be about to create an
|
|
# entry while a second is testing to see if the entry exists. (Need to handle
|
|
# quick accidental double-clicks when submitting.)
|
|
|
|
# check to see if task is already running
|
|
task_name = 'regrade'
|
|
if _task_is_running(course_id, task_name, problem_url):
|
|
# TODO: figure out how to return info that it's already running
|
|
raise Exception("task is already running")
|
|
|
|
# Create log entry now, so that future requests won't
|
|
tasklog_args = {'course_id': course_id,
|
|
'task_name': task_name,
|
|
'task_args': problem_url,
|
|
'task_state': 'QUEUING',
|
|
'requester': request.user}
|
|
course_task_log = CourseTaskLog.objects.create(**tasklog_args)
|
|
|
|
|
|
# At a low level of processing, the task currently fetches some information from the web request.
|
|
# This is used for setting up X-Queue, as well as for tracking.
|
|
# An actual request will not successfully serialize with json or with pickle.
|
|
# TODO: we can just pass all META info as a dict.
|
|
request_environ = {'HTTP_USER_AGENT': request.META['HTTP_USER_AGENT'],
|
|
'REMOTE_ADDR': request.META['REMOTE_ADDR'],
|
|
'SERVER_NAME': request.META['SERVER_NAME'],
|
|
'REQUEST_METHOD': 'GET',
|
|
# 'HTTP_X_FORWARDED_PROTO': request.META['HTTP_X_FORWARDED_PROTO'],
|
|
}
|
|
|
|
# Submit task:
|
|
task_args = [request_environ, course_id, problem_url]
|
|
result = regrade_problem_for_all_students.apply_async(task_args)
|
|
|
|
# Put info into table with the resulting task_id.
|
|
course_task_log.task_state = result.state
|
|
course_task_log.task_id = result.id
|
|
course_task_log.save()
|
|
return course_task_log
|
|
|
|
|
|
def course_task_log_status(request, task_id=None):
|
|
"""
|
|
This returns the status of a course-related task as a JSON-serialized dict.
|
|
"""
|
|
output = {}
|
|
if task_id is not None:
|
|
output = _get_course_task_log_status(task_id)
|
|
elif 'task_id' in request.POST:
|
|
task_id = request.POST['task_id']
|
|
output = _get_course_task_log_status(task_id)
|
|
elif 'task_ids[]' in request.POST:
|
|
tasks = request.POST.getlist('task_ids[]')
|
|
for task_id in tasks:
|
|
task_output = _get_course_task_log_status(task_id)
|
|
if task_output is not None:
|
|
output[task_id] = task_output
|
|
# TODO decide whether to raise exception if bad args are passed.
|
|
# May be enough just to return an empty output.
|
|
|
|
return HttpResponse(json.dumps(output, indent=4))
|
|
|
|
|
|
def _get_course_task_log_status(task_id):
|
|
"""
|
|
Get the status for a given task_id.
|
|
|
|
Returns a dict, with the following keys:
|
|
'task_id'
|
|
'task_state'
|
|
'in_progress': boolean indicating if the task is still running.
|
|
'task_traceback': optional, returned if task failed and produced a traceback.
|
|
|
|
If task doesn't exist, returns None.
|
|
"""
|
|
# First check if the task_id is known
|
|
try:
|
|
course_task_log_entry = CourseTaskLog.objects.get(task_id=task_id)
|
|
except CourseTaskLog.DoesNotExist:
|
|
# TODO: log a message here
|
|
return None
|
|
|
|
output = {}
|
|
|
|
# if the task is already known to be done, then there's no reason to query
|
|
# the underlying task:
|
|
if course_task_log_entry.task_state not in READY_STATES:
|
|
# we need to get information from the task result directly now.
|
|
# Just create the result object.
|
|
result = AsyncResult(task_id)
|
|
|
|
if result.traceback is not None:
|
|
output['task_traceback'] = result.traceback
|
|
|
|
if result.state == "PROGRESS":
|
|
# construct a status message directly from the task result's metadata:
|
|
if hasattr(result, 'result') and 'current' in result.result:
|
|
fmt = "Attempted {attempted} of {total}, {action_name} {updated}"
|
|
message = fmt.format(attempted=result.result['attempted'],
|
|
updated=result.result['updated'],
|
|
total=result.result['total'],
|
|
action_name=result.result['action_name'])
|
|
output['message'] = message
|
|
log.info("progress: {0}".format(message))
|
|
for name in ['attempted', 'updated', 'total', 'action_name']:
|
|
output[name] = result.result[name]
|
|
else:
|
|
log.info("still making progress... ")
|
|
|
|
# update the entry if the state has changed:
|
|
if result.state != course_task_log_entry.task_state:
|
|
course_task_log_entry.task_state = result.state
|
|
course_task_log_entry.save()
|
|
|
|
output['task_id'] = course_task_log_entry.task_id
|
|
output['task_state'] = course_task_log_entry.task_state
|
|
output['in_progress'] = course_task_log_entry.task_state not in READY_STATES
|
|
|
|
if course_task_log_entry.task_progress is not None:
|
|
output['task_progress'] = course_task_log_entry.task_progress
|
|
|
|
if course_task_log_entry.task_state == 'SUCCESS':
|
|
succeeded, message = _get_task_completion_message(course_task_log_entry)
|
|
output['message'] = message
|
|
output['succeeded'] = succeeded
|
|
|
|
return output
|
|
|
|
|
|
def _get_task_completion_message(course_task_log_entry):
|
|
"""
|
|
Construct progress message from progress information in CourseTaskLog entry.
|
|
|
|
Returns (boolean, message string) duple.
|
|
"""
|
|
succeeded = False
|
|
|
|
if course_task_log_entry.task_progress is None:
|
|
log.warning("No task_progress information found for course_task {0}".format(course_task_log_entry.task_id))
|
|
return (succeeded, "No status information available")
|
|
|
|
task_progress = json.loads(course_task_log_entry.task_progress)
|
|
action_name = task_progress['action_name']
|
|
num_attempted = task_progress['attempted']
|
|
num_updated = task_progress['updated']
|
|
# num_total = task_progress['total']
|
|
if course_task_log_entry.student is not None:
|
|
if num_attempted == 0:
|
|
msg = "Unable to find submission to be {action} for student '{student}' and problem '{problem}'."
|
|
elif num_updated == 0:
|
|
msg = "Problem failed to be {action} for student '{student}' and problem '{problem}'!"
|
|
else:
|
|
succeeded = True
|
|
msg = "Problem successfully {action} for student '{student}' and problem '{problem}'"
|
|
elif num_attempted == 0:
|
|
msg = "Unable to find any students with submissions to be {action} for problem '{problem}'."
|
|
elif num_updated == 0:
|
|
msg = "Problem failed to be {action} for any of {attempted} students for problem '{problem}'!"
|
|
elif num_updated == num_attempted:
|
|
succeeded = True
|
|
msg = "Problem successfully {action} for {attempted} students for problem '{problem}'!"
|
|
elif num_updated < num_attempted:
|
|
msg = "Problem {action} for {updated} of {attempted} students for problem '{problem}'!"
|
|
|
|
# Update status in task result object itself:
|
|
message = msg.format(action=action_name, updated=num_updated, attempted=num_attempted,
|
|
student=course_task_log_entry.student, problem=course_task_log_entry.task_args)
|
|
return (succeeded, message)
|