Beispiel #1
0
 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_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=no-member
     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_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 my_update_subtask_status(entry_id, current_task_id, new_subtask_status):
    """
    Check whether a subtask has been updated before really updating.

    Check whether a subtask which has been retried
    has had the retry already write its results here before the code
    that was invoking the retry had a chance to update this status.

    This is the norm in "eager" mode (used by tests) where the retry is called
    and run to completion before control is returned to the code that
    invoked the retry.  If the retries eventually end in failure (e.g. due to
    a maximum number of retries being attempted), the "eager" code will return
    the error for each retry as it is popped off the stack.  We want to just ignore
    the later updates that are called as the result of the earlier retries.

    This should not be an issue in production, where status is updated before
    a task is retried, and is then updated afterwards if the retry fails.
    """
    entry = InstructorTask.objects.get(pk=entry_id)
    subtask_dict = json.loads(entry.subtasks)
    subtask_status_info = subtask_dict['status']
    current_subtask_status = SubtaskStatus.from_dict(subtask_status_info[current_task_id])
    current_retry_count = current_subtask_status.get_retry_count()
    new_retry_count = new_subtask_status.get_retry_count()
    if current_retry_count <= new_retry_count:
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
Beispiel #5
0
def my_update_subtask_status(entry_id, current_task_id, new_subtask_status):
    """
    Check whether a subtask has been updated before really updating.

    Check whether a subtask which has been retried
    has had the retry already write its results here before the code
    that was invoking the retry had a chance to update this status.

    This is the norm in "eager" mode (used by tests) where the retry is called
    and run to completion before control is returned to the code that
    invoked the retry.  If the retries eventually end in failure (e.g. due to
    a maximum number of retries being attempted), the "eager" code will return
    the error for each retry as it is popped off the stack.  We want to just ignore
    the later updates that are called as the result of the earlier retries.

    This should not be an issue in production, where status is updated before
    a task is retried, and is then updated afterwards if the retry fails.
    """
    entry = InstructorTask.objects.get(pk=entry_id)
    subtask_dict = json.loads(entry.subtasks)
    subtask_status_info = subtask_dict['status']
    current_subtask_status = SubtaskStatus.from_dict(
        subtask_status_info[current_task_id])
    current_retry_count = current_subtask_status.get_retry_count()
    new_retry_count = new_subtask_status.get_retry_count()
    if current_retry_count <= new_retry_count:
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
Beispiel #6
0
def check_all_users_email_subtask_is_valid(entry_id, current_task_id,
                                           new_subtask_status):
    # Confirm that the InstructorTask actually defines subtasks.
    entry = EmailAllUsersTask.objects.get(pk=entry_id)
    if len(entry.subtasks) == 0:
        format_str = "Unexpected task_id '{}': unable to find subtasks of email for all users task '{}': rejecting task {}"
        msg = format_str.format(current_task_id, entry, new_subtask_status)
        TASK_LOG.warning(msg)
        dog_stats_api.increment('instructor_task.subtask.duplicate.nosubtasks')
        raise DuplicateTaskException(msg)

    # Confirm that the InstructorTask knows about this particular subtask.
    subtask_dict = json.loads(entry.subtasks)
    subtask_status_info = subtask_dict['status']
    if current_task_id not in subtask_status_info:
        format_str = "Unexpected task_id '{}': unable to find status for subtask of email for all users task '{}': rejecting task {}"
        msg = format_str.format(current_task_id, entry, new_subtask_status)
        TASK_LOG.warning(msg)
        dog_stats_api.increment('instructor_task.subtask.duplicate.unknown')
        raise DuplicateTaskException(msg)

    # Confirm that the InstructorTask doesn't think that this subtask has already been
    # performed successfully.
    subtask_status = SubtaskStatus.from_dict(
        subtask_status_info[current_task_id])
    subtask_state = subtask_status.state
    if subtask_state in READY_STATES:
        format_str = "Unexpected task_id '{}': already completed - status {} for subtask of email for all users task '{}': rejecting task {}"
        msg = format_str.format(current_task_id, subtask_status, entry,
                                new_subtask_status)
        TASK_LOG.warning(msg)
        dog_stats_api.increment('instructor_task.subtask.duplicate.completed')
        raise DuplicateTaskException(msg)

    # Confirm that the InstructorTask doesn't think that this subtask is already being
    # retried by another task.
    if subtask_state == RETRY:
        # Check to see if the input number of retries is less than the recorded number.
        # If so, then this is an earlier version of the task, and a duplicate.
        new_retry_count = new_subtask_status.get_retry_count()
        current_retry_count = subtask_status.get_retry_count()
        if new_retry_count < current_retry_count:
            format_str = "Unexpected task_id '{}': already retried - status {} for subtask of email for all users task '{}': rejecting task {}"
            msg = format_str.format(current_task_id, subtask_status, entry,
                                    new_subtask_status)
            TASK_LOG.warning(msg)
            dog_stats_api.increment(
                'instructor_task.subtask.duplicate.retried')
            raise DuplicateTaskException(msg)

    # Now we are ready to start working on this.  Try to lock it.
    # If it fails, then it means that another worker is already in the
    # middle of working on this.
    if not _acquire_subtask_lock(current_task_id):
        format_str = "Unexpected task_id '{}': already being executed - for subtask of email for all users task '{}'"
        msg = format_str.format(current_task_id, entry)
        TASK_LOG.warning(msg)
        dog_stats_api.increment('instructor_task.subtask.duplicate.locked')
        raise DuplicateTaskException(msg)
 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=E1101
     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_undefined_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"
     subtask_status = SubtaskStatus.create(subtask_id)
     email_id = 1001
     with self.assertRaisesRegexp(DuplicateTaskException, 'unable to find subtasks of instructor task'):
         send_course_email(entry_id, email_id, to_list, global_email_context, 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())
