diff --git a/CHANGELOG.md b/CHANGELOG.md index 29dd54c6..f5062295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,7 @@ ctrl/command/middle-mouse click #2170 - ⚡️(frontend) add jitter to WS reconnection #2162 - 🐛(frontend) fix tree pagination #2145 - 🐛(nginx) add page reconciliation on nginx #2154 - +- 🐛(backend) fix race condition in reconciliation requests CSV import #2153 ## [v4.8.4] - 2026-03-25 diff --git a/src/backend/core/admin.py b/src/backend/core/admin.py index 3d2b0ef7..88647f94 100644 --- a/src/backend/core/admin.py +++ b/src/backend/core/admin.py @@ -1,7 +1,10 @@ """Admin classes and registrations for core app.""" +from functools import partial + from django.contrib import admin, messages from django.contrib.auth import admin as auth_admin +from django.db import transaction from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ @@ -108,7 +111,9 @@ class UserReconciliationCsvImportAdmin(admin.ModelAdmin): super().save_model(request, obj, form, change) if not change: - user_reconciliation_csv_import_job.delay(obj.pk) + transaction.on_commit( + partial(user_reconciliation_csv_import_job.delay, obj.pk) + ) messages.success(request, _("Import job created and queued.")) return redirect("..") diff --git a/src/backend/core/tasks/user_reconciliation.py b/src/backend/core/tasks/user_reconciliation.py index 364a628d..cdcfbb1d 100644 --- a/src/backend/core/tasks/user_reconciliation.py +++ b/src/backend/core/tasks/user_reconciliation.py @@ -1,6 +1,7 @@ """Processing tasks for user reconciliation CSV imports.""" import csv +import logging import traceback import uuid @@ -14,6 +15,8 @@ from core.models import UserReconciliation, UserReconciliationCsvImport from impress.celery_app import app +logger = logging.getLogger(__name__) + def _process_row(row, job, counters): """Process a single row from the CSV file.""" @@ -89,8 +92,12 @@ def user_reconciliation_csv_import_job(job_id): Rows with errors are logged in the job logs and skipped, but do not cause the entire job to fail or prevent the next rows from being processed. """ - # Imports the CSV file, breaks it into UserReconciliation items - job = UserReconciliationCsvImport.objects.get(id=job_id) + try: + job = UserReconciliationCsvImport.objects.get(id=job_id) + except UserReconciliationCsvImport.DoesNotExist: + logger.warning("CSV import job %s no longer exists; skipping.", job_id) + return + job.status = "running" job.save()