Commit 1984cfec by Brian Wilson

Add duration to task status. Add tests for reset-attempts.

parent 297206f2
...@@ -224,6 +224,7 @@ def _get_course_task_log_status(task_id): ...@@ -224,6 +224,7 @@ def _get_course_task_log_status(task_id):
'updated': number of attempts that "succeeded" 'updated': number of attempts that "succeeded"
'total': number of possible subtasks to attempt 'total': number of possible subtasks to attempt
'action_name': user-visible verb to use in status messages. Should be past-tense. 'action_name': user-visible verb to use in status messages. Should be past-tense.
'duration_ms': how long the task has (or had) been running.
'task_traceback': optional, returned if task failed and produced a traceback. 'task_traceback': optional, returned if task failed and produced a traceback.
'succeeded': on complete tasks, indicates if the task outcome was successful: 'succeeded': on complete tasks, indicates if the task outcome was successful:
did it achieve what it set out to do. did it achieve what it set out to do.
......
import json import json
from time import sleep from time import sleep, time
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import transaction from django.db import transaction
from celery import task, current_task from celery import task, current_task
# from celery.signals import worker_ready
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
import mitxmako.middleware as middleware import mitxmako.middleware as middleware
from courseware.models import StudentModule from courseware.models import StudentModule
from courseware.model_data import ModelDataCache from courseware.model_data import ModelDataCache
# from courseware.module_render import get_module
from courseware.module_render import get_module_for_descriptor_internal from courseware.module_render import get_module_for_descriptor_internal
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -40,14 +39,18 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc ...@@ -40,14 +39,18 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
fmt = 'Starting to update problem modules as task "{task_id}": course "{course_id}" problem "{state_key}": nothing {action} yet' fmt = 'Starting to update problem modules as task "{task_id}": course "{course_id}" problem "{state_key}": nothing {action} yet'
task_log.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, action=action_name)) task_log.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, action=action_name))
# get start time for task:
start_time = time()
# add task_id to xmodule_instance_args, so that it can be output with tracking info: # add task_id to xmodule_instance_args, so that it can be output with tracking info:
xmodule_instance_args['task_id'] = task_id xmodule_instance_args['task_id'] = task_id
# add hack so that mako templates will work on celery worker server: # Hack to get mako templates to work on celery worker server's worker thread.
# The initialization of Make templating is usually done when Django is # The initialization of Mako templating is usually done when Django is
# initializing middleware packages as part of processing a server request. # initializing middleware packages as part of processing a server request.
# When this is run on a celery worker server, no such initialization is # When this is run on a celery worker server, no such initialization is
# called. So we look for the result: the defining of the lookup paths # called. Using @worker_ready.connect doesn't run in the right container.
# So we look for the result: the defining of the lookup paths
# for templates. # for templates.
if 'main' not in middleware.lookup: if 'main' not in middleware.lookup:
task_log.info("Initializing Mako middleware explicitly") task_log.info("Initializing Mako middleware explicitly")
...@@ -74,14 +77,16 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc ...@@ -74,14 +77,16 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
num_total = len(modules_to_update) # TODO: make this more efficient. Count()? num_total = len(modules_to_update) # TODO: make this more efficient. Count()?
def get_task_progress(): def get_task_progress():
current_time = time()
progress = {'action_name': action_name, progress = {'action_name': action_name,
'attempted': num_attempted, 'attempted': num_attempted,
'updated': num_updated, 'updated': num_updated,
'total': num_total, 'total': num_total,
'start_ms': int(start_time * 1000),
'duration_ms': int((current_time - start_time) * 1000),
} }
return progress return progress
for module_to_update in modules_to_update: for module_to_update in modules_to_update:
num_attempted += 1 num_attempted += 1
# There is no try here: if there's an error, we let it throw, and the task will # There is no try here: if there's an error, we let it throw, and the task will
...@@ -102,7 +107,8 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc ...@@ -102,7 +107,8 @@ def _update_problem_module_state(course_id, module_state_key, student, update_fc
task_progress = get_task_progress() task_progress = get_task_progress()
current_task.update_state(state='PROGRESS', meta=task_progress) current_task.update_state(state='PROGRESS', meta=task_progress)
task_log.info("Finished processing task") fmt = 'Finishing task "{task_id}": course "{course_id}" problem "{state_key}": final: {progress}'
task_log.info(fmt.format(task_id=task_id, course_id=course_id, state_key=module_state_key, progress=task_progress))
return task_progress return task_progress
...@@ -288,11 +294,15 @@ def delete_problem_state_for_all_students(course_id, problem_url, xmodule_instan ...@@ -288,11 +294,15 @@ def delete_problem_state_for_all_students(course_id, problem_url, xmodule_instan
xmodule_instance_args=xmodule_instance_args) xmodule_instance_args=xmodule_instance_args)
# Using @worker_ready.connect was an effort to call middleware initialization
# only once, when the worker was coming up. However, the actual worker task
# was not getting initialized, so it was likely running in a separate process
# from the worker server.
#@worker_ready.connect #@worker_ready.connect
#def initialize_middleware(**kwargs): #def initialize_middleware(**kwargs):
# # The initialize Django middleware - some middleware components # # Initialize Django middleware - some middleware components
# # are initialized lazily when the first request is served. Since # # are initialized lazily when the first request is served. Since
# # the celery workers do not serve request, the components never # # the celery workers do not serve requests, the components never
# # get initialized, causing errors in some dependencies. # # get initialized, causing errors in some dependencies.
# # In particular, the Mako template middleware is used by some xmodules # # In particular, the Mako template middleware is used by some xmodules
# task_log.info("Initializing all middleware from worker_ready.connect hook") # task_log.info("Initializing all middleware from worker_ready.connect hook")
......
...@@ -19,7 +19,8 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory, AdminF ...@@ -19,7 +19,8 @@ from student.tests.factories import CourseEnrollmentFactory, UserFactory, AdminF
from courseware.model_data import StudentModule from courseware.model_data import StudentModule
from courseware.task_queue import (submit_regrade_problem_for_all_students, from courseware.task_queue import (submit_regrade_problem_for_all_students,
submit_regrade_problem_for_student, submit_regrade_problem_for_student,
course_task_log_status) course_task_log_status,
submit_reset_problem_attempts_for_all_students)
from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE from courseware.tests.tests import LoginEnrollmentTestCase, TEST_DATA_MONGO_MODULESTORE
...@@ -228,7 +229,7 @@ class TestRegrading(TestRegradingBase): ...@@ -228,7 +229,7 @@ class TestRegrading(TestRegradingBase):
self.create_student('u4') self.create_student('u4')
self.logout() self.logout()
def testRegradingOptionProblem(self): def test_regrading_option_problem(self):
'''Run regrade scenario on option problem''' '''Run regrade scenario on option problem'''
# get descriptor: # get descriptor:
problem_url_name = 'H1P1' problem_url_name = 'H1P1'
...@@ -280,7 +281,7 @@ class TestRegrading(TestRegradingBase): ...@@ -280,7 +281,7 @@ class TestRegrading(TestRegradingBase):
display_name=str(problem_url_name), display_name=str(problem_url_name),
data=problem_xml) data=problem_xml)
def testRegradingFailure(self): def test_regrading_failure(self):
"""Simulate a failure in regrading a problem""" """Simulate a failure in regrading a problem"""
problem_url_name = 'H1P1' problem_url_name = 'H1P1'
self.define_option_problem(problem_url_name) self.define_option_problem(problem_url_name)
...@@ -307,19 +308,19 @@ class TestRegrading(TestRegradingBase): ...@@ -307,19 +308,19 @@ class TestRegrading(TestRegradingBase):
status = json.loads(response.content) status = json.loads(response.content)
self.assertEqual(status['message'], expected_message) self.assertEqual(status['message'], expected_message)
def testRegradingNonProblem(self): def test_regrading_non_problem(self):
"""confirm that a non-problem will not submit""" """confirm that a non-problem will not submit"""
problem_url_name = self.problem_section.location.url() problem_url_name = self.problem_section.location.url()
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
self.regrade_all_student_answers('instructor', problem_url_name) self.regrade_all_student_answers('instructor', problem_url_name)
def testRegradingNonexistentProblem(self): def test_regrading_nonexistent_problem(self):
"""confirm that a non-existent problem will not submit""" """confirm that a non-existent problem will not submit"""
problem_url_name = 'NonexistentProblem' problem_url_name = 'NonexistentProblem'
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.regrade_all_student_answers('instructor', problem_url_name) self.regrade_all_student_answers('instructor', problem_url_name)
def testRegradingCodeProblem(self): def test_regrading_code_problem(self):
'''Run regrade scenario on problem with code submission''' '''Run regrade scenario on problem with code submission'''
problem_url_name = 'H1P2' problem_url_name = 'H1P2'
self.define_code_response_problem(problem_url_name) self.define_code_response_problem(problem_url_name)
...@@ -338,3 +339,84 @@ class TestRegrading(TestRegradingBase): ...@@ -338,3 +339,84 @@ class TestRegrading(TestRegradingBase):
response = course_task_log_status(mock_request, task_id=course_task_log.task_id) response = course_task_log_status(mock_request, task_id=course_task_log.task_id)
status = json.loads(response.content) status = json.loads(response.content)
self.assertEqual(status['message'], "Problem's definition does not support regrading") self.assertEqual(status['message'], "Problem's definition does not support regrading")
class TestResetAttempts(TestRegradingBase):
userlist = ['u1', 'u2', 'u3', 'u4']
def setUp(self):
self.initialize_course()
self.create_instructor('instructor')
for username in self.userlist:
self.create_student(username)
self.logout()
def get_num_attempts(self, username, descriptor):
module = self.get_student_module(username, descriptor)
state = json.loads(module.state)
return state['attempts']
def reset_problem_attempts(self, instructor, problem_url_name):
"""Submits the current problem for resetting"""
return submit_reset_problem_attempts_for_all_students(self._create_task_request(instructor), self.course.id,
TestRegradingBase.problem_location(problem_url_name))
def test_reset_attempts_on_problem(self):
'''Run reset-attempts scenario on option problem'''
# get descriptor:
problem_url_name = 'H1P1'
self.define_option_problem(problem_url_name)
location = TestRegradingBase.problem_location(problem_url_name)
descriptor = self.module_store.get_instance(self.course.id, location)
num_attempts = 3
# first store answers for each of the separate users:
for _ in range(num_attempts):
for username in self.userlist:
self.submit_student_answer(username, problem_url_name, ['Option 1', 'Option 1'])
for username in self.userlist:
self.assertEquals(self.get_num_attempts(username, descriptor), num_attempts)
self.reset_problem_attempts('instructor', problem_url_name)
for username in self.userlist:
self.assertEquals(self.get_num_attempts(username, descriptor), 0)
def test_reset_failure(self):
"""Simulate a failure in resetting attempts on a problem"""
problem_url_name = 'H1P1'
self.define_option_problem(problem_url_name)
self.submit_student_answer('u1', problem_url_name, ['Option 1', 'Option 1'])
expected_message = "bad things happened"
with patch('courseware.models.StudentModule.save') as mock_save:
mock_save.side_effect = ZeroDivisionError(expected_message)
course_task_log = self.reset_problem_attempts('instructor', problem_url_name)
# check task_log returned
self.assertEqual(course_task_log.task_state, 'FAILURE')
self.assertEqual(course_task_log.student, None)
self.assertEqual(course_task_log.requester.username, 'instructor')
self.assertEqual(course_task_log.task_name, 'reset_problem_attempts')
self.assertEqual(course_task_log.task_args, TestRegrading.problem_location(problem_url_name))
status = json.loads(course_task_log.task_progress)
self.assertEqual(status['exception'], 'ZeroDivisionError')
self.assertEqual(status['message'], expected_message)
# check status returned:
mock_request = Mock()
response = course_task_log_status(mock_request, task_id=course_task_log.task_id)
status = json.loads(response.content)
self.assertEqual(status['message'], expected_message)
def test_reset_non_problem(self):
"""confirm that a non-problem can still be successfully reset"""
problem_url_name = self.problem_section.location.url()
course_task_log = self.reset_problem_attempts('instructor', problem_url_name)
self.assertEqual(course_task_log.task_state, 'SUCCESS')
def test_reset_nonexistent_problem(self):
"""confirm that a non-existent problem will not submit"""
problem_url_name = 'NonexistentProblem'
with self.assertRaises(ItemNotFoundError):
self.reset_problem_attempts('instructor', problem_url_name)
...@@ -1308,25 +1308,34 @@ def get_background_task_table(course_id, problem_url, student=None): ...@@ -1308,25 +1308,34 @@ def get_background_task_table(course_id, problem_url, student=None):
"Task Id", "Task Id",
"Requester", "Requester",
"Submitted", "Submitted",
"Updated", "Duration",
"Task State", "Task State",
"Task Status", "Task Status",
"Message"] "Message"]
datatable['data'] = [] datatable['data'] = []
for i, course_task in enumerate(history_entries): for i, course_task in enumerate(history_entries):
# get duration info, if known:
duration_ms = 'unknown'
if hasattr(course_task, 'task_progress'):
task_progress = json.loads(course_task.task_progress)
if 'duration_ms' in task_progress:
duration_ms = task_progress['duration_ms']
# get progress status message:
success, message = task_queue.get_task_completion_message(course_task) success, message = task_queue.get_task_completion_message(course_task)
if success: if success:
status = "Complete" status = "Complete"
else: else:
status = "Incomplete" status = "Incomplete"
# generate row for this task:
row = ["#{0}".format(len(history_entries) - i), row = ["#{0}".format(len(history_entries) - i),
str(course_task.task_name), str(course_task.task_name),
str(course_task.student), str(course_task.student),
str(course_task.task_id), str(course_task.task_id),
str(course_task.requester), str(course_task.requester),
course_task.created.strftime("%Y/%m/%d %H:%M:%S"), course_task.created.strftime("%Y/%m/%d %H:%M:%S"),
course_task.updated.strftime("%Y/%m/%d %H:%M:%S"), duration_ms,
#course_task.updated.strftime("%Y/%m/%d %H:%M:%S"),
str(course_task.task_state), str(course_task.task_state),
status, status,
message] message]
......
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