2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-02 05:26:45 +00:00

Task group (#8332)

* Add 'group' to offload_task

- Make use of 'group' field in AsyncTask model
- Allows better db filtering

* Log error if low_stock check cannot be performed

* Ensure low-stock checks are performed by the background worker

* Change encoding of arguments to 'notify_low_stock_if_required'

- Pass part ID, not part instance
- Safer, but requires DB hit

* Fix typo

* Fix to allow tests to run
This commit is contained in:
Oliver 2024-10-24 00:06:44 +11:00 committed by GitHub
parent ba3bac10a7
commit fb17dcce9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 99 additions and 44 deletions

View File

@ -86,4 +86,5 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
recipients, recipients,
fail_silently=False, fail_silently=False,
html_message=html_message, html_message=html_message,
group='notification',
) )

View File

@ -174,6 +174,9 @@ def offload_task(
""" """
from InvenTree.exceptions import log_error from InvenTree.exceptions import log_error
# Extract group information from kwargs
group = kwargs.pop('group', 'inventree')
try: try:
import importlib import importlib
@ -200,7 +203,7 @@ def offload_task(
if force_async or (is_worker_running() and not force_sync): if force_async or (is_worker_running() and not force_sync):
# Running as asynchronous task # Running as asynchronous task
try: try:
task = AsyncTask(taskname, *args, **kwargs) task = AsyncTask(taskname, *args, group=group, **kwargs)
task.run() task.run()
except ImportError: except ImportError:
raise_warning(f"WARNING: '{taskname}' not offloaded - Function not found") raise_warning(f"WARNING: '{taskname}' not offloaded - Function not found")

View File

@ -645,7 +645,8 @@ class Build(
if not InvenTree.tasks.offload_task( if not InvenTree.tasks.offload_task(
build.tasks.complete_build_allocations, build.tasks.complete_build_allocations,
self.pk, self.pk,
user.pk if user else None user.pk if user else None,
group='build'
): ):
raise ValidationError(_("Failed to offload task to complete build allocations")) raise ValidationError(_("Failed to offload task to complete build allocations"))
@ -772,7 +773,8 @@ class Build(
if not InvenTree.tasks.offload_task( if not InvenTree.tasks.offload_task(
build.tasks.complete_build_allocations, build.tasks.complete_build_allocations,
self.pk, self.pk,
user.pk if user else None user.pk if user else None,
group='build',
): ):
raise ValidationError(_("Failed to offload task to complete build allocations")) raise ValidationError(_("Failed to offload task to complete build allocations"))
@ -1441,7 +1443,11 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
instance.create_build_line_items() instance.create_build_line_items()
# Run checks on required parts # Run checks on required parts
InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance) InvenTree.tasks.offload_task(
build_tasks.check_build_stock,
instance,
group='build'
)
# Notify the responsible users that the build order has been created # Notify the responsible users that the build order has been created
InvenTree.helpers_model.notify_responsible(instance, sender, exclude=instance.issued_by) InvenTree.helpers_model.notify_responsible(instance, sender, exclude=instance.issued_by)

View File

@ -191,6 +191,7 @@ class BuildSerializer(NotesFieldMixin, DataImportExportSerializerMixin, InvenTre
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
build.tasks.create_child_builds, build.tasks.create_child_builds,
build_order.pk, build_order.pk,
group='build',
) )
return build_order return build_order
@ -1134,7 +1135,8 @@ class BuildAutoAllocationSerializer(serializers.Serializer):
exclude_location=data.get('exclude_location', None), exclude_location=data.get('exclude_location', None),
interchangeable=data['interchangeable'], interchangeable=data['interchangeable'],
substitutes=data['substitutes'], substitutes=data['substitutes'],
optional_items=data['optional_items'] optional_items=data['optional_items'],
group='build'
): ):
raise ValidationError(_("Failed to start auto-allocation task")) raise ValidationError(_("Failed to start auto-allocation task"))

View File

@ -216,7 +216,8 @@ def create_child_builds(build_id: int) -> None:
# Offload the child build order creation to the background task queue # Offload the child build order creation to the background task queue
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
create_child_builds, create_child_builds,
sub_order.pk sub_order.pk,
group='build'
) )

View File

@ -113,7 +113,9 @@ def after_change_currency(setting) -> None:
InvenTree.tasks.update_exchange_rates(force=True) InvenTree.tasks.update_exchange_rates(force=True)
# Offload update of part prices to a background task # Offload update of part prices to a background task
InvenTree.tasks.offload_task(part_tasks.check_missing_pricing, force_async=True) InvenTree.tasks.offload_task(
part_tasks.check_missing_pricing, force_async=True, group='pricing'
)
def validate_currency_codes(value): def validate_currency_codes(value):

View File

@ -1919,6 +1919,7 @@ class SalesOrderShipment(
order.tasks.complete_sales_order_shipment, order.tasks.complete_sales_order_shipment,
shipment_id=self.pk, shipment_id=self.pk,
user_id=user.pk if user else None, user_id=user.pk if user else None,
group='sales_order',
) )
trigger_event('salesordershipment.completed', id=self.pk) trigger_event('salesordershipment.completed', id=self.pk)

View File

@ -2550,7 +2550,7 @@ class Part(
@receiver(post_save, sender=Part, dispatch_uid='part_post_save_log') @receiver(post_save, sender=Part, dispatch_uid='part_post_save_log')
def after_save_part(sender, instance: Part, created, **kwargs): def after_save_part(sender, instance: Part, created, **kwargs):
"""Function to be executed after a Part is saved.""" """Function to be executed after a Part is saved."""
from pickle import PicklingError from django.conf import settings
from part import tasks as part_tasks from part import tasks as part_tasks
@ -2558,17 +2558,19 @@ def after_save_part(sender, instance: Part, created, **kwargs):
# Check part stock only if we are *updating* the part (not creating it) # Check part stock only if we are *updating* the part (not creating it)
# Run this check in the background # Run this check in the background
try: InvenTree.tasks.offload_task(
InvenTree.tasks.offload_task( part_tasks.notify_low_stock_if_required,
part_tasks.notify_low_stock_if_required, instance instance.pk,
) group='notification',
except PicklingError: force_async=not settings.TESTING, # Force async unless in testing mode
# Can sometimes occur if the referenced Part has issues )
pass
# Schedule a background task to rebuild any supplier parts # Schedule a background task to rebuild any supplier parts
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
part_tasks.rebuild_supplier_parts, instance.pk, force_async=True part_tasks.rebuild_supplier_parts,
instance.pk,
force_async=True,
group='part',
) )
@ -2705,6 +2707,7 @@ class PartPricing(common.models.MetaMixin):
self, self,
counter=counter, counter=counter,
force_async=background, force_async=background,
group='pricing',
) )
def update_pricing( def update_pricing(
@ -3856,7 +3859,10 @@ def post_save_part_parameter_template(sender, instance, created, **kwargs):
if not created: if not created:
# Schedule a background task to rebuild the parameters against this template # Schedule a background task to rebuild the parameters against this template
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
part_tasks.rebuild_parameters, instance.pk, force_async=True part_tasks.rebuild_parameters,
instance.pk,
force_async=True,
group='part',
) )
@ -4548,7 +4554,9 @@ def update_bom_build_lines(sender, instance, created, **kwargs):
if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData(): if InvenTree.ready.canAppAccessDatabase() and not InvenTree.ready.isImportingData():
import build.tasks import build.tasks
InvenTree.tasks.offload_task(build.tasks.update_build_order_lines, instance.pk) InvenTree.tasks.offload_task(
build.tasks.update_build_order_lines, instance.pk, group='build'
)
@receiver(post_save, sender=BomItem, dispatch_uid='post_save_bom_item') @receiver(post_save, sender=BomItem, dispatch_uid='post_save_bom_item')

View File

@ -1316,6 +1316,7 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
exclude_external=data.get('exclude_external', True), exclude_external=data.get('exclude_external', True),
generate_report=data.get('generate_report', True), generate_report=data.get('generate_report', True),
update_parts=data.get('update_parts', True), update_parts=data.get('update_parts', True),
group='report',
) )

View File

@ -14,7 +14,7 @@ import company.models
import InvenTree.helpers import InvenTree.helpers
import InvenTree.helpers_model import InvenTree.helpers_model
import InvenTree.tasks import InvenTree.tasks
import part.models import part.models as part_models
import part.stocktake import part.stocktake
from common.settings import get_global_setting from common.settings import get_global_setting
from InvenTree.tasks import ( from InvenTree.tasks import (
@ -27,7 +27,7 @@ from InvenTree.tasks import (
logger = logging.getLogger('inventree') logger = logging.getLogger('inventree')
def notify_low_stock(part: part.models.Part): def notify_low_stock(part: part_models.Part):
"""Notify interested users that a part is 'low stock'. """Notify interested users that a part is 'low stock'.
Rules: Rules:
@ -51,20 +51,28 @@ def notify_low_stock(part: part.models.Part):
) )
def notify_low_stock_if_required(part: part.models.Part): def notify_low_stock_if_required(part_id: int):
"""Check if the stock quantity has fallen below the minimum threshold of part. """Check if the stock quantity has fallen below the minimum threshold of part.
If true, notify the users who have subscribed to the part If true, notify the users who have subscribed to the part
""" """
try:
part = part_models.Part.objects.get(pk=part_id)
except part_models.Part.DoesNotExist:
logger.warning(
'notify_low_stock_if_required: Part with ID %s does not exist', part_id
)
return
# Run "up" the tree, to allow notification for "parent" parts # Run "up" the tree, to allow notification for "parent" parts
parts = part.get_ancestors(include_self=True, ascending=True) parts = part.get_ancestors(include_self=True, ascending=True)
for p in parts: for p in parts:
if p.is_part_low_on_stock(): if p.is_part_low_on_stock():
InvenTree.tasks.offload_task(notify_low_stock, p) InvenTree.tasks.offload_task(notify_low_stock, p, group='notification')
def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0): def update_part_pricing(pricing: part_models.PartPricing, counter: int = 0):
"""Update cached pricing data for the specified PartPricing instance. """Update cached pricing data for the specified PartPricing instance.
Arguments: Arguments:
@ -93,7 +101,7 @@ def check_missing_pricing(limit=250):
limit: Maximum number of parts to process at once limit: Maximum number of parts to process at once
""" """
# Find parts for which pricing information has never been updated # Find parts for which pricing information has never been updated
results = part.models.PartPricing.objects.filter(updated=None)[:limit] results = part_models.PartPricing.objects.filter(updated=None)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info('Found %s parts with empty pricing', results.count()) logger.info('Found %s parts with empty pricing', results.count())
@ -105,7 +113,7 @@ def check_missing_pricing(limit=250):
days = int(get_global_setting('PRICING_UPDATE_DAYS', 30)) days = int(get_global_setting('PRICING_UPDATE_DAYS', 30))
stale_date = datetime.now().date() - timedelta(days=days) stale_date = datetime.now().date() - timedelta(days=days)
results = part.models.PartPricing.objects.filter(updated__lte=stale_date)[:limit] results = part_models.PartPricing.objects.filter(updated__lte=stale_date)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info('Found %s stale pricing entries', results.count()) logger.info('Found %s stale pricing entries', results.count())
@ -115,7 +123,7 @@ def check_missing_pricing(limit=250):
# Find any pricing data which is in the wrong currency # Find any pricing data which is in the wrong currency
currency = common.currency.currency_code_default() currency = common.currency.currency_code_default()
results = part.models.PartPricing.objects.exclude(currency=currency) results = part_models.PartPricing.objects.exclude(currency=currency)
if results.count() > 0: if results.count() > 0:
logger.info('Found %s pricing entries in the wrong currency', results.count()) logger.info('Found %s pricing entries in the wrong currency', results.count())
@ -124,7 +132,7 @@ def check_missing_pricing(limit=250):
pp.schedule_for_update() pp.schedule_for_update()
# Find any parts which do not have pricing information # Find any parts which do not have pricing information
results = part.models.Part.objects.filter(pricing_data=None)[:limit] results = part_models.Part.objects.filter(pricing_data=None)[:limit]
if results.count() > 0: if results.count() > 0:
logger.info('Found %s parts without pricing', results.count()) logger.info('Found %s parts without pricing', results.count())
@ -152,7 +160,7 @@ def scheduled_stocktake_reports():
get_global_setting('STOCKTAKE_DELETE_REPORT_DAYS', 30, cache=False) get_global_setting('STOCKTAKE_DELETE_REPORT_DAYS', 30, cache=False)
) )
threshold = datetime.now() - timedelta(days=delete_n_days) threshold = datetime.now() - timedelta(days=delete_n_days)
old_reports = part.models.PartStocktakeReport.objects.filter(date__lt=threshold) old_reports = part_models.PartStocktakeReport.objects.filter(date__lt=threshold)
if old_reports.count() > 0: if old_reports.count() > 0:
logger.info('Deleting %s stale stocktake reports', old_reports.count()) logger.info('Deleting %s stale stocktake reports', old_reports.count())
@ -187,11 +195,11 @@ def rebuild_parameters(template_id):
which may cause the base unit to be adjusted. which may cause the base unit to be adjusted.
""" """
try: try:
template = part.models.PartParameterTemplate.objects.get(pk=template_id) template = part_models.PartParameterTemplate.objects.get(pk=template_id)
except part.models.PartParameterTemplate.DoesNotExist: except part_models.PartParameterTemplate.DoesNotExist:
return return
parameters = part.models.PartParameter.objects.filter(template=template) parameters = part_models.PartParameter.objects.filter(template=template)
n = 0 n = 0
@ -216,8 +224,8 @@ def rebuild_supplier_parts(part_id):
which may cause the native units of any supplier parts to be updated which may cause the native units of any supplier parts to be updated
""" """
try: try:
prt = part.models.Part.objects.get(pk=part_id) prt = part_models.Part.objects.get(pk=part_id)
except part.models.Part.DoesNotExist: except part_models.Part.DoesNotExist:
return return
supplier_parts = company.models.SupplierPart.objects.filter(part=prt) supplier_parts = company.models.SupplierPart.objects.filter(part=prt)

View File

@ -40,7 +40,7 @@ def trigger_event(event, *args, **kwargs):
if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS: if 'force_async' not in kwargs and not settings.PLUGIN_TESTING_EVENTS:
kwargs['force_async'] = True kwargs['force_async'] = True
offload_task(register_event, event, *args, **kwargs) offload_task(register_event, event, *args, group='plugin', **kwargs)
def register_event(event, *args, **kwargs): def register_event(event, *args, **kwargs):
@ -77,7 +77,9 @@ def register_event(event, *args, **kwargs):
kwargs['force_async'] = True kwargs['force_async'] = True
# Offload a separate task for each plugin # Offload a separate task for each plugin
offload_task(process_event, slug, event, *args, **kwargs) offload_task(
process_event, slug, event, *args, group='plugin', **kwargs
)
def process_event(plugin_slug, event, *args, **kwargs): def process_event(plugin_slug, event, *args, **kwargs):

View File

@ -185,7 +185,12 @@ class LabelPrintingMixin:
# Exclude the 'context' object - cannot be pickled # Exclude the 'context' object - cannot be pickled
print_args.pop('context', None) print_args.pop('context', None)
offload_task(plugin_label.print_label, self.plugin_slug(), **print_args) offload_task(
plugin_label.print_label,
self.plugin_slug(),
group='plugin',
**print_args,
)
# Update the progress of the print job # Update the progress of the print job
output.progress += int(100 / N) output.progress += int(100 / N)