Beispiel #10
0
def send_all_users_email(entry_id, email_id, to_list, global_email_context,
                         subtask_status_dict):
    subtask_status = SubtaskStatus.from_dict(subtask_status_dict)
    current_task_id = subtask_status.task_id
    num_to_send = len(to_list)
    log.info((u"Preparing to send email %s to %d recipients as subtask %s "
              u"for email for all users task %d: context = %s, status=%s"),
             email_id, num_to_send, current_task_id, entry_id,
             global_email_context, subtask_status)

    check_all_users_email_subtask_is_valid(entry_id, current_task_id,
                                           subtask_status)

    send_exception = None
    new_subtask_status = None
    try:
        # course_title = global_email_context['course_title']
        # ENTENDER LOS PARAMETROS DEL TIMER
        with dog_stats_api.timer('course_email.single_task.time.overall'):
            new_subtask_status, send_exception = _send_all_users_email(
                entry_id,
                email_id,
                to_list,
                global_email_context,
                subtask_status,
            )
    except Exception:
        log.exception("Send-email task %s for email %s: failed unexpectedly!",
                      current_task_id, email_id)
        subtask_status.increment(failed=num_to_send, state=FAILURE)
        update_subtask_status(entry_id, current_task_id, subtask_status)
        raise

    if send_exception is None:
        log.info("Send-email task %s for email %s: succeeded", current_task_id,
                 email_id)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
    elif isinstance(send_exception, RetryTaskError):
        log.warning("Send-email task %s for email %s: being retried",
                    current_task_id, email_id)
        raise send_exception  # pylint: disable=raising-bad-type
    else:
        log.error("Send-email task %s for email %s: failed: %s",
                  current_task_id, email_id, send_exception)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
        raise send_exception  # pylint: disable=raising-bad-type

    log.info("Send-email task %s for email %s: returning status %s",
             current_task_id, email_id, new_subtask_status)
    return new_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=E1101
     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_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)
Beispiel #14
0
def queue_subtasks_for_query_all_users_email(
    entry,
    action_name,
    create_subtask_fcn,
    item_querysets,
    item_fields,
    items_per_task,
    total_num_items,
):
    """
    Generates and queues subtasks to each execute a chunk of "items" generated by a queryset.

    Arguments:
        `entry` : the InstructorTask object for which subtasks are being queued.
        `action_name` : a past-tense verb that can be used for constructing readable status messages.
        `create_subtask_fcn` : a function of two arguments that constructs the desired kind of subtask object.
            Arguments are the list of items to be processed by this subtask, and a SubtaskStatus
            object reflecting initial status (and containing the subtask's id).
        `item_querysets` : a list of query sets that define the "items" that should be passed to subtasks.
        `item_fields` : the fields that should be included in the dict that is returned.
            These are in addition to the 'pk' field.
        `items_per_task` : maximum size of chunks to break each query chunk into for use by a subtask.
        `total_num_items` : total amount of items that will be put into subtasks

    Returns:  the task progress as stored in the InstructorTask object.

    """
    task_id = entry.task_id

    # Calculate the number of tasks that will be created, and create a list of ids for each task.
    total_num_subtasks = _get_number_of_subtasks(total_num_items,
                                                 items_per_task)
    subtask_id_list = [str(uuid4()) for _ in range(total_num_subtasks)]

    # Update the InstructorTask  with information about the subtasks we've defined.
    TASK_LOG.info(
        "Task %s: updating EmailAllUsersTask %s with subtask info for %s subtasks to process %s items.",
        task_id,
        entry.id,
        total_num_subtasks,
        total_num_items,
    )
    # Make sure this is committed to database before handing off subtasks to celery.
    #with outer_atomic():
    progress = initialize_all_users_email_subtask_info(entry, action_name,
                                                       total_num_items,
                                                       subtask_id_list)

    # Construct a generator that will return the recipients to use for each subtask.
    # Pass in the desired fields to fetch for each recipient.
    item_list_generator = _generate_items_for_all_users_email_subtask(
        item_querysets,
        item_fields,
        total_num_items,
        items_per_task,
        total_num_subtasks,
    )

    # Now create the subtasks, and start them running.
    TASK_LOG.info(
        "Task %s: creating %s subtasks to process %s items.",
        task_id,
        total_num_subtasks,
        total_num_items,
    )
    num_subtasks = 0
    for item_list in item_list_generator:
        subtask_id = subtask_id_list[num_subtasks]
        num_subtasks += 1
        subtask_status = SubtaskStatus.create(subtask_id)
        new_subtask = create_subtask_fcn(item_list, subtask_status)
        new_subtask.apply_async()

    # Subtasks have been queued so no exceptions should be raised after this point.

    # Return the task progress as stored in the InstructorTask object.
    return progress
Beispiel #15
0
def send_course_email(entry_id, email_id, to_list, global_email_context,
                      subtask_status_dict):
    """
    Sends an email to a list of recipients.

    Inputs are:
      * `entry_id`: id of the InstructorTask object to which progress should be recorded.
      * `email_id`: id of the CourseEmail model that is to be emailed.
      * `to_list`: list of recipients.  Each is represented as a dict with the following keys:
        - 'profile__name': full name of User.
        - 'email': email address of User.
        - 'pk': primary key of User model.
      * `global_email_context`: dict containing values that are unique for this email but the same
        for all recipients of this email.  This dict is to be used to fill in slots in email
        template.  It does not include 'name' and 'email', which will be provided by the to_list.
      * `subtask_status_dict` : dict containing values representing current status.  Keys are:

        'task_id' : id of subtask.  This is used to pass task information across retries.
        'attempted' : number of attempts -- should equal succeeded plus failed
        'succeeded' : number that succeeded in processing
        'skipped' : number that were not processed.
        'failed' : number that failed during processing
        'retried_nomax' : number of times the subtask has been retried for conditions that
            should not have a maximum count applied
        'retried_withmax' : number of times the subtask has been retried for conditions that
            should have a maximum count applied
        'state' : celery state of the subtask (e.g. QUEUING, PROGRESS, RETRY, FAILURE, SUCCESS)

        Most values will be zero on initial call, but may be different when the task is
        invoked as part of a retry.

    Sends to all addresses contained in to_list that are not also in the Optout table.
    Emails are sent multi-part, in both plain text and html.  Updates InstructorTask object
    with status information (sends, failures, skips) and updates number of subtasks completed.
    """
    subtask_status = SubtaskStatus.from_dict(subtask_status_dict)
    current_task_id = subtask_status.task_id
    num_to_send = len(to_list)
    log.info((u"Preparing to send email %s to %d recipients as subtask %s "
              u"for instructor task %d: context = %s, status=%s"), email_id,
             num_to_send, current_task_id, entry_id, global_email_context,
             subtask_status)

    # Check that the requested subtask is actually known to the current InstructorTask entry.
    # If this fails, it throws an exception, which should fail this subtask immediately.
    # This can happen when the parent task has been run twice, and results in duplicate
    # subtasks being created for the same InstructorTask entry.  This can happen when Celery
    # loses its connection to its broker, and any current tasks get requeued.
    # We hope to catch this condition in perform_delegate_email_batches() when it's the parent
    # task that is resubmitted, but just in case we fail to do so there, we check here as well.
    # There is also a possibility that this task will be run twice by Celery, for the same reason.
    # To deal with that, we need to confirm that the task has not already been completed.
    check_subtask_is_valid(entry_id, current_task_id, subtask_status)

    send_exception = None
    new_subtask_status = None
    try:
        course_title = global_email_context['course_title']
        with dog_stats_api.timer('course_email.single_task.time.overall',
                                 tags=[_statsd_tag(course_title)]):
            new_subtask_status, send_exception = _send_course_email(
                entry_id,
                email_id,
                to_list,
                global_email_context,
                subtask_status,
            )
    except Exception:
        # Unexpected exception. Try to write out the failure to the entry before failing.
        log.exception("Send-email task %s for email %s: failed unexpectedly!",
                      current_task_id, email_id)
        # We got here for really unexpected reasons.  Since we don't know how far
        # the task got in emailing, we count all recipients as having failed.
        # It at least keeps the counts consistent.
        subtask_status.increment(failed=num_to_send, state=FAILURE)
        update_subtask_status(entry_id, current_task_id, subtask_status)
        raise

    if send_exception is None:
        # Update the InstructorTask object that is storing its progress.
        log.info("Send-email task %s for email %s: succeeded", current_task_id,
                 email_id)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
    elif isinstance(send_exception, RetryTaskError):
        # If retrying, a RetryTaskError needs to be returned to Celery.
        # We assume that the the progress made before the retry condition
        # was encountered has already been updated before the retry call was made,
        # so we only log here.
        log.warning("Send-email task %s for email %s: being retried",
                    current_task_id, email_id)
        raise send_exception  # pylint: disable=raising-bad-type
    else:
        log.error("Send-email task %s for email %s: failed: %s",
                  current_task_id, email_id, send_exception)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
        raise send_exception  # pylint: disable=raising-bad-type

    # return status in a form that can be serialized by Celery into JSON:
    log.info("Send-email task %s for email %s: returning status %s",
             current_task_id, email_id, new_subtask_status)
    return new_subtask_status.to_dict()
Beispiel #16
0
def send_course_email(entry_id, email_id, to_list, global_email_context, subtask_status_dict):
    """
    Sends an email to a list of recipients.

    Inputs are:
      * `entry_id`: id of the InstructorTask object to which progress should be recorded.
      * `email_id`: id of the CourseEmail model that is to be emailed.
      * `to_list`: list of recipients.  Each is represented as a dict with the following keys:
        - 'profile__name': full name of User.
        - 'email': email address of User.
        - 'pk': primary key of User model.
      * `global_email_context`: dict containing values that are unique for this email but the same
        for all recipients of this email.  This dict is to be used to fill in slots in email
        template.  It does not include 'name' and 'email', which will be provided by the to_list.
      * `subtask_status_dict` : dict containing values representing current status.  Keys are:

        'task_id' : id of subtask.  This is used to pass task information across retries.
        'attempted' : number of attempts -- should equal succeeded plus failed
        'succeeded' : number that succeeded in processing
        'skipped' : number that were not processed.
        'failed' : number that failed during processing
        'retried_nomax' : number of times the subtask has been retried for conditions that
            should not have a maximum count applied
        'retried_withmax' : number of times the subtask has been retried for conditions that
            should have a maximum count applied
        'state' : celery state of the subtask (e.g. QUEUING, PROGRESS, RETRY, FAILURE, SUCCESS)

        Most values will be zero on initial call, but may be different when the task is
        invoked as part of a retry.

    Sends to all addresses contained in to_list that are not also in the Optout table.
    Emails are sent multi-part, in both plain text and html.  Updates InstructorTask object
    with status information (sends, failures, skips) and updates number of subtasks completed.
    """
    subtask_status = SubtaskStatus.from_dict(subtask_status_dict)
    current_task_id = subtask_status.task_id
    num_to_send = len(to_list)
    log.info((u"Preparing to send email %s to %d recipients as subtask %s "
              u"for instructor task %d: context = %s, status=%s"),
             email_id, num_to_send, current_task_id, entry_id, global_email_context, subtask_status)

    # Check that the requested subtask is actually known to the current InstructorTask entry.
    # If this fails, it throws an exception, which should fail this subtask immediately.
    # This can happen when the parent task has been run twice, and results in duplicate
    # subtasks being created for the same InstructorTask entry.  This can happen when Celery
    # loses its connection to its broker, and any current tasks get requeued.
    # We hope to catch this condition in perform_delegate_email_batches() when it's the parent
    # task that is resubmitted, but just in case we fail to do so there, we check here as well.
    # There is also a possibility that this task will be run twice by Celery, for the same reason.
    # To deal with that, we need to confirm that the task has not already been completed.
    check_subtask_is_valid(entry_id, current_task_id, subtask_status)

    send_exception = None
    new_subtask_status = None
    try:
        course_title = global_email_context['course_title']
        with dog_stats_api.timer('course_email.single_task.time.overall', tags=[_statsd_tag(course_title)]):
            new_subtask_status, send_exception = _send_course_email(
                entry_id,
                email_id,
                to_list,
                global_email_context,
                subtask_status,
            )
    except Exception:
        # Unexpected exception. Try to write out the failure to the entry before failing.
        log.exception("Send-email task %s for email %s: failed unexpectedly!", current_task_id, email_id)
        # We got here for really unexpected reasons.  Since we don't know how far
        # the task got in emailing, we count all recipients as having failed.
        # It at least keeps the counts consistent.
        subtask_status.increment(failed=num_to_send, state=FAILURE)
        update_subtask_status(entry_id, current_task_id, subtask_status)
        raise

    if send_exception is None:
        # Update the InstructorTask object that is storing its progress.
        log.info("Send-email task %s for email %s: succeeded", current_task_id, email_id)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
    elif isinstance(send_exception, RetryTaskError):
        # If retrying, a RetryTaskError needs to be returned to Celery.
        # We assume that the the progress made before the retry condition
        # was encountered has already been updated before the retry call was made,
        # so we only log here.
        log.warning("Send-email task %s for email %s: being retried", current_task_id, email_id)
        raise send_exception  # pylint: disable=raising-bad-type
    else:
        log.error("Send-email task %s for email %s: failed: %s", current_task_id, email_id, send_exception)
        update_subtask_status(entry_id, current_task_id, new_subtask_status)
        raise send_exception  # pylint: disable=raising-bad-type

    # return status in a form that can be serialized by Celery into JSON:
    log.info("Send-email task %s for email %s: returning status %s", current_task_id, email_id, new_subtask_status)
    return new_subtask_status.to_dict()
