Skip to content

Commit 5e5495c

Browse files
fix: wrap award_course_certificate.delay in transaction.on_commit
COURSE_CERT_CHANGED fires synchronously during GeneratedCertificate.save(), which runs inside the generate_certificate Celery task. The .delay() call was enqueuing award_course_certificate before the DB transaction committed, so the task raced ahead and hit eligible_certificates.get() DoesNotExist — exiting silently with no retry, and never posting the course cert to Credentials. Wrapping in transaction.on_commit() guarantees the cert row is committed before the task is enqueued. Fixes: EDLYPRODUCT-5411
1 parent 22512ba commit 5e5495c

2 files changed

Lines changed: 11 additions & 4 deletions

File tree

openedx/core/djangoapps/programs/signals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import logging
66

7+
from django.db import transaction
78
from django.dispatch import receiver
89

910
from openedx.core.djangoapps.content.course_overviews.signals import COURSE_PACING_CHANGED
@@ -86,7 +87,7 @@ def handle_course_cert_changed(sender, user, course_key, mode, status, **kwargs)
8687
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
8788
from openedx.core.djangoapps.programs.tasks import award_course_certificate
8889

89-
award_course_certificate.delay(user.username, str(course_key))
90+
transaction.on_commit(lambda: award_course_certificate.delay(user.username, str(course_key)))
9091

9192

9293
@receiver(COURSE_CERT_REVOKED)

openedx/core/djangoapps/programs/tests/test_signals.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ def test_credentials_disabled(self, mock_is_learner_issuance_enabled, mock_task)
142142
assert mock_is_learner_issuance_enabled.call_count == 1
143143
assert mock_task.call_count == 0
144144

145-
def test_credentials_enabled(self, mock_is_learner_issuance_enabled, mock_task):
145+
@mock.patch("django.db.transaction.on_commit", side_effect=lambda f: f())
146+
def test_credentials_enabled(self, mock_on_commit, mock_is_learner_issuance_enabled, mock_task):
146147
"""
147148
Ensures that the receiver function invokes the expected celery task
148149
when the credentials API configuration is enabled.
@@ -152,23 +153,28 @@ def test_credentials_enabled(self, mock_is_learner_issuance_enabled, mock_task):
152153
handle_course_cert_changed(**self.signal_kwargs)
153154

154155
assert mock_is_learner_issuance_enabled.call_count == 1
156+
assert mock_on_commit.call_count == 1
155157
assert mock_task.call_count == 1
156158
assert mock_task.call_args[0] == (TEST_USERNAME, str(TEST_COURSE_KEY))
157159

158-
def test_records_enabled(self, mock_is_learner_issuance_enabled, mock_task):
160+
@mock.patch("django.db.transaction.on_commit", side_effect=lambda f: f())
161+
def test_records_enabled(self, mock_on_commit, mock_is_learner_issuance_enabled, mock_task):
159162
mock_is_learner_issuance_enabled.return_value = True
160163

161164
site_config = SiteConfigurationFactory.create(site_values={"course_org_filter": ["edX"]})
162165

163-
# Correctly sent
166+
# Correctly sent (scheduled via transaction.on_commit)
164167
handle_course_cert_changed(**self.signal_kwargs)
168+
assert mock_on_commit.called
165169
assert mock_task.called
170+
mock_on_commit.reset_mock()
166171
mock_task.reset_mock()
167172

168173
# Correctly not sent
169174
site_config.site_values["ENABLE_LEARNER_RECORDS"] = False
170175
site_config.save()
171176
handle_course_cert_changed(**self.signal_kwargs)
177+
assert not mock_on_commit.called
172178
assert not mock_task.called
173179

174180

0 commit comments

Comments
 (0)