Commit 7711c00e by Brian Wilson

Pull task_queue.py methods out from tasks.py, to represent API calls

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.
parent d503e331
...@@ -8,22 +8,23 @@ from django.db import models ...@@ -8,22 +8,23 @@ from django.db import models
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'CourseTaskLog' # Adding model 'CourseTaskLog'
db.create_table('courseware_coursetasklog', ( db.create_table('courseware_coursetasklog', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('task_name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('course_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('student', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['auth.User'])), ('student', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', null=True, to=orm['auth.User'])),
('task_name', self.gf('django.db.models.fields.CharField')(max_length=50, db_index=True)),
('task_args', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('task_args', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('task_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), ('task_id', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)),
('task_status', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, db_index=True)), ('task_state', self.gf('django.db.models.fields.CharField')(max_length=50, null=True, db_index=True)),
('task_progress', self.gf('django.db.models.fields.CharField')(max_length=1024, null=True, db_index=True)),
('requester', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['auth.User'])), ('requester', self.gf('django.db.models.fields.related.ForeignKey')(related_name='+', to=orm['auth.User'])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)), ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True, db_index=True, blank=True)),
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)), ('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, db_index=True, blank=True)),
)) ))
db.send_create_signal('courseware', ['CourseTaskLog']) db.send_create_signal('courseware', ['CourseTaskLog'])
def backwards(self, orm): def backwards(self, orm):
# Deleting model 'CourseTaskLog' # Deleting model 'CourseTaskLog'
db.delete_table('courseware_coursetasklog') db.delete_table('courseware_coursetasklog')
...@@ -76,7 +77,8 @@ class Migration(SchemaMigration): ...@@ -76,7 +77,8 @@ class Migration(SchemaMigration):
'task_args': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 'task_args': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'task_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), 'task_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'task_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}), 'task_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'task_status': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'db_index': 'True'}), 'task_progress': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True', 'db_index': 'True'}),
'task_state': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'db_index': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}) 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'})
}, },
'courseware.offlinecomputedgrade': { 'courseware.offlinecomputedgrade': {
......
...@@ -271,12 +271,27 @@ class CourseTaskLog(models.Model): ...@@ -271,12 +271,27 @@ class CourseTaskLog(models.Model):
perform course-specific work. perform course-specific work.
Examples include grading and regrading. Examples include grading and regrading.
""" """
task_name = models.CharField(max_length=50, db_index=True)
course_id = models.CharField(max_length=255, db_index=True) course_id = models.CharField(max_length=255, db_index=True)
student = models.ForeignKey(User, null=True, db_index=True, related_name='+') # optional: None = task applies to all students student = models.ForeignKey(User, null=True, db_index=True, related_name='+') # optional: None = task applies to all students
task_name = models.CharField(max_length=50, db_index=True)
task_args = models.CharField(max_length=255, db_index=True) task_args = models.CharField(max_length=255, db_index=True)
task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta task_id = models.CharField(max_length=255, db_index=True) # max_length from celery_taskmeta
task_status = models.CharField(max_length=50, null=True, db_index=True) # max_length from celery_taskmeta task_state = models.CharField(max_length=50, null=True, db_index=True) # max_length from celery_taskmeta
task_progress = models.CharField(max_length=1024, null=True, db_index=True)
requester = models.ForeignKey(User, db_index=True, related_name='+') requester = models.ForeignKey(User, db_index=True, related_name='+')
created = models.DateTimeField(auto_now_add=True, null=True, db_index=True) created = models.DateTimeField(auto_now_add=True, null=True, db_index=True)
updated = models.DateTimeField(auto_now=True, db_index=True) updated = models.DateTimeField(auto_now=True, db_index=True)
def __repr__(self):
return 'CourseTaskLog<%r>' % ({
'task_name': self.task_name,
'course_id': self.course_id,
'student': self.student.username,
'task_args': self.task_args,
'task_id': self.task_id,
'task_state': self.task_state,
'task_progress': self.task_progress,
},)
def __unicode__(self):
return unicode(repr(self))
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)
...@@ -74,18 +74,14 @@ ...@@ -74,18 +74,14 @@
var task_id = name; var task_id = name;
var task_dict = response[task_id]; var task_dict = response[task_id];
// this should be a dict of properties for this task_id // this should be a dict of properties for this task_id
var in_progress = task_dict.in_progress if (task_dict.in_progress === true) {
if (in_progress === true) {
something_in_progress = true; something_in_progress = true;
} }
// find the corresponding entry, and update it: // find the corresponding entry, and update it:
selector = '[data-task-id="' + task_id + '"]'; entry = $(_this.element).find('[data-task-id="' + task_id + '"]');
entry = $(_this.element).find(selector); entry.find('.task-state').text(task_dict.task_state)
var task_status_el = entry.find('.task-status'); var progress_value = task_dict.message || '';
task_status_el.text(task_dict.task_status) entry.find('.task-progress').text(progress_value);
var task_progress_el = entry.find('.task-progress');
var progress_value = task_dict.task_progress || '';
task_progress_el.text(progress_value);
} }
} }
if (something_in_progress) { if (something_in_progress) {
...@@ -491,7 +487,7 @@ function goto( mode) ...@@ -491,7 +487,7 @@ function goto( mode)
##----------------------------------------------------------------------------- ##-----------------------------------------------------------------------------
## Output tasks in progress ## Output tasks in progress
%if course_tasks is not None: %if course_tasks is not None and len(course_tasks) > 0:
<p>Pending Course Tasks</p> <p>Pending Course Tasks</p>
<div id="task-progress-wrapper"> <div id="task-progress-wrapper">
<table class="stat_table"> <table class="stat_table">
...@@ -503,7 +499,7 @@ function goto( mode) ...@@ -503,7 +499,7 @@ function goto( mode)
<th>Requester</th> <th>Requester</th>
<th>Submitted</th> <th>Submitted</th>
<th>Last Update</th> <th>Last Update</th>
<th>Task Status</th> <th>Task State</th>
<th>Task Progress</th> <th>Task Progress</th>
</tr> </tr>
%for tasknum, course_task in enumerate(course_tasks): %for tasknum, course_task in enumerate(course_tasks):
...@@ -515,7 +511,7 @@ function goto( mode) ...@@ -515,7 +511,7 @@ function goto( mode)
<td>${course_task.requester}</td> <td>${course_task.requester}</td>
<td>${course_task.created}</td> <td>${course_task.created}</td>
<td><div class="task-updated">${course_task.updated}</div></td> <td><div class="task-updated">${course_task.updated}</div></td>
<td><div class="task-status">${course_task.task_status}</div></td> <td><div class="task-state">${course_task.task_state}</div></td>
<td><div class="task-progress">unknown</div></td> <td><div class="task-progress">unknown</div></td>
</tr> </tr>
%endfor %endfor
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment