fix: prevent rerun_course from deleting an already-succeeded course [EDLYPRODUCT-8393] - #755
Open
muhammadali286 wants to merge 1 commit into
Open
Conversation
…EDLYPRODUCT-8393] rerun_course() marks the destination course's CourseRerunState as succeeded well before all of its work is done (video copy, restricted course/country rules clone, organization linking). If any of those auxiliary, best-effort steps raised, the catch-all exception handler would flip the state back to failed and delete the destination course -- even though it was already a complete, usable course. - Track whether the rerun has already been marked succeeded. The catch-all handler now only deletes the destination course when that hasn't happened yet (a genuinely partial/garbage clone); once succeeded, the course is preserved and the failure is logged instead. - Wrap video copying, RestrictedCourse/CountryAccessRule cloning, and organization linking each in their own try/except so a failure in one no longer prevents the others from running or endangers the course, with a distinct diagnostic log message per step. Motivated by an incident where a course rerun was confirmed usable and then silently deleted, leaving Studio Home showing a "Configuration error". The exact step that raised in that incident was not conclusively identified; this hardens rerun_course against the whole class of post-succeeded failures regardless of which step throws. This is stock upstream edx-platform behavior with no existing upstream fix; worth upstreaming.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens
rerun_course(cms/djangoapps/contentstore/tasks.py) so it never deletes a destination course that has already been fully cloned and markedsucceeded, and makes the auxiliary post-succeeded steps (video copy, RestrictedCourse/CountryAccessRule clone, organization linking) individually non-fatal.Motivation (EDLYPRODUCT-8393): a client reran a course with an org change. The rerun completed, the course was confirmed usable (a Course Team member was successfully added) -- then it silently disappeared and Studio Home showed "Configuration error". The only code path that deletes a rerun's destination course is the catch-all exception handler in
rerun_course, so something in the post-succeeded()tail of that function (video copy / RestrictedCourse clone / org linking / event dispatch) must have raised.We have not identified the exact operation that threw, and this PR does not claim to. QA could not reproduce the deletion/"Configuration error" on staging (only a transient, unrelated "Not found" on reload for a larger course, which cleared itself). Given that, the fix is deliberately defensive: it protects the course regardless of which post-succeeded step fails, rather than patching one specific suspected cause.
This is stock upstream
edx-platformbehavior (confirmed identical onopenedx/edx-platformmaster) with no existing upstream fix for this bug. (Note: PR/issue openedx#38840/openedx#38826 upstream is a different rerun bug, about theauthz.enable_course_authoringflag failing before any course exists -- not related.) Edly should consider upstreaming this hardening.What changed
rerun_succeededguard, set immediately afterCourseRerunState.objects.succeeded(...)is called. The catch-allexcept Exceptionhandler now only callsmodulestore().delete_course(...)when this is stillFalse(i.e. the clone never completed / never got marked succeeded -- genuine partial garbage). Once the guard isTrue, a later failure is logged but the course is preserved.copy_course_videos, the RestrictedCourse/CountryAccessRule clone, andensure_organization/add_organization_course-- in its own try/except with a distinct diagnostic log message. A failure in one no longer prevents the others from running and no longer flips the rerun to failed or deletes the course.DuplicateCourseErrorhandling and pre-/mid-clone failure cleanup (delete the partial course, mark failed) are unchanged.Behavior change to be aware of
Because the course is no longer deleted after a post-succeeded auxiliary failure, re-running with the exact same destination org+number+run a second time will now hit
DuplicateCourseError(reported as"duplicate course") instead of silently proceeding against a course that no longer exists. This is the correct/intended outcome -- the first course is real and still there -- but it's a visible behavior change from today, where a failed auxiliary step would previously vanish the course and let a retry recreate it from scratch under the same key.Tests
cms/djangoapps/contentstore/tests/test_clone_course.py:test_rerun_course_auxiliary_step_failure_does_not_delete_course-- forcesadd_organization_courseto raise; asserts the task returns"succeeded",CourseRerunStateisSUCCEEDED, and the course is retrievable.test_rerun_course_post_succeeded_failure_preserves_course-- forces a failure inCOURSE_RERUN_COMPLETED.send_event(aftersucceeded(), outside the individually-wrapped steps); asserts the task reports the failure and the state isFAILED, but the course is not deleted.test_rerun_course(failure duringclone_course-> course deleted, stateFAILED) is unchanged and still covers the genuine-partial-clone cleanup path.cms/djangoapps/contentstore/tests/test_tasks.py:test_auxiliary_step_failure_does_not_delete_course-- focused unit test forcingcopy_course_videosto raise, same assertions.Local verification caveat
This local Tutor dev environment has a pre-existing, unrelated issue: any test that invokes a
@shared_task(confirmed on bothrerun_courseand the unrelatedexport_olxtask, and reproducible on unmodifieddevelop-ulmo) currently hitsInvalidCacheBackendError: The connection 'general' doesn't existfrom Django's cache framework during/after task execution. This is independent of this change (verified on the pre-rebase baseline too) and blocks a fully green localpytestrun for these suites in this container specifically. Across all runs, zeroAssertionErrors were raised -- only this cache error -- and captured logs confirm the new guard/logging fires exactly as designed (e.g."...already succeeded before this error was raised; preserving the course instead of deleting it."). CI is the real gate for this PR; recommend treating a green CI run here as authoritative over the local repro.Out-of-scope follow-up observed
While investigating, noticed
handle_reindex_on_signalinopenedx/core/djangoapps/content/search/handlers.py(the receiver forCOURSE_RERUN_COMPLETED/COURSE_IMPORT_COMPLETED) is missing the@only_if_meilisearch_enabledguard that every sibling handler in that file has, so it will raise if Meilisearch is disabled/unreachable when a rerun completes. This fires synchronously off the sameCOURSE_RERUN_COMPLETED.send_event()call insidererun_course, so a Meilisearch hiccup there would land in exactly the failure window this PR now protects -- plausibly consistent with QA's observation that a larger course showed transient issues, though this is speculative and not confirmed. Filing separately rather than expanding this PR's scope.