Beispiel #17
0
def initialize_all_users_email_subtask_info(entry, action_name, total_num,
                                            subtask_id_list):
    """
    Store initial subtask information to InstructorTask object.

    The InstructorTask's "task_output" field is initialized.  This is a JSON-serialized dict.
    Counters for 'attempted', 'succeeded', 'failed', 'skipped' keys are initialized to zero,
    as is the 'duration_ms' value.  A 'start_time' is stored for later duration calculations,
    and the total number of "things to do" is set, so the user can be told how much needs to be
    done overall.  The `action_name` is also stored, to help with constructing more readable
    task_progress messages.

    The InstructorTask's "subtasks" field is also initialized.  This is also a JSON-serialized dict.
    Keys include 'total', 'succeeded', 'retried', 'failed', which are counters for the number of
    subtasks.  'Total' is set here to the total number, while the other three are initialized to zero.
    Once the counters for 'succeeded' and 'failed' match the 'total', the subtasks are done and
    the InstructorTask's "status" will be changed to SUCCESS.

    The "subtasks" field also contains a 'status' key, that contains a dict that stores status
    information for each subtask.  The value for each subtask (keyed by its task_id)
    is its subtask status, as defined by SubtaskStatus.to_dict().

    This information needs to be set up in the InstructorTask before any of the subtasks start
    running.  If not, there is a chance that the subtasks could complete before the parent task
    is done creating subtasks.  Doing so also simplifies the save() here, as it avoids the need
    for locking.

    Monitoring code should assume that if an InstructorTask has subtask information, that it should
    rely on the status stored in the InstructorTask object, rather than status stored in the
    corresponding AsyncResult.
    """
    task_progress = {
        'action_name': action_name,
        'attempted': 0,
        'failed': 0,
        'skipped': 0,
        'succeeded': 0,
        'total': total_num,
        'duration_ms': int(0),
        'start_time': time()
    }
    entry.task_output = EmailAllUsersTask.create_output_for_success(
        task_progress)
    entry.task_state = "PROGRESS"

    # Write out the subtasks information.
    num_subtasks = len(subtask_id_list)
    # Note that may not be necessary to store initial value with all those zeroes!
    # Write out as a dict, so it will go more smoothly into json.
    subtask_status = {
        subtask_id: (SubtaskStatus.create(subtask_id)).to_dict()
        for subtask_id in subtask_id_list
    }
    subtask_dict = {
        'total': num_subtasks,
        'succeeded': 0,
        'failed': 0,
        'status': subtask_status
    }
    entry.subtasks = json.dumps(subtask_dict)

    # and save the entry immediately, before any subtasks actually start work:
    entry.save_now()
    return task_progress