View File

@ -59,7 +59,11 @@ class LocatePluginView(GenericAPIView):
StockItem.objects.get(pk=item_pk) StockItem.objects.get(pk=item_pk)
offload_task( offload_task(
registry.call_plugin_function, plugin, 'locate_stock_item', item_pk registry.call_plugin_function,
plugin,
'locate_stock_item',
item_pk,
group='plugin',
) )
data['item'] = item_pk data['item'] = item_pk
@ -78,6 +82,7 @@ class LocatePluginView(GenericAPIView):
plugin, plugin,
'locate_stock_location', 'locate_stock_location',
location_pk, location_pk,
group='plugin',
) )
data['location'] = location_pk data['location'] = location_pk

View File

@ -95,7 +95,9 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, InvenTreePlugin):
if driver.USE_BACKGROUND_WORKER is False: if driver.USE_BACKGROUND_WORKER is False:
return driver.print_labels(machine, label, items, **print_kwargs) return driver.print_labels(machine, label, items, **print_kwargs)
offload_task(driver.print_labels, machine, label, items, **print_kwargs) offload_task(
driver.print_labels, machine, label, items, group='plugin', **print_kwargs
)
return JsonResponse({ return JsonResponse({
'success': True, 'success': True,

View File

@ -236,7 +236,9 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
if active: if active:
offload_task(check_for_migrations) offload_task(check_for_migrations)
offload_task(plugin.staticfiles.copy_plugin_static_files, self.key) offload_task(
plugin.staticfiles.copy_plugin_static_files, self.key, group='plugin'
)
class PluginSetting(common.models.BaseInvenTreeSetting): class PluginSetting(common.models.BaseInvenTreeSetting):

View File

@ -1680,7 +1680,7 @@ class StockItem(
# Rebuild the stock tree # Rebuild the stock tree
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
stock.tasks.rebuild_stock_item_tree, tree_id=self.tree_id stock.tasks.rebuild_stock_item_tree, tree_id=self.tree_id, group='part'
) )
@transaction.atomic @transaction.atomic
@ -1915,7 +1915,7 @@ class StockItem(
# Rebuild stock trees as required # Rebuild stock trees as required
for tree_id in tree_ids: for tree_id in tree_ids:
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
stock.tasks.rebuild_stock_item_tree, tree_id=tree_id stock.tasks.rebuild_stock_item_tree, tree_id=tree_id, group='stock'
) )
@transaction.atomic @transaction.atomic
@ -2012,7 +2012,7 @@ class StockItem(
# Rebuild the tree for this parent item # Rebuild the tree for this parent item
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
stock.tasks.rebuild_stock_item_tree, tree_id=self.tree_id stock.tasks.rebuild_stock_item_tree, tree_id=self.tree_id, group='stock'
) )
# Attempt to reload the new item from the database # Attempt to reload the new item from the database
@ -2405,7 +2405,10 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
if InvenTree.ready.canAppAccessDatabase(allow_test=True): if InvenTree.ready.canAppAccessDatabase(allow_test=True):
# Run this check in the background # Run this check in the background
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
part_tasks.notify_low_stock_if_required, instance.part part_tasks.notify_low_stock_if_required,
instance.part.pk,
group='notification',
force_async=True,
) )
if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING): if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING):
@ -2422,7 +2425,10 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs):
if created and not InvenTree.ready.isImportingData(): if created and not InvenTree.ready.isImportingData():
if InvenTree.ready.canAppAccessDatabase(allow_test=True): if InvenTree.ready.canAppAccessDatabase(allow_test=True):
InvenTree.tasks.offload_task( InvenTree.tasks.offload_task(
part_tasks.notify_low_stock_if_required, instance.part part_tasks.notify_low_stock_if_required,
instance.part.pk,
group='notification',
force_async=True,
) )
if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING): if InvenTree.ready.canAppAccessDatabase(allow_test=settings.TESTING_PRICING):