chore: added a django management command to expire old entitlements and create new one against them

This commit is contained in:
Muhammad Zubair
2023-06-20 18:48:51 +05:00
committed by Phillip Shiu
parent b3fd3c9562
commit 4d4f1cd1b5
2 changed files with 138 additions and 2 deletions

View File

@@ -0,0 +1,77 @@
# lint-amnesty, pylint: disable=django-not-configured
"""
Management command for expiring old entitlements.
"""
import logging
from textwrap import dedent
from django.core.management import BaseCommand
from common.djangoapps.entitlements.models import CourseEntitlement
from common.djangoapps.entitlements.tasks import expire_and_create_entitlements
logger = logging.getLogger(__name__) # pylint: disable=invalid-name
class Command(BaseCommand):
"""
Management command for expiring old entitlements and issuing new one against them.
Most entitlements get expired as the user interacts with the platform,
because the LMS checks as it goes. This command is to expire entitlements older than one year and issue new one
against them. But if the learner has not logged in
for a while, we still want to reap these old entitlements. So this command
should be run every now and then (probably daily) to expire old
entitlements.
The command's goal is to pass a narrow subset of entitlements to an
idempotent Celery task for further (parallelized) processing.
"""
help = dedent(__doc__).strip()
def add_arguments(self, parser):
parser.add_argument(
'-c', '--commit',
action='store_true',
default=False,
help='Submit tasks for processing'
)
parser.add_argument(
'--count',
type=int,
default=100, # arbitrary, should be adjusted if it is found to be inadequate
help='How many entitlements to expire'
)
parser.add_argument(
'--batch-size',
type=int,
default=10, # arbitrary, should be adjusted if it is found to be inadequate
help='How many entitlements to give each celery task'
)
def handle(self, *args, **options):
logger.info('Looking for entitlements which may be expirable.')
total = max(1, options.get('count'))
batch_size = max(1, options.get('batch_size'))
num_batches = ((total - 1) / batch_size + 1) if total > 0 else 0
if options.get('commit'):
logger.info('Enqueuing %d entitlement expiration tasks.', num_batches)
else:
logger.info(
'Found %d batches. To enqueue entitlement expiration tasks, pass the -c or --commit flags.',
num_batches
)
return
for batch_num in range(int(num_batches)):
start = batch_num * batch_size + 1 # ids are 1-based, so add 1
end = min(start + batch_size, total + 1)
expire_and_create_entitlements.delay(start, end, logid=str(batch_num))
logger.info('Done. Successfully enqueued %d tasks.', num_batches)

View File

@@ -1,7 +1,8 @@
"""
This file contains celery tasks for entitlements-related functionality.
"""
from datetime import date
from dateutil.relativedelta import relativedelta
from celery import shared_task
from celery.utils.log import get_task_logger
@@ -9,6 +10,7 @@ from django.conf import settings # lint-amnesty, pylint: disable=unused-import
from edx_django_utils.monitoring import set_code_owner_attribute
from common.djangoapps.entitlements.models import CourseEntitlement
from common.djangoapps.entitlements.rest_api.v1.views import EntitlementViewSet
LOGGER = get_task_logger(__name__)
@@ -17,7 +19,15 @@ LOGGER = get_task_logger(__name__)
# time of 2047 seconds (about 30 minutes). Setting this to None could yield
# unwanted behavior: infinite retries.
MAX_RETRIES = 11
#course uuids for which entitlements should be expired after 18 months.
MIT_SUPPLY_CHAIN_COURSES = [
'0d9b47982e3d486aa3189a7035bbda77',
'09532745c837467b9078093b8e1265a8',
'324970b703a444d7b39e10bbda6f119f',
'5f1c55b4354e4155af4a76450953e10d',
'ed927a1a4a95415ba865c3d722ac549c',
'6513ed9c112a495182ad7036cbe52831',
]
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
@@ -62,3 +72,52 @@ def expire_old_entitlements(self, start, end, logid='...'):
raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
LOGGER.info('Successfully completed the task expire_old_entitlements after examining %d entries [%s]', entitlements.count(), logid) # lint-amnesty, pylint: disable=line-too-long
@shared_task(bind=True, ignore_result=True)
@set_code_owner_attribute
def expire_and_create_entitlements(self):
"""
This task is designed to be called to process and expire bundle of entitlements
that are older than one year on in exceptional case 18 months.
Args:
None
Returns:
None
"""
LOGGER.info('Running task expire_and_create_entitlements')
current_date = date.today()
expiration_period = current_date - relativedelta(years=1)
exceptional_expiration_period = current_date - relativedelta(years=1, months=6)
normal_entitlements = CourseEntitlement.objects.filter(expired_at__isnull=True, created__lte=expiration_period).exclude(course_uuid__in=MIT_SUPPLY_CHAIN_COURSES)
exceptional_entitlements = CourseEntitlement.objects.filter(expired_at__isnull=True, created__lte=exceptional_expiration_period, course_uuid__in=MIT_SUPPLY_CHAIN_COURSES)
entitlements = normal_entitlements | exceptional_entitlements
countdown = 2 ** self.request.retries
try:
for entitlement in entitlements:
# This property request will update the expiration if necessary as
# a side effect. We could manually call update_expired_at(), but
# let's use the same API the rest of the LMS does, to mimic normal
# usage and allow the update call to be an internal detail.
entitlement.expire_entitlement()
LOGGER.info('Expired entitlement with id %d ', entitlement.id)
entitlement.pk = None
entitlement.expired_at = None
entitlement.modified = None
entitlement.save()
LOGGER.info('created new entitlement with id %d ', entitlement.id)
except Exception as exc:
LOGGER.exception('Failed to expire entitlements ',)
# The call above is idempotent, so retry at will
raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
LOGGER.info('Successfully completed the task expire_and_create_entitlements after examining %d entries', entitlements.count()) # lint-amnesty, pylint: disable=line-too-long