-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: use event producer config #33269
Changes from 4 commits
b46d149
b0f157f
e92bd61
8f6b642
9c0cf67
2f96f8a
982b224
bf8224f
19363a4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -301,23 +301,10 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing | |
CREDENTIALS_PUBLIC_SERVICE_URL = 'http://localhost:18150' | ||
|
||
#################### Event bus backend ######################## | ||
# .. toggle_name: FEATURES['ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS'] | ||
# .. toggle_implementation: DjangoSetting | ||
# .. toggle_default: False | ||
# .. toggle_description: Temporary configuration which enables sending xblock events over the event bus. | ||
# .. toggle_use_cases: open_edx | ||
# .. toggle_creation_date: 2023-02-21 | ||
# .. toggle_warning: For consistency in user experience, keep the value in sync with the setting of the same name | ||
# in the LMS and CMS. | ||
# .. toggle_tickets: 'https://github.com/openedx/edx-platform/pull/31813' | ||
FEATURES['ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS'] = True | ||
FEATURES['ENABLE_SEND_ENROLLMENT_EVENTS_OVER_BUS'] = True | ||
EVENT_BUS_PRODUCER = 'edx_event_bus_redis.create_producer' | ||
EVENT_BUS_REDIS_CONNECTION_URL = 'redis://:[email protected]:6379/' | ||
EVENT_BUS_TOPIC_PREFIX = 'dev' | ||
EVENT_BUS_CONSUMER = 'edx_event_bus_redis.RedisEventConsumer' | ||
EVENT_BUS_XBLOCK_LIFECYCLE_TOPIC = 'course-authoring-xblock-lifecycle' | ||
EVENT_BUS_ENROLLMENT_LIFECYCLE_TOPIC = 'course-authoring-enrollment-lifecycle' | ||
|
||
################# New settings must go ABOVE this line ################# | ||
######################################################################## | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,12 +6,10 @@ | |
|
||
from django.db.models.signals import post_save | ||
from django.dispatch import receiver | ||
from openedx_events.event_bus import get_producer | ||
|
||
from common.djangoapps.course_modes import api as modes_api | ||
from common.djangoapps.student.models import CourseEnrollment | ||
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED | ||
from lms.djangoapps.certificates.config import SEND_CERTIFICATE_CREATED_SIGNAL | ||
from lms.djangoapps.certificates.generation_handler import ( | ||
CertificateGenerationNotAllowed, | ||
generate_allowlist_certificate_task, | ||
|
@@ -32,7 +30,6 @@ | |
COURSE_GRADE_NOW_PASSED, | ||
LEARNER_NOW_VERIFIED | ||
) | ||
from openedx_events.learning.signals import CERTIFICATE_CREATED | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
@@ -159,18 +156,3 @@ def _listen_for_enrollment_mode_change(sender, user, course_key, mode, **kwargs) | |
course_key, | ||
) | ||
return False | ||
|
||
|
||
@receiver(CERTIFICATE_CREATED) | ||
def listen_for_certificate_created_event(sender, signal, **kwargs): | ||
""" | ||
Publish `CERTIFICATE_CREATED` events to the event bus. | ||
""" | ||
if SEND_CERTIFICATE_CREATED_SIGNAL.is_enabled(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you could share the private PR for edx-internal settings so we can cross-reference that the old settings are being replaced by the new, and that they match values (enabled vs disabled)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Share it where? I don't think it makes sense to link it on a public PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you don't want to add private links here, maybe we just add more details to the deployment plan document? |
||
get_producer().send( | ||
signal=CERTIFICATE_CREATED, | ||
topic='learning-certificate-lifecycle', | ||
event_key_field='certificate.course.course_key', | ||
event_data={'certificate': kwargs['certificate']}, | ||
event_metadata=kwargs['metadata'] | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,12 +3,9 @@ | |
and disabling for instructor-paced courses. | ||
""" | ||
|
||
from datetime import datetime, timezone | ||
from unittest import mock | ||
from uuid import uuid4 | ||
|
||
import ddt | ||
from django.test.utils import override_settings | ||
from edx_toggles.toggles.testutils import override_waffle_flag, override_waffle_switch | ||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase | ||
from xmodule.modulestore.tests.factories import CourseFactory | ||
|
@@ -21,14 +18,10 @@ | |
CertificateGenerationConfiguration, | ||
GeneratedCertificate | ||
) | ||
from lms.djangoapps.certificates.signals import listen_for_certificate_created_event | ||
from lms.djangoapps.certificates.tests.factories import CertificateAllowlistFactory, GeneratedCertificateFactory | ||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory | ||
from lms.djangoapps.grades.tests.utils import mock_passing_grade | ||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification | ||
from openedx_events.data import EventsMetadata | ||
from openedx_events.learning.signals import CERTIFICATE_CREATED | ||
from openedx_events.learning.data import CourseData, UserData, UserPersonalData, CertificateData | ||
|
||
|
||
class SelfGeneratedCertsSignalTest(ModuleStoreTestCase): | ||
|
@@ -440,78 +433,3 @@ def test_verified_to_audit(self): | |
) as mock_allowlist_task: | ||
self.verified_enrollment.change_mode('audit') | ||
mock_allowlist_task.assert_not_called() | ||
|
||
|
||
class CertificateEventBusTests(ModuleStoreTestCase): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [non-blocking question] I can't think of anything, but I'm wondering how we might ensure that people would know that a signal is in use by the event bus, and that it can't just be deleted or refactored? Maybe I should create a separate issue for this? |
||
""" | ||
Tests for Certificate events that interact with the event bus. | ||
""" | ||
def setUp(self): | ||
super().setUp() | ||
self.user = UserFactory.create() | ||
self.name = f'{self.user.first_name} {self.user.last_name}' | ||
self.course = CourseFactory.create(self_paced=True) | ||
self.enrollment = CourseEnrollmentFactory( | ||
user=self.user, | ||
course_id=self.course.id, | ||
is_active=True, | ||
mode='verified', | ||
) | ||
|
||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=False) | ||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True) | ||
def test_event_disabled(self, mock_producer): | ||
""" | ||
Test to verify that we do not push `CERTIFICATE_CREATED` events to the event bus if the | ||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is disabled. | ||
""" | ||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED) | ||
mock_producer.assert_not_called() | ||
|
||
@override_settings(SEND_CERTIFICATE_CREATED_SIGNAL=True) | ||
@mock.patch('lms.djangoapps.certificates.signals.get_producer', autospec=True) | ||
def test_event_enabled(self, mock_producer): | ||
""" | ||
Test to verify that we push `CERTIFICATE_CREATED` events to the event bus if the | ||
`SEND_CERTIFICATE_CREATED_SIGNAL` setting is enabled. | ||
""" | ||
expected_course_data = CourseData(course_key=self.course.id) | ||
expected_user_data = UserData( | ||
pii=UserPersonalData( | ||
username=self.user.username, | ||
email=self.user.email, | ||
name=self.name, | ||
), | ||
id=self.user.id, | ||
is_active=self.user.is_active | ||
) | ||
expected_certificate_data = CertificateData( | ||
user=expected_user_data, | ||
course=expected_course_data, | ||
mode='verified', | ||
grade='', | ||
current_status='downloadable', | ||
download_url='', | ||
name='', | ||
) | ||
event_metadata = EventsMetadata( | ||
event_type=CERTIFICATE_CREATED.event_type, | ||
id=uuid4(), | ||
minorversion=0, | ||
source='openedx/lms/web', | ||
sourcehost='lms.test', | ||
time=datetime.now(timezone.utc) | ||
) | ||
|
||
event_kwargs = { | ||
'certificate': expected_certificate_data, | ||
'metadata': event_metadata | ||
} | ||
|
||
listen_for_certificate_created_event(None, CERTIFICATE_CREATED, **event_kwargs) | ||
# verify that the data sent to the event bus matches what we expect | ||
data = mock_producer.return_value.send.call_args.kwargs | ||
assert data['signal'].event_type == CERTIFICATE_CREATED.event_type | ||
assert data['event_data']['certificate'] == expected_certificate_data | ||
assert data['topic'] == 'learning-certificate-lifecycle' | ||
assert data['event_key_field'] == 'certificate.course.course_key' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3304,6 +3304,9 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring | |
|
||
# Notifications | ||
'openedx.core.djangoapps.notifications', | ||
|
||
# Openedx events | ||
'openedx_events', | ||
] | ||
|
||
######################### CSRF ######################################### | ||
|
@@ -5379,6 +5382,26 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring | |
NOTIFICATIONS_EXPIRY = 60 | ||
EXPIRED_NOTIFICATIONS_DELETE_BATCH_SIZE = 10000 | ||
|
||
######################## Event bus producer config ######################## | ||
|
||
# .. setting_name: EVENT_BUS_PRODUCER_CONFIG | ||
# .. setting_default: {} | ||
# .. setting_description: Dictionary of event_types mapped to lists of dictionaries containing topic related configuration. | ||
# Each topic configuration dictionary contains | ||
# * a topic/stream name called `topic` where the event will be pushed to. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think |
||
# * a flag called `enabled` denoting whether the event will be published to the topic. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
# * `event_key_field` which is a period-delimited string path to event data field to use as event key. | ||
# Note: The topic names should not include environment prefix as it will be dynamically added based on | ||
# EVENT_BUS_TOPIC_PREFIX setting. | ||
EVENT_BUS_PRODUCER_CONFIG = { | ||
'org.openedx.learning.certificate.created.v1': [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add toggle annotations for each of these following: https://edx.readthedocs.io/projects/edx-toggles/en/latest/how_to/documenting_new_feature_toggles.html? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The best I can think of is "temporary" but we also don't have a removal date, which is required. opt_in, maybe? or open_edx? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it seems dependent on openedx/openedx-events#265 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good questions.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking more I think we may still want to keep 'ENABLE_SEND_XBLOCK_EVENTS_OVER_BUS' since it actually gates several events which presumably should be launched or held back as a group. We can use the value of it to set the value in the config map. What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Beginning to wonder if the entire map should be derived, with different settings for each event, at least in edx-platform. It's not a perfect solution because of the proliferation of settings, but it I think it's better than having some of them be set in the map and some derived from elsewhere. Less risk of someone trying to only enable or disable one event and not realizing they need to recreate the entire rest of the map in settings. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If someone wants to add or change a topic it will still require setting the entire map, which is frustrating. But I think a better-than-before approach would just be to have enabled/disabled extracted as a different setting for each event and then derive the map from that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you concerned with a single topic, or the entire setting? We could also use KEYS_WITH_MERGED_VALUES. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The entire setting. If they wanted to change a single topic, they'd have to copy over the entire config dict and set all the derived settings correctly, which is non obvious. Keys with merged values looks like it may be our friend here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With keys with merged values, do you feel more comfortable using derived settings on a per-topic basis? Feel free to answer with actual code changes for where you are landing. |
||
{'topic': 'learning-certificate-lifecycle', 'event_key_field': 'certificate.course.course_key', | ||
'enabled': False}, | ||
], | ||
'org.openedx.learning.xblock.skill.verified.v1': [ | ||
{'topic': 'learning-xblock-skill-verified', 'event_key_field': 'xblock_info.usage_key', 'enabled': False}, | ||
] | ||
} | ||
#### django-simple-history## | ||
# disable indexing on date field its coming from django-simple-history. | ||
SIMPLE_HISTORY_DATE_INDEX = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.