Ran pyupgrade on lms/djangoapps/instructor_analytics Ran pyugprade on lms/djangoapps/instructor_task Ran pyupgrade on lms/djangoapps/learner_dashboard
141 lines
5.0 KiB
Python
141 lines
5.0 KiB
Python
"""
|
|
Instrutor Task runner
|
|
"""
|
|
|
|
|
|
import json
|
|
import logging
|
|
from time import time
|
|
|
|
from celery import current_task
|
|
from django.db import reset_queries
|
|
|
|
from common.djangoapps.util.db import outer_atomic
|
|
from lms.djangoapps.instructor_task.models import PROGRESS, InstructorTask
|
|
|
|
TASK_LOG = logging.getLogger('edx.celery.task')
|
|
|
|
|
|
class TaskProgress:
|
|
"""
|
|
Encapsulates the current task's progress by keeping track of
|
|
'attempted', 'succeeded', 'skipped', 'failed', 'total',
|
|
'action_name', and 'duration_ms' values.
|
|
"""
|
|
def __init__(self, action_name, total, start_time):
|
|
self.action_name = action_name
|
|
self.total = total
|
|
self.start_time = start_time
|
|
self.attempted = 0
|
|
self.succeeded = 0
|
|
self.skipped = 0
|
|
self.failed = 0
|
|
self.preassigned = 0
|
|
|
|
@property
|
|
def state(self):
|
|
return {
|
|
'action_name': self.action_name,
|
|
'attempted': self.attempted,
|
|
'succeeded': self.succeeded,
|
|
'skipped': self.skipped,
|
|
'failed': self.failed,
|
|
'total': self.total,
|
|
'preassigned': self.preassigned,
|
|
'duration_ms': int((time() - self.start_time) * 1000),
|
|
}
|
|
|
|
def update_task_state(self, extra_meta=None):
|
|
"""
|
|
Update the current celery task's state to the progress state
|
|
specified by the current object. Returns the progress
|
|
dictionary for use by `run_main_task` and
|
|
`BaseInstructorTask.on_success`.
|
|
|
|
Arguments:
|
|
extra_meta (dict): Extra metadata to pass to `update_state`
|
|
|
|
Returns:
|
|
dict: The current task's progress dict
|
|
"""
|
|
progress_dict = self.state
|
|
if extra_meta is not None:
|
|
progress_dict.update(extra_meta)
|
|
_get_current_task().update_state(state=PROGRESS, meta=progress_dict)
|
|
return progress_dict
|
|
|
|
|
|
def run_main_task(entry_id, task_fcn, action_name):
|
|
"""
|
|
Applies the `task_fcn` to the arguments defined in `entry_id` InstructorTask.
|
|
|
|
Arguments passed to `task_fcn` are:
|
|
|
|
`entry_id` : the primary key for the InstructorTask entry representing the task.
|
|
`course_id` : the id for the course.
|
|
`task_input` : dict containing task-specific arguments, JSON-decoded from InstructorTask's task_input.
|
|
`action_name` : past-tense verb to use for constructing status messages.
|
|
|
|
If no exceptions are raised, the `task_fcn` should return a dict containing
|
|
the task's result with the following keys:
|
|
|
|
'attempted': number of attempts made
|
|
'succeeded': number of attempts that "succeeded"
|
|
'skipped': number of attempts that "skipped"
|
|
'failed': number of attempts that "failed"
|
|
'total': number of possible subtasks to attempt
|
|
'action_name': user-visible verb to use in status messages.
|
|
Should be past-tense. Pass-through of input `action_name`.
|
|
'duration_ms': how long the task has (or had) been running.
|
|
|
|
"""
|
|
|
|
# Get the InstructorTask to be updated. If this fails then let the exception return to Celery.
|
|
# There's no point in catching it here.
|
|
with outer_atomic():
|
|
entry = InstructorTask.objects.get(pk=entry_id)
|
|
entry.task_state = PROGRESS
|
|
entry.save_now()
|
|
|
|
# Get inputs to use in this task from the entry
|
|
task_id = entry.task_id
|
|
course_id = entry.course_id
|
|
task_input = json.loads(entry.task_input)
|
|
|
|
# Construct log message
|
|
fmt = 'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}'
|
|
task_info_string = fmt.format(task_id=task_id, entry_id=entry_id, course_id=course_id, task_input=task_input)
|
|
TASK_LOG.info('%s, Starting update (nothing %s yet)', task_info_string, action_name)
|
|
|
|
# Check that the task_id submitted in the InstructorTask matches the current task
|
|
# that is running.
|
|
request_task_id = _get_current_task().request.id
|
|
if task_id != request_task_id:
|
|
fmt = '{task_info}, Requested task did not match actual task "{actual_id}"'
|
|
message = fmt.format(task_info=task_info_string, actual_id=request_task_id)
|
|
TASK_LOG.error(message)
|
|
raise ValueError(message)
|
|
|
|
# Now do the work
|
|
task_progress = task_fcn(entry_id, course_id, task_input, action_name)
|
|
|
|
# Release any queries that the connection has been hanging onto
|
|
reset_queries()
|
|
|
|
# Log and exit, returning task_progress info as task result
|
|
TASK_LOG.info('%s, Task type: %s, Finishing task: %s', task_info_string, action_name, task_progress)
|
|
return task_progress
|
|
|
|
|
|
def _get_current_task():
|
|
"""
|
|
Stub to make it easier to test without actually running Celery.
|
|
|
|
This is a wrapper around celery.current_task, which provides access
|
|
to the top of the stack of Celery's tasks. When running tests, however,
|
|
it doesn't seem to work to mock current_task directly, so this wrapper
|
|
is used to provide a hook to mock in tests, while providing the real
|
|
`current_task` in production.
|
|
"""
|
|
return current_task
|