From 9b542ed23f84e472c994748117e69803c91d4721 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 22 Jul 2021 15:55:17 -0400 Subject: [PATCH 1/5] Run exchange rate updated as task if worker cluster is running --- InvenTree/InvenTree/views.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 4b559642ca..80d0a015ff 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -27,6 +27,7 @@ from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet +import InvenTree.status import InvenTree.tasks from .forms import DeleteForm, EditUserForm, SetPasswordForm @@ -802,8 +803,17 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - # Will block for a little bit - InvenTree.tasks.update_exchange_rates() + # Define associated task + task_name = 'InvenTree.tasks.update_exchange_rates' + + if InvenTree.status.is_worker_running(): + # Running as task + InvenTree.tasks.offload_task(task_name) + else: + # Retrieve function from task name + _func = eval(task_name) + # Run it: will block for a little bit + _func() return self.get(request, *args, **kwargs) From d7028b6d744991148bcacb6e2eb260c1733ffed0 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 22 Jul 2021 16:34:35 -0400 Subject: [PATCH 2/5] Make it generic method instead --- InvenTree/InvenTree/tasks.py | 46 ++++++++++++++++++++++++++++++++++++ InvenTree/InvenTree/views.py | 14 ++++------- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index cab87ee64b..1c77febf79 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -69,6 +69,47 @@ def offload_task(taskname, *args, **kwargs): task.run() +def run_task(taskname): + """ + 1. Check if task is implemented + - yes: proceed + - no: return + 2. Check if worker cluster is running + - yes: add task to queue + - no: run it as blocking process + """ + + # Get task list + tasks = get_task_list() + + # Check if task exists + if taskname not in tasks: + logger.warning(f'Task "{taskname}" is not implemented') + return + + from InvenTree.status import is_worker_running + + if is_worker_running(): + # Append module path + taskname = 'InvenTree.tasks.' + taskname + # Running as task + offload_task(taskname) + else: + # Retrieve local method from task name + _func = eval(taskname) + # Run it as blocking process + _func() + + +def get_task_list(): + return [task for task in LOCAL_METHODS if task not in TASK_MANAGEMENT] + + +# Keep TASK_MANAGEMENT before task methods +TASK_MANAGEMENT = [key for key, value in locals().items() if callable(value) and value.__module__ == __name__] +# + + def heartbeat(): """ Simple task which runs at 5 minute intervals, @@ -217,3 +258,8 @@ def send_email(subject, body, recipients, from_email=None): from_email, recipients, ) + + +# Keep LOCAL_METHODS at the end of the file +LOCAL_METHODS = [key for key, value in locals().items() if callable(value) and value.__module__ == __name__] +# diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 80d0a015ff..88199f39a0 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -803,17 +803,11 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - # Define associated task - task_name = 'InvenTree.tasks.update_exchange_rates' + # Define associated task from InvenTree.tasks list of methods + taskname = 'update_exchange_rates' - if InvenTree.status.is_worker_running(): - # Running as task - InvenTree.tasks.offload_task(task_name) - else: - # Retrieve function from task name - _func = eval(task_name) - # Run it: will block for a little bit - _func() + # Run it + InvenTree.tasks.run_task(taskname) return self.get(request, *args, **kwargs) From 3f44233074525ceb89f215e3c0975fe129c6bd9b Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 22 Jul 2021 16:41:45 -0400 Subject: [PATCH 3/5] Improve import --- InvenTree/InvenTree/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 88199f39a0..a783e8fcb3 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -27,9 +27,6 @@ from stock.models import StockLocation, StockItem from common.models import InvenTreeSetting, ColorTheme from users.models import check_user_role, RuleSet -import InvenTree.status -import InvenTree.tasks - from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import ColorThemeSelectForm, SettingCategorySelectForm from .helpers import str2bool @@ -803,11 +800,13 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ + from InvenTree.tasks import run_task + # Define associated task from InvenTree.tasks list of methods taskname = 'update_exchange_rates' # Run it - InvenTree.tasks.run_task(taskname) + run_task(taskname) return self.get(request, *args, **kwargs) From fbdf11e6e7d68062c90083dbd368f6bb2fc4e279 Mon Sep 17 00:00:00 2001 From: eeintech Date: Wed, 4 Aug 2021 11:23:52 -0400 Subject: [PATCH 4/5] Merged run_task code into offload_task function Added option to force synchronous operation Use that option for update_exchange_rates --- InvenTree/InvenTree/tasks.py | 47 +++++++++++++++--------------------- InvenTree/InvenTree/views.py | 4 +-- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 1c77febf79..efe1361bd1 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -51,32 +51,17 @@ def schedule_task(taskname, **kwargs): pass -def offload_task(taskname, *args, **kwargs): - """ - Create an AsyncTask. - This is different to a 'scheduled' task, - in that it only runs once! +def offload_task(taskname, force_sync=False, *args, **kwargs): """ + First check if the task method pointed + by taskname is implemented inside this file. - try: - from django_q.tasks import AsyncTask - except (AppRegistryNotReady): - logger.warning("Could not offload task - app registry not ready") - return + Then create an AsyncTask if workers are running. + This is different to a 'scheduled' task, + in that it only runs once! - task = AsyncTask(taskname, *args, **kwargs) - - task.run() - - -def run_task(taskname): - """ - 1. Check if task is implemented - - yes: proceed - - no: return - 2. Check if worker cluster is running - - yes: add task to queue - - no: run it as blocking process + If workers are not running or force_sync flag + is set then the task is ran synchronously. """ # Get task list @@ -84,20 +69,26 @@ def run_task(taskname): # Check if task exists if taskname not in tasks: - logger.warning(f'Task "{taskname}" is not implemented') + logger.warning(f'Task "{taskname}" is not implemented in InvenTree/tasks.py') return + try: + from django_q.tasks import AsyncTask + except (AppRegistryNotReady): + logger.warning("Could not offload task - app registry not ready") + return from InvenTree.status import is_worker_running - if is_worker_running(): + if is_worker_running() and not force_sync: # Append module path taskname = 'InvenTree.tasks.' + taskname - # Running as task - offload_task(taskname) + # Running as asynchronous task + task = AsyncTask(taskname, *args, **kwargs) + task.run() else: # Retrieve local method from task name _func = eval(taskname) - # Run it as blocking process + # Run it as synchronous task _func() diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index b27b58b40d..03fee01837 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -815,13 +815,13 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - from InvenTree.tasks import run_task + from InvenTree.tasks import offload_task # Define associated task from InvenTree.tasks list of methods taskname = 'update_exchange_rates' # Run it - run_task(taskname) + offload_task(taskname, force_sync=True) return redirect(reverse_lazy('settings')) From 69d1c3cea279e738a0cfda14a25aab5955371781 Mon Sep 17 00:00:00 2001 From: eeintech Date: Mon, 9 Aug 2021 11:55:56 -0400 Subject: [PATCH 5/5] Improved task import to support global --- InvenTree/InvenTree/tasks.py | 70 +++++++++++++++++++----------------- InvenTree/InvenTree/views.py | 2 +- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index efe1361bd1..51a7b6e6f0 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -53,10 +53,7 @@ def schedule_task(taskname, **kwargs): def offload_task(taskname, force_sync=False, *args, **kwargs): """ - First check if the task method pointed - by taskname is implemented inside this file. - - Then create an AsyncTask if workers are running. + Create an AsyncTask if workers are running. This is different to a 'scheduled' task, in that it only runs once! @@ -64,43 +61,55 @@ def offload_task(taskname, force_sync=False, *args, **kwargs): is set then the task is ran synchronously. """ - # Get task list - tasks = get_task_list() - - # Check if task exists - if taskname not in tasks: - logger.warning(f'Task "{taskname}" is not implemented in InvenTree/tasks.py') - return - try: from django_q.tasks import AsyncTask except (AppRegistryNotReady): logger.warning("Could not offload task - app registry not ready") return + import importlib from InvenTree.status import is_worker_running if is_worker_running() and not force_sync: - # Append module path - taskname = 'InvenTree.tasks.' + taskname # Running as asynchronous task - task = AsyncTask(taskname, *args, **kwargs) - task.run() + try: + task = AsyncTask(taskname, *args, **kwargs) + task.run() + except ImportError: + logger.warning(f"WARNING: '{taskname}' not started - Function not found") else: - # Retrieve local method from task name - _func = eval(taskname) - # Run it as synchronous task + # Split path + try: + app, mod, func = taskname.split('.') + app_mod = app + '.' + mod + except ValueError: + logger.warning(f"WARNING: '{taskname}' not started - Malformed function path") + return + + # Import module from app + try: + _mod = importlib.import_module(app_mod) + except ModuleNotFoundError: + logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'") + return + + # Retrieve function + try: + _func = getattr(_mod, func) + except AttributeError: + # getattr does not work for local import + _func = None + + try: + if not _func: + _func = eval(func) + except NameError: + logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") + return + + # Workers are not running: run it as synchronous task _func() -def get_task_list(): - return [task for task in LOCAL_METHODS if task not in TASK_MANAGEMENT] - - -# Keep TASK_MANAGEMENT before task methods -TASK_MANAGEMENT = [key for key, value in locals().items() if callable(value) and value.__module__ == __name__] -# - - def heartbeat(): """ Simple task which runs at 5 minute intervals, @@ -249,8 +258,3 @@ def send_email(subject, body, recipients, from_email=None): from_email, recipients, ) - - -# Keep LOCAL_METHODS at the end of the file -LOCAL_METHODS = [key for key, value in locals().items() if callable(value) and value.__module__ == __name__] -# diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index fbc2190453..0528c6c694 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -828,7 +828,7 @@ class CurrencyRefreshView(RedirectView): from InvenTree.tasks import offload_task # Define associated task from InvenTree.tasks list of methods - taskname = 'update_exchange_rates' + taskname = 'InvenTree.tasks.update_exchange_rates' # Run it offload_task(taskname, force_sync=True)