Skip to content
Snippets Groups Projects
Commit 86e79c59 authored by Michael Terry's avatar Michael Terry Committed by Michael Terry
Browse files

Paginate notify_credentials queries

Avoid blowing up on the giant queries that notify_credentials does by
paginating the queries before we resolve them.
parent 0d5c660d
No related branches found
No related tags found
No related merge requests found
...@@ -9,8 +9,9 @@ This management command will manually trigger the receivers we care about. ...@@ -9,8 +9,9 @@ This management command will manually trigger the receivers we care about.
(We don't want to trigger all receivers for these signals, since these are busy (We don't want to trigger all receivers for these signals, since these are busy
signals.) signals.)
""" """
from __future__ import print_function from __future__ import print_function, division
import logging import logging
import math
import time import time
import sys import sys
...@@ -45,6 +46,25 @@ def parsetime(timestr): ...@@ -45,6 +46,25 @@ def parsetime(timestr):
return dt return dt
def paged_query(queryset, delay, page_size):
"""
A generator that iterates through a queryset but only resolves chunks of it at once, to avoid overwhelming memory
with a giant query. Also adds an optional delay between yields, to help with load.
"""
count = queryset.count()
pages = int(math.ceil(count / page_size))
for page in range(pages):
page_start = page * page_size
page_end = page_start + page_size
subquery = queryset[page_start:page_end]
for i, item in enumerate(subquery, start=1):
if delay:
time.sleep(delay)
yield page_start + i, item
class Command(BaseCommand): class Command(BaseCommand):
""" """
Example usage: Example usage:
...@@ -101,6 +121,12 @@ class Command(BaseCommand): ...@@ -101,6 +121,12 @@ class Command(BaseCommand):
default=0, default=0,
help="Number of seconds to sleep between processing certificates, so that we don't flood our queues.", help="Number of seconds to sleep between processing certificates, so that we don't flood our queues.",
) )
parser.add_argument(
'--page-size',
type=int,
default=100,
help="Number of items to query at once.",
)
def handle(self, *args, **options): def handle(self, *args, **options):
log.info( log.info(
...@@ -133,23 +159,21 @@ class Command(BaseCommand): ...@@ -133,23 +159,21 @@ class Command(BaseCommand):
grades = PersistentCourseGrade.objects.filter(**grade_filter_args).order_by('modified') grades = PersistentCourseGrade.objects.filter(**grade_filter_args).order_by('modified')
if options['dry_run']: if options['dry_run']:
self.print_dry_run(list(certs), list(grades)) self.print_dry_run(certs, grades)
else: else:
self.send_notifications(certs, grades, delay=options['delay']) self.send_notifications(certs, grades, delay=options['delay'], page_size=options['page_size'])
log.info('notify_credentials finished') log.info('notify_credentials finished')
def send_notifications(self, certs, grades, delay=0): def send_notifications(self, certs, grades, delay=0, page_size=0):
""" Run actual handler commands for the provided certs and grades. """ """ Run actual handler commands for the provided certs and grades. """
# First, do certs # First, do certs
for i, cert in enumerate(certs, start=1): for i, cert in paged_query(certs, delay, page_size):
log.info( log.info(
"Handling credential changes %d for certificate %s", "Handling credential changes %d for certificate %s",
i, certstr(cert), i, certstr(cert),
) )
if delay:
time.sleep(delay)
signal_args = { signal_args = {
'sender': None, 'sender': None,
...@@ -163,13 +187,11 @@ class Command(BaseCommand): ...@@ -163,13 +187,11 @@ class Command(BaseCommand):
handle_cert_change(**signal_args) handle_cert_change(**signal_args)
# Then do grades # Then do grades
for i, grade in enumerate(grades, start=1): for i, grade in paged_query(grades, delay, page_size):
log.info( log.info(
"Handling grade changes %d for grade %s", "Handling grade changes %d for grade %s",
i, gradestr(grade), i, gradestr(grade),
) )
if delay:
time.sleep(delay)
user = User.objects.get(id=grade.user_id) user = User.objects.get(id=grade.user_id)
send_grade_if_interesting(user, grade.course_id, None, None, grade.letter_grade, grade.percent_grade) send_grade_if_interesting(user, grade.course_id, None, None, grade.letter_grade, grade.percent_grade)
...@@ -201,14 +223,14 @@ class Command(BaseCommand): ...@@ -201,14 +223,14 @@ class Command(BaseCommand):
print("DRY-RUN: This command would have handled changes for...") print("DRY-RUN: This command would have handled changes for...")
ITEMS_TO_SHOW = 10 ITEMS_TO_SHOW = 10
print(len(certs), "Certificates:") print(certs.count(), "Certificates:")
for cert in certs[:ITEMS_TO_SHOW]: for cert in certs[:ITEMS_TO_SHOW]:
print(" ", certstr(cert)) print(" ", certstr(cert))
if len(certs) > ITEMS_TO_SHOW: if certs.count() > ITEMS_TO_SHOW:
print(" (+ {} more)".format(len(certs) - ITEMS_TO_SHOW)) print(" (+ {} more)".format(certs.count() - ITEMS_TO_SHOW))
print(len(grades), "Grades:") print(grades.count(), "Grades:")
for grade in grades[:ITEMS_TO_SHOW]: for grade in grades[:ITEMS_TO_SHOW]:
print(" ", gradestr(grade)) print(" ", gradestr(grade))
if len(grades) > ITEMS_TO_SHOW: if grades.count() > ITEMS_TO_SHOW:
print(" (+ {} more)".format(len(grades) - ITEMS_TO_SHOW)) print(" (+ {} more)".format(grades.count() - ITEMS_TO_SHOW))
...@@ -8,7 +8,8 @@ import mock ...@@ -8,7 +8,8 @@ import mock
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import CommandError from django.core.management.base import CommandError
from django.test import TestCase from django.db import connection, reset_queries
from django.test import TestCase, override_settings
from freezegun import freeze_time from freezegun import freeze_time
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
...@@ -107,3 +108,16 @@ class TestNotifyCredentials(TestCase): ...@@ -107,3 +108,16 @@ class TestNotifyCredentials(TestCase):
call_command(Command(), '--start-date', '2017-02-01', '--delay', '0.2') call_command(Command(), '--start-date', '2017-02-01', '--delay', '0.2')
self.assertEqual(mock_time.sleep.call_count, 4) # After each cert and each grade (2 each) self.assertEqual(mock_time.sleep.call_count, 4) # After each cert and each grade (2 each)
self.assertEqual(mock_time.sleep.call_args[0][0], 0.2) self.assertEqual(mock_time.sleep.call_args[0][0], 0.2)
@override_settings(DEBUG=True)
def test_page_size(self):
call_command(Command(), '--start-date', '2017-01-01')
baseline = len(connection.queries)
reset_queries()
call_command(Command(), '--start-date', '2017-01-01', '--page-size=1')
self.assertEqual(len(connection.queries), baseline + 4) # two extra page queries each for certs & grades
reset_queries()
call_command(Command(), '--start-date', '2017-01-01', '--page-size=2')
self.assertEqual(len(connection.queries), baseline + 2) # one extra page query each for certs & grades
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment