def test_send_email_retried_subtask(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=no-member subtask_id = "subtask-id-value" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) subtask_status = SubtaskStatus.create(subtask_id, state=RETRY, retried_nomax=2) update_subtask_status(entry_id, subtask_id, subtask_status) bogus_email_id = 1001 to_list = ['*****@*****.**'] global_email_context = {'course_title': 'dummy course'} # try running with a clean subtask: new_subtask_status = SubtaskStatus.create(subtask_id) with self.assertRaisesRegexp(DuplicateTaskException, 'already retried'): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, new_subtask_status.to_dict()) # try again, with a retried subtask with lower count: new_subtask_status = SubtaskStatus.create(subtask_id, state=RETRY, retried_nomax=1) with self.assertRaisesRegexp(DuplicateTaskException, 'already retried'): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, new_subtask_status.to_dict())
def test_send_email_missing_subtask(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=no-member to_list = ['*****@*****.**'] global_email_context = {'course_title': 'dummy course'} subtask_id = "subtask-id-value" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) different_subtask_id = "bogus-subtask-id-value" subtask_status = SubtaskStatus.create(different_subtask_id) bogus_email_id = 1001 with self.assertRaisesRegexp(DuplicateTaskException, 'unable to find status for subtask of instructor task'): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status.to_dict())
def test_send_email_running_subtask(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=no-member subtask_id = "subtask-id-value" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) subtask_status = SubtaskStatus.create(subtask_id) update_subtask_status(entry_id, subtask_id, subtask_status) check_subtask_is_valid(entry_id, subtask_id, subtask_status) bogus_email_id = 1001 to_list = ['*****@*****.**'] global_email_context = {'course_title': 'dummy course'} with self.assertRaisesRegexp(DuplicateTaskException, 'already being executed'): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status.to_dict())
def test_send_email_completed_subtask(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=E1101 subtask_id = "subtask-id-value" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) subtask_status = SubtaskStatus.create(subtask_id, state=SUCCESS) update_subtask_status(entry_id, subtask_id, subtask_status) bogus_email_id = 1001 to_list = ["*****@*****.**"] global_email_context = {"course_title": "dummy course"} new_subtask_status = SubtaskStatus.create(subtask_id) with self.assertRaisesRegexp(DuplicateTaskException, "already completed"): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, new_subtask_status.to_dict())
def test_send_email_undefined_email(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=no-member to_list = ['*****@*****.**'] global_email_context = {'course_title': 'dummy course'} subtask_id = "subtask-id-undefined-email" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) subtask_status = SubtaskStatus.create(subtask_id) bogus_email_id = 1001 with self.assertRaises(CourseEmail.DoesNotExist): # we skip the call that updates subtask status, since we've not set up the InstructorTask # for the subtask, and it's not important to the test. with patch('bulk_email.tasks.update_subtask_status'): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status.to_dict())
def test_send_email_with_locked_instructor_task(self): # test at a lower level, to ensure that the course gets checked down below too. entry = InstructorTask.create(self.course.id, "task_type", "task_key", "task_input", self.instructor) entry_id = entry.id # pylint: disable=no-member subtask_id = "subtask-id-locked-model" initialize_subtask_info(entry, "emailed", 100, [subtask_id]) subtask_status = SubtaskStatus.create(subtask_id) bogus_email_id = 1001 to_list = ['*****@*****.**'] global_email_context = {'course_title': 'dummy course'} with patch('instructor_task.subtasks.InstructorTask.save') as mock_task_save: mock_task_save.side_effect = DatabaseError with self.assertRaises(DatabaseError): send_course_email(entry_id, bogus_email_id, to_list, global_email_context, subtask_status.to_dict()) self.assertEquals(mock_task_save.call_count, MAX_DATABASE_LOCK_RETRIES)
def perform_delegate_email_batches(entry_id, course_id, task_input, action_name): """ Delegates emails by querying for the list of recipients who should get the mail, chopping up into batches of settings.BULK_EMAIL_EMAILS_PER_TASK size, and queueing up worker jobs. Returns the number of batches (workers) kicked off. """ entry = InstructorTask.objects.get(pk=entry_id) # Get inputs to use in this task from the entry. user_id = entry.requester.id task_id = entry.task_id # Perfunctory check, since expansion is made for convenience of other task # code that doesn't need the entry_id. if course_id != entry.course_id: format_msg = "Course id conflict: explicit value {} does not match task value {}" raise ValueError(format_msg.format(course_id, entry.course_id)) # Fetch the CourseEmail. email_id = task_input['email_id'] try: email_obj = CourseEmail.objects.get(id=email_id) except CourseEmail.DoesNotExist: # The CourseEmail object should be committed in the view function before the task # is submitted and reaches this point. log.warning("Task %s: Failed to get CourseEmail with id %s", task_id, email_id) raise # Check to see if email batches have already been defined. This seems to # happen sometimes when there is a loss of connection while a task is being # queued. When this happens, the same task gets called again, and a whole # new raft of subtasks gets queued up. We will assume that if subtasks # have already been defined, there is no need to redefine them below. # So we just return right away. We don't raise an exception, because we want # the current task to be marked with whatever it had been marked with before. if len(entry.subtasks) > 0 and len(entry.task_output) > 0: log.warning("Task %s has already been processed for email %s! InstructorTask = %s", task_id, email_id, entry) progress = json.loads(entry.task_output) return progress # Sanity check that course for email_obj matches that of the task referencing it. if course_id != email_obj.course_id: format_msg = "Course id conflict: explicit value {} does not match email value {}" raise ValueError(format_msg.format(course_id, email_obj.course_id)) # Fetch the course object. try: course = get_course(course_id) except ValueError: log.exception("Task %s: course not found: %s", task_id, course_id) raise to_option = email_obj.to_option recipient_qset = _get_recipient_queryset(user_id, to_option, course_id, course.location) global_email_context = _get_course_email_context(course) def _create_send_email_subtask(to_list, subtask_id): """Creates a subtask to send email to a given recipient list.""" subtask_status = create_subtask_status(subtask_id) new_subtask = send_course_email.subtask( ( entry_id, email_id, to_list, global_email_context, subtask_status, ), task_id=subtask_id, routing_key=settings.BULK_EMAIL_ROUTING_KEY, ) return new_subtask log.info("Task %s: Preparing to generate subtasks for course %s, email %s, to_option %s", task_id, course_id, email_id, to_option) task_list, subtask_id_list, total_num_emails = _generate_subtasks(_create_send_email_subtask, recipient_qset) # Update the InstructorTask with information about the subtasks we've defined. log.info("Task %s: Preparing to update task for sending %d emails for course %s, email %s, to_option %s", task_id, total_num_emails, course_id, email_id, to_option) progress = initialize_subtask_info(entry, action_name, total_num_emails, subtask_id_list) num_subtasks = len(subtask_id_list) # Now group the subtasks, and start them running. This allows all the subtasks # in the list to be submitted at the same time. log.info("Task %s: Preparing to queue %d email tasks (%d emails) for course %s, email %s, to %s", task_id, num_subtasks, total_num_emails, course_id, email_id, to_option) task_group = group(task_list) task_group.apply_async(routing_key=settings.BULK_EMAIL_ROUTING_KEY) # We want to return progress here, as this is what will be stored in the # AsyncResult for the parent task as its return value. # The AsyncResult will then be marked as SUCCEEDED, and have this return value as its "result". # That's okay, for the InstructorTask will have the "real" status, and monitoring code # should be using that instead. return progress