diff --git a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/constants.py b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/constants.py index 3d1b9882e..16def68a1 100644 --- a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/constants.py +++ b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/constants.py @@ -5,3 +5,12 @@ COURSE_RERUN_STATE_SUCCEEDED = "succeeded" REPOSITORY_NAME_MAX_LENGTH = 100 # Max length from GitHub for repo name + +# Debounce settings for the signal handler. +# A single course save triggers 10-30 COURSE_PUBLISHED signals in one request. +# cache.add() on this key ensures only the first signal schedules a task; all +# subsequent signals within the window are silently dropped before hitting the broker. +# The task is scheduled with countdown=EXPORT_DEBOUNCE_DELAY so it runs after +# the burst window has closed and the course state is fully settled. +EXPORT_DEBOUNCE_DELAY = 5 # seconds — must exceed the publish burst window +EXPORT_DEBOUNCE_CACHE_KEY = "git_export_debounce:{course_key}" diff --git a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/tasks.py b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/tasks.py index faaf5c889..71420302f 100644 --- a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/tasks.py +++ b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/tasks.py @@ -16,6 +16,7 @@ from ol_openedx_git_auto_export.models import CourseGitRepository from ol_openedx_git_auto_export.utils import ( + clear_stale_git_lock, export_course_to_git, github_repo_name_format, is_auto_repo_creation_enabled, @@ -39,6 +40,10 @@ def async_export_to_git(course_key_string, user=None): "Starting async course content export to git (course id: %s)", course_module.id, ) + # Remove any stale .git/index.lock left by a previously crashed worker. + # Dirty working-tree files from a prior crash are cleaned by the + # `git reset --hard origin/` + `git clean` inside export_to_git. + clear_stale_git_lock(course_repo.git_url) export_to_git(course_module.id, course_repo.git_url, user=user) else: LOGGER.info( diff --git a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/utils.py b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/utils.py index c422fda36..9303ce8b1 100644 --- a/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/utils.py +++ b/src/ol_openedx_git_auto_export/ol_openedx_git_auto_export/utils.py @@ -5,15 +5,19 @@ import logging import os import re +from pathlib import Path from django.conf import settings from django.contrib.auth.models import User +from django.core.cache import cache from django.core.exceptions import ImproperlyConfigured from xmodule.modulestore.django import modulestore from ol_openedx_git_auto_export.constants import ( ENABLE_AUTO_GITHUB_REPO_CREATION, ENABLE_GIT_AUTO_EXPORT, + EXPORT_DEBOUNCE_CACHE_KEY, + EXPORT_DEBOUNCE_DELAY, REPOSITORY_NAME_MAX_LENGTH, ) @@ -100,7 +104,41 @@ def export_course_to_git(course_key): ) user = get_publisher_username(course_module) - async_export_to_git.delay(str(course_key), user) + + debounce_key = EXPORT_DEBOUNCE_CACHE_KEY.format(course_key=str(course_key)) + if cache.add(debounce_key, "1", timeout=EXPORT_DEBOUNCE_DELAY): + log.info( + "Scheduling git export for course %s with %ds debounce delay", + course_key, + EXPORT_DEBOUNCE_DELAY, + ) + async_export_to_git.apply_async( + args=[str(course_key), user], + countdown=EXPORT_DEBOUNCE_DELAY, + ) + else: + log.info( + "Git export already scheduled for course %s, skipping duplicate signal", + course_key, + ) + + +def clear_stale_git_lock(git_url): + """ + Remove a stale .git/index.lock file for the local clone of git_url, if present. + + A stale lock file can be left behind when a worker process is killed mid-operation. + """ + git_repo_export_dir = getattr( + settings, "GIT_REPO_EXPORT_DIR", "/openedx/export_course_repos" + ) + rdir = git_url.rsplit("/", 1)[-1].rsplit(".git", 1)[0] + index_lock = Path(git_repo_export_dir) / rdir / ".git" / "index.lock" + if index_lock.exists(): + log.warning( + "Removing stale .git/index.lock for repo %s at %s", git_url, index_lock + ) + index_lock.unlink() def is_auto_repo_creation_enabled(): diff --git a/src/ol_openedx_git_auto_export/pyproject.toml b/src/ol_openedx_git_auto_export/pyproject.toml index ece56d2dc..8dc2ab1ed 100644 --- a/src/ol_openedx_git_auto_export/pyproject.toml +++ b/src/ol_openedx_git_auto_export/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ol-openedx-git-auto-export" -version = "0.7.1" +version = "0.7.2" description = "A plugin that auto saves the course OLX to git when an author publishes it" authors = [ {name = "MIT Office of Digital Learning"}