From 913a05cf45bd72af0efa8065b3a7bc45b583dc1c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 31 Oct 2024 13:59:53 +1100 Subject: [PATCH] [Bug] Fix for create_child_builds (#8399) * Fix for create_child_builds - Account for concurrency between multiple worker processes - Ensure db commits are atomic - Add random delays between build creation * Check for existing build order * Initially force task off to background worker * Revert force_async change --- src/backend/InvenTree/build/serializers.py | 3 +- src/backend/InvenTree/build/tasks.py | 68 +++++++++++++++------- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/backend/InvenTree/build/serializers.py b/src/backend/InvenTree/build/serializers.py index 85053dabf4be..236641b69ccd 100644 --- a/src/backend/InvenTree/build/serializers.py +++ b/src/backend/InvenTree/build/serializers.py @@ -180,6 +180,7 @@ def validate_reference(self, reference): return reference + @transaction.atomic def create(self, validated_data): """Save the Build object.""" @@ -192,7 +193,7 @@ def create(self, validated_data): InvenTree.tasks.offload_task( build.tasks.create_child_builds, build_order.pk, - group='build', + group='build' ) return build_order diff --git a/src/backend/InvenTree/build/tasks.py b/src/backend/InvenTree/build/tasks.py index fbdf4f39c3f9..0df3233179f2 100644 --- a/src/backend/InvenTree/build/tasks.py +++ b/src/backend/InvenTree/build/tasks.py @@ -1,11 +1,15 @@ """Background task definitions for the BuildOrder app.""" import logging +import random +import time + from datetime import timedelta from decimal import Decimal from django.contrib.auth.models import User from django.template.loader import render_to_string +from django.db import transaction from django.utils.translation import gettext_lazy as _ from allauth.account.models import EmailAddress @@ -198,27 +202,49 @@ def create_child_builds(build_id: int) -> None: assembly_items = build_order.part.get_bom_items().filter(sub_part__assembly=True) - for item in assembly_items: - quantity = item.quantity * build_order.quantity - - sub_order = build_models.Build.objects.create( - part=item.sub_part, - quantity=quantity, - title=build_order.title, - batch=build_order.batch, - parent=build_order, - target_date=build_order.target_date, - sales_order=build_order.sales_order, - issued_by=build_order.issued_by, - responsible=build_order.responsible, - ) - - # Offload the child build order creation to the background task queue - InvenTree.tasks.offload_task( - create_child_builds, - sub_order.pk, - group='build' - ) + # Random delay, to reduce likelihood of race conditions from multiple build orders being created simultaneously + time.sleep(random.random()) + + with transaction.atomic(): + # Atomic transaction to ensure that all child build orders are created together, or not at all + # This is critical to prevent duplicate child build orders being created (e.g. if the task is re-run) + + sub_build_ids = [] + + for item in assembly_items: + quantity = item.quantity * build_order.quantity + + + # Check if the child build order has already been created + if build_models.Build.objects.filter( + part=item.sub_part, + parent=build_order, + quantity=quantity, + status__in=BuildStatusGroups.ACTIVE_CODES + ).exists(): + continue + + sub_order = build_models.Build.objects.create( + part=item.sub_part, + quantity=quantity, + title=build_order.title, + batch=build_order.batch, + parent=build_order, + target_date=build_order.target_date, + sales_order=build_order.sales_order, + issued_by=build_order.issued_by, + responsible=build_order.responsible, + ) + + sub_build_ids.append(sub_order.pk) + + for pk in sub_build_ids: + # Offload the child build order creation to the background task queue + InvenTree.tasks.offload_task( + create_child_builds, + pk, + group='build' + ) def notify_overdue_build_order(bo: build_models.Build):