├── .gitignore ├── LICENSE ├── README.md ├── celery ├── __init__.py ├── __manifest__.py ├── data │ ├── ir_config_parameter_data.xml │ └── ir_cron_data.xml ├── example.odoo.conf ├── example_section_celery.odoo.conf ├── fields.py ├── migrations │ └── 11.0.0.16 │ │ └── post-populate_existing_celery_queues.py ├── models │ ├── __init__.py │ ├── celery_queue.py │ ├── celery_task.py │ ├── celery_task_setting.py │ ├── res_config_settings.py │ └── res_users.py ├── odoo.py ├── report │ ├── __init__.py │ ├── celery_stuck_task_report.py │ └── celery_stuck_task_report_views.xml ├── security │ ├── celery_security.xml │ └── ir_model_access.xml ├── static │ └── description │ │ ├── banner.png │ │ ├── banner.xcf │ │ ├── icon.png │ │ ├── icon.xcf │ │ ├── index.html │ │ ├── odoo-example-task-form.png │ │ ├── odoo-example-task-list.png │ │ ├── odoo-requeue-multiple-tasks.png │ │ ├── odoo-requeue-single-task.png │ │ ├── odoo-task-failure.png │ │ ├── odoo-task-queue-failure.png │ │ ├── odoo-task-queue-retry.png │ │ └── odoo-task-retry.png ├── tests │ ├── __init__.py │ └── test_celery_task.py ├── views │ ├── celery_menu.xml │ ├── celery_queue_views.xml │ ├── celery_task_setting_views.xml │ ├── celery_task_views.xml │ └── res_config_settings_views.xml └── wizard │ ├── __init__.py │ ├── celery_cancel_task.py │ ├── celery_cancel_task_views.xml │ ├── celery_handle_stuck_task.py │ ├── celery_handle_stuck_task_views.xml │ ├── celery_requeue_task.py │ └── celery_requeue_task_views.xml ├── celery_example ├── __init__.py ├── __manifest__.py ├── data │ └── celery_example_data.xml ├── models │ ├── __init__.py │ └── celery_example.py ├── security │ └── ir_model_access.xml ├── static │ └── description │ │ ├── banner.png │ │ └── icon.png └── views │ └── celery_example_views.xml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odoo-celery 2 | Connect Odoo (API) with Celery (Distributed Task Queue) 3 | -------------------------------------------------------------------------------- /celery/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import odoo 5 | from . import fields 6 | from . import models 7 | from . import report 8 | from . import wizard 9 | -------------------------------------------------------------------------------- /celery/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | { 4 | 'name': 'Celery', 5 | 'summary': 'Celery (Distributed Task Queue)', 6 | 'category': 'Extra Tools', 7 | 'version': '0.27', 8 | 'description': """Execute Odoo methods by Celery worker tasks.""", 9 | 'author': 'Nova Code', 10 | 'website': 'https://www.novacode.nl', 11 | 'license': "LGPL-3", 12 | 'depends': [ 13 | 'mail' 14 | ], 15 | 'external_dependencies': { 16 | 'python': ['celery'], 17 | }, 18 | 'data': [ 19 | 'data/ir_cron_data.xml', 20 | 'data/ir_config_parameter_data.xml', 21 | 'security/celery_security.xml', 22 | 'security/ir_model_access.xml', 23 | 'wizard/celery_requeue_task_views.xml', 24 | 'wizard/celery_cancel_task_views.xml', 25 | 'wizard/celery_handle_stuck_task_views.xml', 26 | 'report/celery_stuck_task_report_views.xml', 27 | 'views/celery_task_views.xml', 28 | 'views/celery_task_setting_views.xml', 29 | 'views/celery_queue_views.xml', 30 | 'views/celery_menu.xml', 31 | 'views/res_config_settings_views.xml', 32 | ], 33 | 'images': [ 34 | 'static/description/banner.png', 35 | ], 36 | 'installable': True, 37 | 'application' : True, 38 | } 39 | -------------------------------------------------------------------------------- /celery/data/ir_config_parameter_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.celery_base_url 9 | http://127.0.0.1:8069 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /celery/data/ir_cron_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Celery: Handle Stuck Tasks 9 | 10 | code 11 | model.cron_handle_stuck_tasks() 12 | 13 | 14 | 2 15 | hours 16 | -1 17 | 10 18 | 19 | 20 | 21 | 22 | Celery: Autovacuum Tasks 23 | 24 | code 25 | ## Default settings to override with kwargs: 26 | # 27 | # days=90 (Delete tasks older then 90 days) 28 | # hours=0 (Set hours to delete tasks older then timedelta addition of N hours) 29 | # minutes=0 (Set minutes to delete tasks older then timedelta addition of N minutes) 30 | # seconds=0 (Set seconds to delete tasks older then timedelta addition of N seconds) 31 | # 32 | # success=True (Delete tasks with state=Success) 33 | # failure=True (Delete tasks with state=Failure) 34 | # cancel=True (Delete tasks with state=Cancel) 35 | # 36 | # batch_size=100 (Delete N records in run/loop) 37 | model.autovacuum() 38 | 39 | 40 | 41 | 1 42 | hours 43 | -1 44 | 10 45 | 46 | 47 | 48 | 49 | 50 | 51 | Celery: Add Existing/Used Queues 52 | 53 | code 54 | model.cron_add_existing_queues() 55 | 56 | 57 | 1 58 | days 59 | -1 60 | 10 61 | 62 | 63 | 64 | Celery: Handle Scheduled Tasks 65 | 66 | code 67 | model.cron_handle_scheduled_tasks() 68 | 69 | 70 | 10 71 | minutes 72 | -1 73 | 10 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /celery/example.odoo.conf: -------------------------------------------------------------------------------- 1 | ## Following settings are required for the Celery (Task Queue) deployment. 2 | 3 | celery_user = Odoo-User 4 | celery_password = Odoo-User-Password 5 | 6 | ## Optional: 7 | ## The run_task() XML-RPC, called from Celery, shall be executed as the Odoo admin user. 8 | ## So this circumvents model-access configuration for models. 9 | # celery_sudo = True -------------------------------------------------------------------------------- /celery/example_section_celery.odoo.conf: -------------------------------------------------------------------------------- 1 | ## Following settings are required for the Celery (Task Queue) deployment. 2 | 3 | [celery] 4 | user = Odoo-User 5 | password = Odoo-User-Password 6 | 7 | ## Optional: 8 | ## The run_task() XML-RPC, called from Celery, shall be executed as the Odoo admin user. 9 | ## So this circumvents model-access configuration for models. 10 | # sudo = True -------------------------------------------------------------------------------- /celery/fields.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import json 5 | 6 | from odoo import fields, models 7 | 8 | 9 | class KwargsSerialized(fields.Field): 10 | """ Serialized fields provide the storage for sparse fields. """ 11 | type = 'task_serialized' 12 | column_type = ('text', 'text') 13 | 14 | def convert_to_column(self, value, record, values=None): 15 | return json.dumps(value) 16 | 17 | 18 | class ListSerialized(fields.Field): 19 | type = 'list_serialized' 20 | column_type = ('text', 'text') 21 | 22 | def convert_to_column(self, value, record, values=None): 23 | return json.dumps(value) 24 | -------------------------------------------------------------------------------- /celery/migrations/11.0.0.16/post-populate_existing_celery_queues.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | 3 | def migrate(cr, version): 4 | query = """ 5 | INSERT INTO celery_queue (name, active) 6 | SELECT DISTINCT queue, true FROM celery_task WHERE queue NOT IN (SELECT name FROM celery_queue) 7 | """ 8 | cr.execute(query) 9 | -------------------------------------------------------------------------------- /celery/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import celery_task 5 | from . import celery_task_setting 6 | from . import celery_queue 7 | from . import res_config_settings 8 | from . import res_users 9 | -------------------------------------------------------------------------------- /celery/models/celery_queue.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models, _ 5 | from .celery_task import STATE_PENDING, STATE_RETRY, STATE_FAILURE, STATE_SUCCESS 6 | 7 | 8 | class CeleryQueue(models.Model): 9 | _name = 'celery.queue' 10 | _description = 'Celery Queue' 11 | _inherit = ['mail.thread'] 12 | _order = 'name' 13 | 14 | def _get_task_settings(self): 15 | for record in self: 16 | task_setting_ids = self.env['celery.task.setting.queue'].search([('queue_id', '=', record.id)]) or [] 17 | if task_setting_ids: 18 | task_setting_ids = [t.task_setting_id.id for t in task_setting_ids] 19 | record.task_setting_ids = task_setting_ids 20 | 21 | def _compute_stats(self): 22 | if 'compute_queue_stats' in self._context: 23 | self._cr.execute("""SELECT 24 | 'total_tasks:all' AS queue_stat_field, COUNT(*) AS counted 25 | FROM celery_task 26 | UNION 27 | SELECT 28 | queue || ':all' AS queue_stat_field, COUNT(*) AS counted 29 | FROM celery_task 30 | GROUP BY queue 31 | UNION 32 | SELECT 33 | queue || ':pending' AS queue_stat_field, COUNT(*) AS counted 34 | FROM celery_task 35 | WHERE state = %s 36 | GROUP BY queue 37 | UNION 38 | SELECT 39 | queue || ':24h' AS queue_stat_field, COUNT(*) AS counted 40 | FROM celery_task 41 | WHERE create_date > (current_timestamp - interval '24 hour') 42 | GROUP BY queue 43 | UNION 44 | SELECT 45 | queue || ':24h_done' AS queue_stat_field, COUNT(*) AS counted 46 | FROM celery_task 47 | WHERE create_date > (current_timestamp - interval '24 hour') 48 | AND state = %s 49 | GROUP BY queue 50 | UNION 51 | SELECT 52 | queue || ':24h_failed' AS queue_stat_field, COUNT(*) AS counted 53 | FROM celery_task 54 | WHERE create_date > (current_timestamp - interval '24 hour') 55 | AND (state = %s OR state = %s) 56 | GROUP BY queue 57 | """, (STATE_PENDING, STATE_SUCCESS, STATE_FAILURE, STATE_RETRY)) 58 | queue_tasks = self._cr.fetchall() 59 | 60 | queue_tasks = dict(queue_tasks) 61 | total_tasks = queue_tasks.get('total_tasks:all', 1) 62 | 63 | for record in self: 64 | record.queue_tasks = queue_tasks.get(record.name + ':all', 0) 65 | record.queue_tasks_pending = queue_tasks.get(record.name + ':pending', 0) 66 | record.queue_tasks_24h = queue_tasks.get(record.name + ':24h', 0) 67 | record.queue_tasks_24h_done = queue_tasks.get(record.name + ':24h_done', 0) 68 | record.queue_tasks_24h_failed = queue_tasks.get(record.name + ':24h_failed', 0) 69 | queue_tasks_24h = record.queue_tasks_24h 70 | if queue_tasks_24h == 0: queue_tasks_24h = 1 # avoid division by zero 71 | record.queue_tasks_ratio = (float(record.queue_tasks) / float(total_tasks)) * 100.00 72 | record.queue_percentage = (float(record.queue_tasks_24h_done) / float(queue_tasks_24h)) * 100.00 73 | 74 | name = fields.Char('Name', required=True, tracking=True) 75 | active = fields.Boolean(string='Active', default=True, tracking=True) 76 | task_setting_ids = fields.Many2many( 77 | string="Tasks", 78 | comodel_name="celery.task.setting", 79 | store=False, 80 | compute='_get_task_settings', 81 | help="Types of tasks that can be executed in this queue.", 82 | ) 83 | 84 | # stat fields 85 | queue_tasks = fields.Integer(string="Total number of tasks in the queue", compute='_compute_stats', store=False, group_operator="avg") 86 | queue_tasks_pending = fields.Integer(string="Pending tasks in the queue", compute='_compute_stats', store=False, group_operator="avg") 87 | queue_tasks_24h = fields.Integer(string="Added in the last 24h", compute='_compute_stats', store=False, group_operator="avg") 88 | queue_tasks_24h_done = fields.Integer(string="Succeeded in the last 24h", compute='_compute_stats', store=False, group_operator="avg") 89 | queue_tasks_24h_failed = fields.Integer(string="Failed in the last 24h", compute='_compute_stats', store=False, group_operator="avg") 90 | queue_percentage = fields.Float(string=" ", compute='_compute_stats', store=False, group_operator="avg") 91 | queue_tasks_ratio = fields.Float(string="Percentage of total tasks", compute='_compute_stats', store=False, group_operator="avg") 92 | color = fields.Integer('Color Index', default=0) 93 | 94 | _sql_constraints = [ 95 | ('name_uniq', 'UNIQUE(name)', "Queue already exists!"), 96 | ] 97 | 98 | @api.model 99 | def cron_add_existing_queues(self): 100 | query = """ 101 | INSERT INTO celery_queue (name, active) 102 | SELECT DISTINCT queue, true FROM celery_task WHERE queue NOT IN (SELECT name FROM celery_queue) 103 | """ 104 | self._cr.execute(query) 105 | -------------------------------------------------------------------------------- /celery/models/celery_task.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import copy 5 | import json 6 | import logging 7 | import os 8 | import traceback 9 | import uuid 10 | import pytz 11 | 12 | from datetime import date, datetime, timedelta 13 | 14 | from odoo import api, fields, models, registry, _ 15 | from odoo.exceptions import UserError 16 | from odoo.modules import registry as model_registry 17 | from odoo.tools import config 18 | 19 | from ..odoo import call_task, TASK_DEFAULT_QUEUE 20 | from ..fields import KwargsSerialized, ListSerialized 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | TASK_NOT_FOUND = 'NOT_FOUND' 25 | 26 | STATE_PENDING = 'PENDING' 27 | STATE_SCHEDULED = 'SCHEDULED' 28 | STATE_STARTED = 'STARTED' 29 | STATE_RETRY = 'RETRY' 30 | STATE_RETRYING = 'RETRYING' 31 | STATE_FAILURE = 'FAILURE' 32 | STATE_SUCCESS = 'SUCCESS' 33 | STATE_CANCEL = 'CANCEL' 34 | 35 | STATES = [(STATE_PENDING, 'Pending'), 36 | (STATE_SCHEDULED, 'Scheduled'), 37 | (STATE_STARTED, 'Started'), 38 | (STATE_RETRY, 'Retry'), 39 | (STATE_RETRYING, 'Retrying'), 40 | (STATE_FAILURE, 'Failure'), 41 | (STATE_SUCCESS, 'Success'), 42 | (STATE_CANCEL, 'Cancel')] 43 | 44 | STATES_TO_STUCK = [STATE_STARTED, STATE_RETRY, STATE_RETRYING] 45 | STATES_TO_CANCEL = [STATE_PENDING, STATE_SCHEDULED] 46 | STATES_TO_REQUEUE = [STATE_PENDING, STATE_SCHEDULED, STATE_RETRY, STATE_FAILURE] 47 | 48 | 49 | CELERY_PARAMS = [ 50 | 'queue', 'retry', 'max_retries', 'interval_start', 'interval_step', 51 | 'countdown', 'retry_countdown_setting', 'retry_countdown_add_seconds', 52 | 'retry_countdown_multiply_retries_seconds'] 53 | 54 | RETRY_COUNTDOWN_ADD_SECONDS = 'ADD_SECS' 55 | RETRY_COUNTDOWN_MULTIPLY_RETRIES = 'MUL_RETRIES' 56 | RETRY_COUNTDOWN_MULTIPLY_RETRIES_SECCONDS = 'MUL_RETRIES_SECS' 57 | 58 | RETRY_COUNTDOWN_SETTINGS = [ 59 | (RETRY_COUNTDOWN_ADD_SECONDS, 'Add seconds to retry countdown'), 60 | (RETRY_COUNTDOWN_MULTIPLY_RETRIES, 'Multiply retry countdown * request retries'), 61 | (RETRY_COUNTDOWN_MULTIPLY_RETRIES_SECCONDS, 'Multiply retry countdown: retries * seconds'), 62 | ] 63 | 64 | 65 | def _get_celery_user_config(): 66 | user = (os.environ.get('ODOO_CELERY_USER') or config.misc.get("celery", {}).get('user') or config.get('celery_user')) 67 | password = (os.environ.get('ODOO_CELERY_PASSWORD') or config.misc.get("celery", {}).get('password') or config.get('celery_password')) 68 | sudo = (os.environ.get('ODOO_CELERY_SUDO') or config.misc.get("celery", {}).get('sudo') or config.get('celery_sudo')) 69 | return (user, password, sudo) 70 | 71 | 72 | class CeleryTask(models.Model): 73 | _name = 'celery.task' 74 | _description = 'Celery Task' 75 | _inherit = ['mail.thread'] 76 | _rec_name = 'uuid' 77 | _order = 'id DESC' 78 | 79 | uuid = fields.Char(string='UUID', readonly=True, index=True, required=True) 80 | queue = fields.Char(string='Queue', readonly=True, required=True, default=TASK_DEFAULT_QUEUE, index=True) 81 | user_id = fields.Many2one('res.users', string='User ID', required=True, readonly=True) 82 | company_id = fields.Many2one('res.company', string='Company', index=True, readonly=True) 83 | model = fields.Char(string='Model', readonly=True) 84 | method = fields.Char(string='Method', readonly=True) 85 | kwargs = KwargsSerialized(readonly=True) 86 | ref = fields.Char(string='Reference', index=True, readonly=True) 87 | started_date = fields.Datetime(string='Start Time', readonly=True) 88 | # TODO REFACTOR compute and store, by @api.depends (replace all ORM writes) 89 | state_date = fields.Datetime(string='State Time', index=True, readonly=True) 90 | scheduled_date = fields.Datetime(string="Scheduled Time", readonly=True) 91 | result = fields.Text(string='Result', readonly=True) 92 | exc_info = fields.Text(string='Exception Info', readonly=True, tracking=True) 93 | state = fields.Selection( 94 | selection='_selection_states', 95 | string="State", 96 | default=STATE_PENDING, 97 | required=True, 98 | readonly=True, 99 | index=True, 100 | tracking=True, 101 | help="""\ 102 | - PENDING: The task is waiting for execution. 103 | - SCHEDULED: The task is pending and scheduled to be run within a specified timeframe. 104 | - STARTED: The task has been started. 105 | - RETRY: The task is to be retried, possibly because of failure. 106 | - RETRYING: The task is executing a retry, possibly because of failure. 107 | - FAILURE: The task raised an exception, or has exceeded the retry limit. 108 | - SUCCESS: The task executed successfully. 109 | - CANCEL: The task has been aborted and cancelled by user action.""") 110 | res_model = fields.Char(string='Related Model', readonly=True) 111 | res_ids = ListSerialized(string='Related Ids', readonly=True) 112 | stuck = fields.Boolean(string='Stuck') 113 | 114 | # Celery Retry Policy 115 | # http://docs.celeryproject.org/en/latest/userguide/calling.html#retry-policy 116 | retry = fields.Boolean(default=True) 117 | max_retries = fields.Integer() # Don't default here (Celery already does) 118 | interval_start = fields.Float( 119 | help='Defines the number of seconds (float or integer) to wait between Broker Connection retries. '\ 120 | 'Default is 0 (the first retry will be instantaneous).') # Don't default here (Celery already does) 121 | interval_step = fields.Float( 122 | help='On each consecutive retry this number will be added to the Broker Connection retry delay (float or integer). '\ 123 | 'Default is 0.2.') # Don't default here (Celery already does) 124 | countdown = fields.Integer(help='ETA by seconds into the future. Also used in the retry.') 125 | retry_countdown_setting = fields.Selection( 126 | selection='_selection_retry_countdown_settings', string='Retry Countdown Setting') 127 | retry_countdown_add_seconds = fields.Integer(string='Retry Countdown add seconds') 128 | retry_countdown_multiply_retries_seconds = fields.Integer(string='Retry Countdown multiply retries seconds') 129 | transaction_strategy = fields.Char(string='Transaction Strategy', readonly=True) 130 | 131 | def _selection_states(self): 132 | return STATES 133 | 134 | def _selection_retry_countdown_settings(self): 135 | return RETRY_COUNTDOWN_SETTINGS 136 | 137 | def write(self, vals): 138 | celery_params = {param: vals[param] for param in CELERY_PARAMS if param in vals} 139 | 140 | if bool(celery_params): 141 | kwargs = self.kwargs and json.loads(self.kwargs) or {} 142 | if not kwargs.get('celery'): 143 | kwargs['celery'] = {} 144 | kwargs['celery'].update(celery_params) 145 | vals['kwargs'] = kwargs 146 | return super(CeleryTask, self).write(vals) 147 | 148 | def unlink(self): 149 | for task in self: 150 | if task.state in [STATE_STARTED, STATE_RETRY]: 151 | raise UserError(_('You cannot delete a running task.')) 152 | super(CeleryTask, self).unlink() 153 | 154 | @api.model 155 | def check_schedule_needed(self, t_setting): 156 | # returns False if the task can be executed right now (current moment fits the scheduling criteria) 157 | # returns a datetime value (in UTC) for when it can be executed next (current moment does not fit the scheduling criteria) 158 | def get_next_day_of_week_diff(current_day_of_week, allowed_days): 159 | next_allowed_day_of_week = list(filter(lambda day: day > current_day_of_week, allowed_days)) 160 | if next_allowed_day_of_week: 161 | # next day is this week 162 | return next_allowed_day_of_week[0] - current_day_of_week 163 | else: 164 | # next day is next week 165 | return 7 - current_day_of_week + list(filter(lambda day: day < current_day_of_week, allowed_days))[0] 166 | 167 | def get_next_hour_diff(current_hour, hour_from, hour_to, current_day_of_week, allowed_days): 168 | if current_hour <= hour_from: 169 | if not allowed_days or list(filter(lambda day: day == current_day_of_week, allowed_days)): 170 | return hour_from - current_hour 171 | else: 172 | return (hour_from - current_hour) + (get_next_day_of_week_diff(current_day_of_week, allowed_days) * 24) 173 | else: 174 | if not allowed_days: 175 | return (24 - current_hour + hour_from) 176 | else: 177 | return (24 - current_hour + hour_from) + (get_next_day_of_week_diff(current_day_of_week, allowed_days) * 24) 178 | 179 | scheduled_date = False 180 | user_tz = self.env['res.users'].sudo().browse(self.env.uid).tz 181 | tz = pytz.timezone(user_tz) if user_tz else pytz.utc 182 | current_day_of_week = datetime.now(tz=tz).weekday() + 1 # adding 1 to avoid comparisons to 0 183 | current_hour = (datetime.now(tz=tz).hour + (datetime.now(tz=tz).minute / 60.0) + (datetime.now(tz=tz).second / 3600.0)) or 0 184 | allowed_days = list(filter(None, [1 if t_setting.schedule_mondays else False, 185 | 2 if t_setting.schedule_tuesdays else False, 186 | 3 if t_setting.schedule_wednesdays else False, 187 | 4 if t_setting.schedule_thursdays else False, 188 | 5 if t_setting.schedule_fridays else False, 189 | 6 if t_setting.schedule_saturdays else False, 190 | 7 if t_setting.schedule_sundays else False])) 191 | weekday_scheduling_set = any([t_setting.schedule_mondays, t_setting.schedule_tuesdays, 192 | t_setting.schedule_wednesdays, t_setting.schedule_thursdays, 193 | t_setting.schedule_fridays, t_setting.schedule_saturdays, t_setting.schedule_sundays]) 194 | hour_scheduling_set = (t_setting.schedule_hours_from + t_setting.schedule_hours_to) > 0 # hourly schedule is set if hours set are different from 0 195 | out_of_allowed_day_range = (current_day_of_week not in allowed_days) 196 | out_of_allowed_hour_range = (current_hour < t_setting.schedule_hours_from or current_hour > t_setting.schedule_hours_to) 197 | if weekday_scheduling_set and out_of_allowed_day_range: 198 | hour_diff = get_next_hour_diff(0, t_setting.schedule_hours_from, t_setting.schedule_hours_to, current_day_of_week, allowed_days) 199 | t = datetime.now(tz=tz) 200 | scheduled_date = (t - timedelta(seconds=t.second + (t.minute * 60) + (t.hour * 3600)) + timedelta(hours=hour_diff)).astimezone(pytz.utc) 201 | elif weekday_scheduling_set and not out_of_allowed_day_range and hour_scheduling_set and out_of_allowed_hour_range: 202 | hour_diff = get_next_hour_diff(current_hour, t_setting.schedule_hours_from, t_setting.schedule_hours_to, current_day_of_week, allowed_days) 203 | scheduled_date = (datetime.now(tz=tz) + timedelta(hours=hour_diff)).astimezone(pytz.utc) 204 | elif not weekday_scheduling_set and hour_scheduling_set and out_of_allowed_hour_range: 205 | hour_diff = get_next_hour_diff(current_hour, t_setting.schedule_hours_from, t_setting.schedule_hours_to, current_day_of_week, allowed_days) 206 | scheduled_date = (datetime.now(tz=tz) + timedelta(hours=hour_diff)).astimezone(pytz.utc) 207 | return scheduled_date and scheduled_date.replace(tzinfo=None) or False 208 | 209 | @api.model 210 | def call_task(self, model, method, **kwargs): 211 | """ Call Task dispatch to the Celery interface. """ 212 | 213 | user, password, sudo = _get_celery_user_config() 214 | res_users = self.env['res.users'].with_context(active_test=False) 215 | user_id = res_users.search_read([('login', '=', user)], fields=['id'], limit=1) 216 | if not user_id: 217 | msg = _('The user "%s" doesn\'t exist.') % user 218 | logger.error(msg) 219 | return False 220 | 221 | user_id = user_id[0]['id'] 222 | task_uuid = str(uuid.uuid4()) 223 | vals = { 224 | 'uuid': task_uuid, 225 | 'user_id': user_id, 226 | 'model': model, 227 | 'method': method, 228 | # The task (method/implementation) kwargs, needed in the rpc_run_task model/method. 229 | 'kwargs': kwargs} 230 | 231 | scheduled_date = False 232 | # queue selection 233 | default_queue = kwargs.get('celery', False) and kwargs.get('celery').get('queue', '') or 'celery' 234 | task_queue = False 235 | task_setting_domain = [('model', '=', model), ('method', '=', method), ('active', '=', True)] 236 | task_setting = self.env['celery.task.setting'].sudo().search(task_setting_domain, limit=1) 237 | if task_setting: 238 | # check if the task needs to be scheduled in a specified timeframe 239 | if task_setting.schedule: 240 | scheduled_date = self.check_schedule_needed(task_setting) 241 | if scheduled_date: 242 | vals.update({'state': STATE_SCHEDULED, 'scheduled_date': scheduled_date}) 243 | 244 | if task_setting.task_queue_ids: 245 | if task_setting.use_first_empty_queue: 246 | for q in task_setting.task_queue_ids.sorted(key=lambda l: l.sequence): 247 | if q.queue_id.active: 248 | if self.search_count([('queue', '=', q.queue_id.name), ('state', '=', STATE_PENDING)]) <= q.queue_max_pending_tasks: 249 | # use the first queue that satisfies the criteria of N or less pending tasks 250 | task_queue = q.queue_id.name 251 | break 252 | if not task_queue: 253 | # use the first active queue from the task settings 254 | active_queues = task_setting.task_queue_ids.filtered(lambda q: q.queue_id.active) 255 | if active_queues: 256 | task_queue = active_queues[0].queue_id.name 257 | if not task_queue: 258 | # use the default queue specified in code if not defined in task settings 259 | task_queue = default_queue 260 | 261 | if not kwargs.get('celery'): 262 | kwargs['celery'] = {} 263 | kwargs['celery'].update({'queue': task_queue}) 264 | 265 | # Supported apply_async parameters/options shall be stored in the Task model-record. 266 | celery_vals = copy.copy(kwargs.get('celery')) 267 | if celery_vals.get('retry_policy'): 268 | vals.update(celery_vals.get('retry_policy')) 269 | del celery_vals['retry_policy'] 270 | vals.update(celery_vals) 271 | 272 | if kwargs.get('celery_task_vals'): 273 | celery_task_vals = copy.copy(kwargs.get('celery_task_vals')) 274 | vals.update(celery_task_vals) 275 | 276 | # Set transaction strategy and apply call_task 277 | transaction_strategy = self._transaction_strategy(task_setting, kwargs) 278 | logger.debug('call_task - transaction strategy: %s' % transaction_strategy) 279 | dbname = self._cr.dbname 280 | vals['transaction_strategy'] = transaction_strategy 281 | 282 | def apply_call_task(): 283 | # Closure uses several variables from enslosing scope. 284 | db_registry = model_registry.Registry.new(dbname) 285 | call_task = False 286 | with api.Environment.manage(), db_registry.cursor() as cr: 287 | env = api.Environment(cr, user_id, {}) 288 | Task = env['celery.task'] 289 | try: 290 | task = Task.create(vals) 291 | call_task = True 292 | except CeleryCallTaskException as e: 293 | logger.error(_('ERROR FROM call_task %s: %s') % (task_uuid, e)) 294 | cr.rollback() 295 | call_task = False 296 | except Exception as e: 297 | logger.error(_('UNKNOWN ERROR FROM call_task: %s') % (e)) 298 | cr.rollback() 299 | call_task = False 300 | 301 | if call_task: 302 | with api.Environment.manage(), db_registry.cursor() as cr: 303 | env = api.Environment(cr, user_id, {}) 304 | Task = env['celery.task'] 305 | if not scheduled_date: # if the task is not scheduled for a later time 306 | Task._celery_call_task(user_id, task_uuid, model, method, kwargs) 307 | 308 | if transaction_strategy == 'immediate': 309 | apply_call_task() 310 | else: 311 | self._cr.after('commit', apply_call_task) 312 | 313 | def _transaction_strategies(self): 314 | transaction_strategies = self.env['celery.task.setting']._fields['transaction_strategy'].selection 315 | # return values except 'api'. 316 | return [r[0] for r in transaction_strategies if r[0] != 'api'] 317 | 318 | def _transaction_strategy(self, task_setting, kwargs): 319 | """ 320 | When the task shall apply (ORM create and send to Celery MQ). 321 | Possible options (return values): 322 | 323 | after_commit 324 | ------------ 325 | The call_task shall apply after the main/caller transaction 326 | has been committed. This avoids undesired side effects due to 327 | the current/main transaction isn't committed yet. Especially 328 | if you need to ensure data from the main transaction has been 329 | committed to use in the task. 330 | 331 | immediate 332 | --------- 333 | The call_task shall apply immediately from the main/caller 334 | transaction, even if it ain't committed yet. Use wisely and 335 | ensure idempotency of the task. Because the main/caller 336 | transaction could fail and encounter a rollback, while the 337 | task shall still be send to the Celery MQ. 338 | """ 339 | 340 | transaction_strategies = self._transaction_strategies() 341 | api_transaction_strategy = kwargs.get('transaction_strategy') 342 | 343 | if task_setting and task_setting.transaction_strategy == 'api' \ 344 | and api_transaction_strategy in transaction_strategies: 345 | transaction_strategy = api_transaction_strategy 346 | elif not task_setting and api_transaction_strategy in transaction_strategies: 347 | transaction_strategy = api_transaction_strategy 348 | elif task_setting and task_setting.transaction_strategy != 'api': 349 | transaction_strategy = task_setting.transaction_strategy 350 | else: 351 | transaction_strategy = 'after_commit' 352 | 353 | # Extra save guard, in case the value is unknown. 354 | if transaction_strategy not in transaction_strategies: 355 | transaction_strategy = 'after_commit' 356 | 357 | return transaction_strategy 358 | 359 | @api.model 360 | def _celery_call_task(self, user_id, uuid, model, method, kwargs): 361 | user, password, sudo = _get_celery_user_config() 362 | url = self.env['ir.config_parameter'].sudo().get_param('celery.celery_base_url') 363 | _args = [url, self._cr.dbname, user_id, uuid, model, method] 364 | 365 | if not kwargs.get('_password'): 366 | kwargs['_password'] = password 367 | 368 | _kwargsrepr = copy.deepcopy(kwargs) 369 | _kwargsrepr['_password'] = '*****' 370 | _kwargsrepr = repr(_kwargsrepr) 371 | 372 | # TODO DEPRECATED compatibility to remove after v12 373 | celery_kwargs = kwargs.get('celery') 374 | if not celery_kwargs: 375 | kwargs['celery'] = {} 376 | elif celery_kwargs.get('retry') and not celery_kwargs.get('retry_policy'): 377 | # The retry_policy defines the retry of the Broker Connection by Celery. 378 | retry_policy = {} 379 | if celery_kwargs.get('max_retries'): 380 | retry_policy['max_retries'] = celery_kwargs.get('max_retries') 381 | if celery_kwargs.get('interval_start'): 382 | retry_policy['interval_start'] = celery_kwargs.get('interval_start') 383 | if celery_kwargs.get('interval_step'): 384 | retry_policy['interval_step'] = celery_kwargs.get('interval_step') 385 | kwargs['celery']['retry_policy'] = retry_policy 386 | 387 | call_task.apply_async(args=_args, kwargs=kwargs, kwargsrepr=_kwargsrepr, **kwargs['celery']) 388 | 389 | @api.model 390 | def rpc_run_task(self, task_uuid, model, method, *args, **kwargs): 391 | """Run/execute the task, which is a model method. 392 | 393 | The idea is that Celery calls this by Odoo its external API, 394 | whereas XML-RPC or a HTTP-controller. 395 | 396 | The model-method can either be called as user: 397 | - The "celery" (Odoo user) defined in the odoo.conf. This is the default, in case 398 | the "sudo" setting isn't configured in the odoo.conf. 399 | - "admin" (Odoo admin user), to circumvent model-access configuration for models 400 | which run/process task. Therefor add "sudo = True" in the odoo.conf (see: example.odoo.conf). 401 | """ 402 | 403 | task_queue = kwargs.get('celery', False) and kwargs.get('celery').get('queue', '') or 'celery' 404 | task_ref = kwargs.get('celery_task_vals', False) and kwargs.get('celery_task_vals').get('ref', '') or '' 405 | logger.info('CELERY rpc_run_task uuid:{uuid} - model: {model} - method: {method} - ref: {ref} - queue: {queue}'.format( 406 | uuid=task_uuid, 407 | model=model, 408 | method=method, 409 | ref=task_ref, 410 | queue=task_queue)) 411 | 412 | exist = self.search_count([('uuid', '=', task_uuid)]) 413 | if exist == 0: 414 | msg = "Task doesn't exist (anymore). Task-UUID: %s" % task_uuid 415 | logger.error(msg) 416 | return (TASK_NOT_FOUND, msg) 417 | 418 | model_obj = self.env[model] 419 | task = self.search([('uuid', '=', task_uuid), ('state', 'in', [STATE_PENDING, STATE_RETRY, STATE_SCHEDULED])], limit=1) 420 | 421 | if not task: 422 | return ('OK', 'Task already processed') 423 | 424 | if task.retry and task.state == STATE_RETRY: 425 | vals = {'state': STATE_RETRYING, 'state_date': fields.Datetime.now()} 426 | else: 427 | vals = {'state': STATE_STARTED, 'started_date': fields.Datetime.now()} 428 | 429 | # Store state before execution. 430 | with registry(self._cr.dbname).cursor() as result_cr: 431 | env = api.Environment(result_cr, self._uid, {}) 432 | task.with_env(env).write(vals) 433 | 434 | user, password, sudo = _get_celery_user_config() 435 | 436 | # TODO 437 | # Re-raise Exception if not called by XML-RPC, but directly from model/Odoo. 438 | # This supports unit-tests and scripting purposes. 439 | result = False 440 | response = False 441 | with registry(self._cr.dbname).cursor() as cr: 442 | # Transaction/cursror for the exception handler. 443 | env = api.Environment(cr, self._uid, {}) 444 | try: 445 | if bool(sudo) and sudo: 446 | res = getattr(model_obj.with_env(env).sudo(), method)(task_uuid, **kwargs) 447 | else: 448 | res = getattr(model_obj.with_env(env), method)(task_uuid, **kwargs) 449 | 450 | if res != False and not bool(res): 451 | msg = "No result/return value for Task UUID: %s. Ensure the task-method returns a value." % task_uuid 452 | logger.error(msg) 453 | raise CeleryTaskNoResultError(msg) 454 | 455 | if isinstance(res, dict): 456 | result = res.get('result', True) 457 | if res.get('res_model'): 458 | vals['res_model'] = res.get('res_model') 459 | if res.get('res_ids'): 460 | vals['res_ids'] = res.get('res_ids') 461 | else: 462 | result = res 463 | vals.update({'state': STATE_SUCCESS, 'state_date': fields.Datetime.now(), 'result': result, 'exc_info': False}) 464 | except Exception as e: 465 | """ The Exception-handler does a rollback. So we need a new 466 | transaction/cursor to store data about RETRY and exception info/traceback. """ 467 | 468 | exc_info = traceback.format_exc() 469 | if task.retry: 470 | state = STATE_RETRY 471 | logger.warning('Retry... exception (see task form) from rpc_run_task {uuid} (model: {model} - method: {method} - ref: {ref} - queue: {queue}): {exc}.'.format( 472 | uuid=task_uuid, 473 | model=model, 474 | method=method, 475 | ref=task_ref, 476 | queue=task_queue, 477 | exc=e)) 478 | else: 479 | state = STATE_FAILURE 480 | logger.warning('Failure... exception (see task form) from rpc_run_task {uuid} (model: {model} - method: {method} - ref: {ref} - queue: {queue}): {exc}.'.format( 481 | uuid=task_uuid, 482 | model=model, 483 | method=method, 484 | ref=task_ref, 485 | queue=task_queue, 486 | exc=e)) 487 | vals.update({'state': state, 'state_date': fields.Datetime.now(), 'exc_info': exc_info}) 488 | logger.debug('Exception rpc_run_task: {exc_info}'.format(uuid=task_uuid, exc_info=exc_info)) 489 | cr.rollback() 490 | finally: 491 | with registry(self._cr.dbname).cursor() as result_cr: 492 | env = api.Environment(result_cr, self._uid, {}) 493 | task.with_env(env).write(vals) 494 | response = (vals.get('state'), result) 495 | return response 496 | 497 | @api.model 498 | def rpc_set_state(self, task_uuid, state): 499 | """Set state of task, which is a model method. 500 | 501 | The idea is that Celery calls this by Odoo its external API, 502 | whereas XML-RPC or a HTTP-controller. 503 | """ 504 | # TODO DRY: Also in rpc_run_task. 505 | # Move into separate method. 506 | exist = self.search_count([('uuid', '=', task_uuid)]) 507 | if exist == 0: 508 | msg = "Task doesn't exist (anymore). Task-UUID: %s" % task_uuid 509 | logger.error(msg) 510 | return (TASK_NOT_FOUND, msg) 511 | 512 | task = self.search([('uuid', '=', task_uuid), ('state', '!=', state)], limit=1) 513 | if task: 514 | task.state = state 515 | msg = 'Update task state to: {state}'.format(state=state) 516 | return ('OK', msg) 517 | else: 518 | msg = 'Task already in state {state}.'.format(state=state) 519 | return ('OK', msg) 520 | 521 | def action_pending(self): 522 | for task in self: 523 | vals = { 524 | 'state': STATE_PENDING, 525 | 'started_date': None, 526 | 'state_date': None, 527 | 'result': None, 528 | 'exc_info': None 529 | } 530 | if task.stuck: 531 | vals['stuck'] = False 532 | task.write(vals) 533 | 534 | def _states_to_requeue(self): 535 | return STATES_TO_REQUEUE 536 | 537 | def action_requeue(self): 538 | user, password, sudo = _get_celery_user_config() 539 | res_users = self.env['res.users'].with_context(active_test=False) 540 | user_id = res_users.search_read([('login', '=', user)], fields=['id'], limit=1) 541 | 542 | if not user_id: 543 | raise UserError('No user found with login: {login}'.format(login=user)) 544 | user_id = user_id[0]['id'] 545 | 546 | states_to_requeue = self._states_to_requeue() 547 | 548 | for task in self: 549 | if task.stuck or task.state in states_to_requeue: 550 | task.action_pending() 551 | try: 552 | _kwargs = json.loads(task.kwargs) 553 | self._celery_call_task(task.user_id.id, task.uuid, task.model, task.method, _kwargs) 554 | except CeleryCallTaskException as e: 555 | logger.error(_('ERROR IN requeue %s: %s') % (task.uuid, e)) 556 | return True 557 | 558 | def _states_to_cancel(self): 559 | return STATES_TO_CANCEL 560 | 561 | def action_cancel(self): 562 | user, password, sudo = _get_celery_user_config() 563 | res_users = self.env['res.users'].with_context(active_test=False) 564 | user_id = res_users.search_read([('login', '=', user)], fields=['id'], limit=1) 565 | 566 | if not user_id: 567 | raise UserError('No user found with login: {login}'.format(login=user)) 568 | user_id = user_id[0]['id'] 569 | 570 | states_to_cancel = self._states_to_cancel() 571 | 572 | for task in self: 573 | if task.stuck or task.state in states_to_cancel: 574 | vals = {'state': STATE_CANCEL, 'state_date': fields.Datetime.now()} 575 | if task.stuck: 576 | vals['stuck'] = False 577 | task.write(vals) 578 | return True 579 | 580 | def action_stuck(self): 581 | user, password, sudo = _get_celery_user_config() 582 | res_users = self.env['res.users'].with_context(active_test=False) 583 | user_id = res_users.search_read([('login', '=', user)], fields=['id'], limit=1) 584 | 585 | if not user_id: 586 | raise UserError('No user found with login: {login}'.format(login=user)) 587 | user_id = user_id[0]['id'] 588 | 589 | for task in self: 590 | if task.state in STATES_TO_STUCK: 591 | task.write({ 592 | 'stuck': True, 593 | 'state_date': fields.Datetime.now() 594 | }) 595 | return True 596 | 597 | @api.model 598 | def cron_handle_stuck_tasks(self): 599 | StuckTaskReport = self.env['celery.stuck.task.report'] 600 | domain = [('stuck', '=', True)] 601 | tasks = StuckTaskReport.search(domain) 602 | for t in tasks: 603 | if t.handle_stuck and t.handle_stuck_by_cron: 604 | t.task_id.action_stuck() 605 | 606 | @api.model 607 | def cron_handle_scheduled_tasks(self): 608 | domain = [('state', '=', STATE_SCHEDULED), ('scheduled_date', '<=', fields.Datetime.now())] 609 | tasks = self.search(domain) 610 | for t in tasks: 611 | t.action_requeue() 612 | 613 | @api.model 614 | def autovacuum(self, **kwargs): 615 | # specify batch_size for high loaded systems 616 | batch_size = kwargs.get('batch_size', 100) 617 | days = kwargs.get('days', 90) 618 | hours = kwargs.get('hours', 0) 619 | minutes = kwargs.get('minutes', 0) 620 | seconds = kwargs.get('seconds', 0) 621 | 622 | success = kwargs.get('success', True) 623 | failure = kwargs.get('failure', True) 624 | cancel = kwargs.get('cancel', True) 625 | 626 | from_date = datetime.now() - timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) 627 | states = [STATE_SUCCESS, STATE_FAILURE, STATE_CANCEL] 628 | if not success: 629 | states.remove(STATE_SUCCESS) 630 | if not failure: 631 | states.remove(STATE_FAILURE) 632 | if not cancel: 633 | states.remove(STATE_CANCEL) 634 | 635 | # state_date: because tasks could be created a while ago, but 636 | # finished much later. 637 | domain = [ 638 | ('state_date', '!=', False), # extra safety check. 639 | ('state_date', '<=', from_date), 640 | ('state', 'in', states) 641 | ] 642 | 643 | # Remove tasks in a loop with batch_size step 644 | while True: 645 | tasks = self.search(domain, limit=batch_size) 646 | task_count = len(tasks) 647 | if not tasks: 648 | break 649 | else: 650 | tasks.unlink() 651 | logger.info('Celery autovacuum %s tasks', task_count) 652 | # Commit current step not to rollback the entire transation 653 | self.env.cr.commit() 654 | return True 655 | 656 | def action_open_related_record(self): 657 | """ Open a view with the record(s) of the task. If it's one record, 658 | it opens a form-view. If it concerns mutltiple records, it opens 659 | a tree view. 660 | """ 661 | 662 | self.ensure_one() 663 | model_name = self.res_model 664 | 665 | if not self.res_ids: 666 | raise UserError(_('Empty field res_ids')) 667 | res_ids = self.res_ids and json.loads(self.res_ids) 668 | 669 | records = self.env[model_name].browse(res_ids) 670 | if not records: 671 | return None 672 | action = { 673 | 'name': _('Related Record'), 674 | 'type': 'ir.actions.act_window', 675 | 'view_type': 'form', 676 | 'view_mode': 'form', 677 | 'res_model': records._name, 678 | } 679 | if len(records) == 1: 680 | action['res_id'] = records.id 681 | else: 682 | action.update({ 683 | 'name': _('Related Records'), 684 | 'view_mode': 'tree,form', 685 | 'domain': [('id', 'in', records.ids)], 686 | }) 687 | return action 688 | 689 | def refresh_view(self): 690 | return True 691 | 692 | 693 | class CeleryCallTaskException(Exception): 694 | """ CeleryCallTaskException """ 695 | 696 | 697 | class CeleryTaskNoResultError(Exception): 698 | """ CeleryCallTaskException """ 699 | -------------------------------------------------------------------------------- /celery/models/celery_task_setting.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models, _ 5 | from odoo.exceptions import ValidationError 6 | 7 | 8 | class CeleryTaskSetting(models.Model): 9 | _name = 'celery.task.setting' 10 | _description = 'Celery Task Setting' 11 | _inherit = ['mail.thread'] 12 | _order = 'name' 13 | 14 | name = fields.Char('Name', compute='_compute_name', store=True) 15 | model = fields.Char(string='Model', required=True, tracking=True) 16 | method = fields.Char(string='Method', required=True, tracking=True) 17 | handle_stuck = fields.Boolean(string="Handle Stuck", tracking=True) 18 | stuck_after_seconds = fields.Integer( 19 | string='Seems Stuck after seconds', tracking=True, 20 | help="A task seems Stuck when it's still in state STARTED or RETRY, after certain elapsed seconds.") 21 | handle_stuck_by_cron = fields.Boolean( 22 | string='Handle Stuck by Cron', default=False, tracking=True, 23 | help='Cron shall update Tasks which seems Stuck.') 24 | task_queue_ids = fields.One2many( 25 | string="Queues", 26 | comodel_name="celery.task.setting.queue", 27 | inverse_name="task_setting_id", 28 | help="Queues used for this type of a task.", 29 | ) 30 | use_first_empty_queue = fields.Boolean(string="Use the first queue with N or less pending tasks", default=True) 31 | active = fields.Boolean(string='Active', default=True, tracking=True) 32 | 33 | schedule = fields.Boolean(string="Schedule?", default=False) 34 | schedule_mondays = fields.Boolean(string="Schedule on Mondays", default=False) 35 | schedule_tuesdays = fields.Boolean(string="Schedule on Tuesdays", default=False) 36 | schedule_wednesdays = fields.Boolean(string="Schedule on Wednesdays", default=False) 37 | schedule_thursdays = fields.Boolean(string="Schedule on Thursdays", default=False) 38 | schedule_fridays = fields.Boolean(string="Schedule on Fridays", default=False) 39 | schedule_saturdays = fields.Boolean(string="Schedule on Saturdays", default=False) 40 | schedule_sundays = fields.Boolean(string="Schedule on Sundays", default=False) 41 | schedule_hours_from = fields.Float(string='Schedule hours from', default=0.0) 42 | schedule_hours_to = fields.Float(string='Schedule hours to', default=0.0) 43 | 44 | transaction_strategy = fields.Selection( 45 | [('after_commit', 'After commit'), ('immediate', 'Immediate'), ('api', 'API')], 46 | string="Transaction Strategy", required=True, default='after_commit', tracking=True, 47 | help="""Specifies when the task shall apply (ORM create and send to Celery MQ): 48 | - After commit: Apply after commit of the main/caller transaction (default setting). 49 | - Immediate: Apply immediately from the main/caller transaction, even if it ain't committed yet. 50 | - API: Programmatically set in the call_task() method kwargs. Also used and applied if no Task Setting record exists.""" 51 | ) 52 | 53 | @api.constrains('model', 'method') 54 | def _check_model_method_unique(self): 55 | count = self.search_count([('model', '=', self.model), ('method', '=', self.method)]) 56 | if count > 1: 57 | raise ValidationError(_('Combination of Model and Method already exists!')) 58 | 59 | @api.constrains('schedule_hours_from', 'schedule_hours_to') 60 | def _check_hour_range(self): 61 | if self.schedule_hours_from > self.schedule_hours_to: 62 | raise ValidationError(_('Only same-day hourly range is allowed (00-24)!')) 63 | if (self.schedule_hours_from < 0 or self.schedule_hours_from > 24) or (self.schedule_hours_to < 0 or self.schedule_hours_to > 24): 64 | raise ValidationError(_('00-24 only!')) 65 | 66 | @api.depends('model', 'method') 67 | def _compute_name(self): 68 | for r in self: 69 | r.name = '{model} - {method}'.format(model=r.model, method=r.method) 70 | 71 | 72 | class CeleryTaskSettingQueue(models.Model): 73 | _name = 'celery.task.setting.queue' 74 | _description = 'Celery Task Queues' 75 | _order = 'sequence' 76 | 77 | def _default_sequence(self): 78 | rec = self.search([('task_setting_id', '=', self.task_setting_id.id)], limit=1, order="sequence DESC") 79 | return rec.sequence or 1 80 | 81 | task_setting_id = fields.Many2one( 82 | string="Task", 83 | comodel_name="celery.task.setting", 84 | ondelete="cascade", 85 | required=True 86 | ) 87 | queue_id = fields.Many2one( 88 | string="Queue", 89 | comodel_name="celery.queue", 90 | ondelete="cascade", 91 | required=True 92 | ) 93 | active = fields.Boolean(string="Queue Active", related='queue_id.active') 94 | sequence = fields.Integer(string="Sequence", required=True, default=_default_sequence) 95 | queue_max_pending_tasks = fields.Integer(string="Use if less then (pending tasks)", default=0, required=True) 96 | -------------------------------------------------------------------------------- /celery/models/res_config_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models 5 | 6 | LOCALHOST = 'http://127.0.0.1:8069' 7 | 8 | 9 | class ResConfigSettings(models.TransientModel): 10 | _inherit = 'res.config.settings' 11 | 12 | celery_base_url = fields.Char('Celery Base URL') 13 | 14 | @api.model 15 | def get_values(self): 16 | res = super(ResConfigSettings, self).get_values() 17 | Param = self.env['ir.config_parameter'].sudo() 18 | res.update( 19 | celery_base_url=Param.get_param('celery.celery_base_url', default=LOCALHOST) 20 | ) 21 | return res 22 | 23 | def set_values(self): 24 | super(ResConfigSettings, self).set_values() 25 | self.env['ir.config_parameter'].sudo().set_param( 26 | "celery.celery_base_url", 27 | self.celery_base_url) 28 | -------------------------------------------------------------------------------- /celery/models/res_users.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import contextlib 5 | 6 | from odoo import api, models, tools 7 | from odoo.exceptions import AccessDenied 8 | from odoo.addons.celery.models.celery_task import _get_celery_user_config 9 | 10 | 11 | class Users(models.Model): 12 | _inherit = "res.users" 13 | 14 | @classmethod 15 | @tools.ormcache('uid', 'passwd') 16 | def check(cls, db, uid, passwd): 17 | """Verifies that the given (uid, password) is authorized for the database ``db`` and 18 | raise an exception if it is not.""" 19 | 20 | # XXX (celery) copy from orignal with overrides 21 | if not passwd: 22 | # empty passwords disallowed for obvious security reasons 23 | raise AccessDenied() 24 | 25 | with contextlib.closing(cls.pool.cursor()) as cr: 26 | self = api.Environment(cr, uid, {})[cls._name] 27 | celery_user, celery_password, celery_sudo = _get_celery_user_config() 28 | if self.env.user.login != celery_user: 29 | # XXX (celery) if not celery user 30 | super(Users, cls).check(db, uid, passwd) 31 | with self._assert_can_auth(): 32 | # XXX (celery) Altered 33 | # if not self.env.user.active: 34 | # raise AccessDenied()- 35 | self._check_credentials(passwd, {'interactive': False}) 36 | -------------------------------------------------------------------------------- /celery/odoo.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import copy 5 | import os 6 | 7 | from celery import Celery 8 | from celery.contrib import rdb 9 | from celery.exceptions import TaskError, Retry, MaxRetriesExceededError 10 | from celery.utils.log import get_task_logger 11 | from xmlrpc import client as xmlrpc_client 12 | 13 | logger = get_task_logger(__name__) 14 | 15 | TASK_DEFAULT_QUEUE = 'celery' 16 | 17 | OK_CODE = 'OK' 18 | 19 | # STATES (checks) should match with [celery.task] model! 20 | STATE_RETRY = 'RETRY' 21 | STATE_FAILURE = 'FAILURE' 22 | TASK_NOT_FOUND = 'NOT_FOUND' 23 | 24 | RETRY_COUNTDOWN_ADD_SECONDS = 'ADD_SECS' 25 | RETRY_COUNTDOWN_MULTIPLY_RETRIES = 'MUL_RETRIES' 26 | RETRY_COUNTDOWN_MULTIPLY_RETRIES_SECCONDS = 'MUL_RETRIES_SECS' 27 | 28 | 29 | class TaskNotFoundInOdoo(TaskError): 30 | """The task doesn't exist (anymore) in Odoo (Celery Task model).""" 31 | 32 | class RunTaskFailure(TaskError): 33 | """Error from rpc_run_task in Odoo.""" 34 | 35 | 36 | app = Celery('odoo.addons.celery', 37 | broker_url=os.environ.get('ODOO_CELERY_BROKER', False), 38 | broker_heartbeat=os.environ.get('ODOO_CELERY_BROKER_HEARTBEAT', False), 39 | worker_prefetch_multiplier=os.environ.get('ODOO_CELERY_WORKER_PREFETCH_MULTIPLIER', False)) 40 | 41 | @app.task(name='odoo.addons.celery.odoo.call_task', bind=True) 42 | def call_task(self, url, db, user_id, task_uuid, model, method, **kwargs): 43 | odoo = xmlrpc_client.ServerProxy('{}/xmlrpc/2/object'.format(url)) 44 | args = [task_uuid, model, method] 45 | _kwargs = copy.deepcopy(kwargs) 46 | 47 | # Needed in the retry (call), to hide _password. 48 | _kwargsrepr = copy.deepcopy(kwargs) 49 | 50 | password = _kwargs.get('_password') 51 | del _kwargs['_password'] 52 | celery_params = _kwargs.get('celery', {}) 53 | 54 | logger.info('{model} {method} - celery.task uuid: {uuid}'.format( 55 | model=model, method=method, uuid=task_uuid)) 56 | logger.info('{model} {method} - kwargs: {kwargs}'.format( 57 | model=model, method=method, kwargs=_kwargs)) 58 | 59 | try: 60 | logger.info( 61 | 'XML-RPC to Odoo server:\n\n' 62 | '- url: {url}\n' 63 | '- db: {db}\n' 64 | '- user_id: {user_id}\n' 65 | '- task_uuid: {task_uuid}\n' 66 | '- model: celery.task\n' 67 | '- method: rpc_run_task\n' 68 | '- args: {args}\n' 69 | '- kwargs {kwargs}\n'.format( 70 | url=url, db=db, user_id=user_id, task_uuid=task_uuid, model=model, method=method, args=args, kwargs=_kwargs)) 71 | response = odoo.execute_kw(db, user_id, password, 'celery.task', 'rpc_run_task', args, _kwargs) 72 | 73 | if (isinstance(response, tuple) or isinstance(response, list)) and len(response) == 2: 74 | code = response[0] 75 | result = response[1] 76 | else: 77 | code = OK_CODE 78 | result = response 79 | 80 | if code == TASK_NOT_FOUND: 81 | msg = "%s, database: %s" % (result, db) 82 | raise TaskNotFoundInOdoo(msg) 83 | elif code in (STATE_RETRY, STATE_FAILURE): 84 | retry = celery_params.get('retry') 85 | countdown = celery_params.get('countdown', 1) 86 | retry_countdown_setting = celery_params.get('retry_countdown_setting') 87 | retry_countdown_add_seconds = celery_params.get('retry_countdown_add_seconds', 0) 88 | retry_countdown_multiply_retries_seconds = celery_params.get('retry_countdown_multiply_retries_seconds', 0) 89 | 90 | # (Optionally) increase the countdown either by: 91 | # - add seconds 92 | # - countdown * retry requests 93 | # - retry requests * a given seconds 94 | if retry and retry_countdown_setting: 95 | if retry_countdown_setting == RETRY_COUNTDOWN_ADD_SECONDS: 96 | countdown = countdown + retry_countdown_add_seconds 97 | elif retry_countdown_setting == RETRY_COUNTDOWN_MULTIPLY_RETRIES: 98 | countdown = countdown * self.request.retries 99 | elif retry_countdown_setting == RETRY_COUNTDOWN_MULTIPLY_RETRIES_SECCONDS \ 100 | and retry_countdown_multiply_retries_seconds > 0: 101 | countdown = self.request.retries * retry_countdown_multiply_retries_seconds 102 | celery_params['countdown'] = countdown 103 | 104 | if retry: 105 | msg = 'Retry task... Failure in Odoo {db} (task: {uuid}, model: {model}, method: {method}).'.format( 106 | db=db, uuid=task_uuid, model=model, method=method) 107 | logger.info(msg) 108 | 109 | # Notify the worker to retry. 110 | logger.info('{task_name} retry params: {params}'.format(task_name=self.name, params=celery_params)) 111 | _kwargsrepr['_password'] = '*****' 112 | _kwargsrepr = repr(_kwargsrepr) 113 | raise self.retry(kwargsrepr=_kwargsrepr, **celery_params) 114 | else: 115 | msg = 'Exit task... Failure in Odoo {db} (task: {uuid}, model: {model}, method: {method})\n'\ 116 | ' => Check task log/info in Odoo'.format(db=db, uuid=task_uuid, model=model, method=method) 117 | logger.info(msg) 118 | else: 119 | return (code, result) 120 | except Exception as e: 121 | """ A rather picky workaround to ignore/silence following exceptions. 122 | Only logs in case of other Exceptions. 123 | 124 | This also prevents concurrent retries causing troubles like 125 | concurrent DB updates (shall rollback) etc. 126 | 127 | - xmlrpc_client.Fault: Catches exception TypeError("cannot 128 | marshal None unless allow_none is enabled"). Setting 129 | allowd_none on the ServcerProxy won't work like expected and 130 | seems vague. 131 | - Retry: Celery exception notified to tell worker the task has 132 | been re-sent for retry. We don't want to re-retry (double 133 | trouble here). 134 | 135 | See also odoo/service/wsgi_server.py for xmlrpc.client.Fault 136 | (codes), e.g: RPC_FAULT_CODE_CLIENT_ERROR = 1 137 | """ 138 | if isinstance(e, MaxRetriesExceededError): 139 | # TODO 140 | # After implementation of "Hide sensitive data (password) by argspec/kwargspec, a re-raise should happen. 141 | # For now it shows sensitive data in the logs. 142 | msg = '[TODO] Failure (caught) MaxRetriesExceededError: db: {db}, task: {uuid}, model: {model}, method: {method}.'.format( 143 | db=db, uuid=task_uuid, model=model, method=method) 144 | logger.error(msg) 145 | # Task is probably in state RETRY. Now set it to FAILURE. 146 | args = [task_uuid, 'FAILURE'] 147 | odoo.execute_kw(db, user_id, password, 'celery.task', 'rpc_set_state', args) 148 | elif not isinstance(e, Retry): 149 | # Maybe there's a also a way the store a xmlrpc.client.Fault into the Odoo exc_info field e.g.: 150 | # args = [xmlrpc_client.Fault.faultCode, xmlrpc_client.Fault.faultString] 151 | # odoo.execute_kw(db, user_id, password, 'celery.task', 'rpc_set_exception', args) 152 | # 153 | # Necessary to implement/call a retry() for other exceptions ? 154 | msg = '{exception}\n'\ 155 | ' => SUGGESTIONS: Check former XML-RPC log messages.\n'.format(exception=e) 156 | logger.error(msg) 157 | raise e 158 | -------------------------------------------------------------------------------- /celery/report/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import celery_stuck_task_report 5 | -------------------------------------------------------------------------------- /celery/report/celery_stuck_task_report.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import logging 5 | 6 | from odoo import tools 7 | from odoo import api, fields, models, tools, _ 8 | 9 | _logger = logging.getLogger(__name__) 10 | 11 | 12 | class CeleryStuckTaskReport(models.Model): 13 | _name = 'celery.stuck.task.report' 14 | _description = 'Stuck Tasks Report' 15 | _auto = False 16 | _rec_name = 'uuid' 17 | _order = 'task_id DESC' 18 | 19 | def _selection_states(self): 20 | return self.env['celery.task']._selection_states() 21 | 22 | task_id = fields.Many2one('celery.task', string='Celery Task', readonly=True) 23 | uuid = fields.Char(string='UUID', readonly=True) 24 | model = fields.Char(string='Model', readonly=True) 25 | method = fields.Char(string='Method', readonly=True) 26 | ref = fields.Char(string='Reference', readonly=True) 27 | queue = fields.Char(string='Queue', readonly=True) 28 | state = fields.Selection(selection='_selection_states', readonly=True) 29 | started_date = fields.Datetime(string='Start Time', readonly=True) 30 | state_date = fields.Datetime(string='State Time', readonly=True) 31 | started_age_seconds = fields.Float(string='Started Age Seconds', readonly=True) 32 | state_age_seconds = fields.Float(string='State Age seconds', readonly=True) 33 | started_age_minutes = fields.Float(string='Started Age Minutes', readonly=True) 34 | state_age_minutes = fields.Float(string='State Age Minutes', readonly=True) 35 | started_age_hours = fields.Float(string='Started Age Hours', readonly=True) 36 | state_age_hours = fields.Float(string='State Age Hours', readonly=True) 37 | stuck = fields.Boolean(string='Seems Stuck', readonly=True) 38 | handle_stuck = fields.Boolean(string='Handle Stuck', readonly=True) 39 | handle_stuck_by_cron = fields.Boolean(string='Handle Stuck by Cron', readonly=True) 40 | 41 | def _query(self): 42 | query_str = """ 43 | WITH tasks AS ( 44 | SELECT 45 | t.id AS id, 46 | t.uuid AS uuid, 47 | t.model AS model, 48 | t.method AS method, 49 | t.ref AS ref, 50 | t.queue AS queue, 51 | t.state AS state, 52 | t.started_date AS started_date, 53 | t.state_date AS state_date, 54 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.started_date) AS started_age_seconds, 55 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.state_date) AS state_age_seconds, 56 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.started_date)/60 AS started_age_minutes, 57 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.state_date)/60 AS state_age_minutes, 58 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.started_date)/3600 AS started_age_hours, 59 | EXTRACT(EPOCH FROM timezone('UTC', now()) - t.state_date)/3600 AS state_age_hours, 60 | ts.handle_stuck AS handle_stuck, 61 | ts.stuck_after_seconds AS stuck_after_seconds, 62 | ts.handle_stuck_by_cron AS handle_stuck_by_cron 63 | FROM 64 | celery_task AS t 65 | LEFT JOIN celery_task_setting ts ON ts.model = t.model AND ts.method = t.method 66 | WHERE 67 | ts.active = True 68 | ), 69 | tasks_stuck AS ( 70 | SELECT 71 | t.id AS id, 72 | t.id AS task_id, 73 | t.uuid AS uuid, 74 | t.model AS model, 75 | t.method AS method, 76 | t.ref AS ref, 77 | t.queue AS queue, 78 | t.state AS state, 79 | t.started_date AS started_date, 80 | t.state_date AS state_date, 81 | t.started_age_seconds AS started_age_seconds, 82 | t.state_age_seconds AS state_age_seconds, 83 | t.started_age_minutes AS started_age_minutes, 84 | t.state_age_minutes AS state_age_minutes, 85 | t.started_age_hours AS started_age_hours, 86 | t.state_age_hours AS state_age_hours, 87 | t.handle_stuck AS handle_stuck, 88 | (CASE 89 | WHEN t.state = 'STARTED' AND (t.handle_stuck AND t.stuck_after_seconds > 0) THEN t.started_age_seconds > t.stuck_after_seconds 90 | WHEN t.state = 'RETRY' AND (t.handle_stuck AND t.stuck_after_seconds > 0) THEN t.state_age_seconds > t.stuck_after_seconds 91 | WHEN t.state = 'RETRYING' AND (t.handle_stuck AND t.stuck_after_seconds > 0) THEN t.state_age_seconds > t.stuck_after_seconds 92 | ELSE False 93 | END) AS stuck, 94 | t.handle_stuck_by_cron AS handle_stuck_by_cron 95 | FROM 96 | tasks AS t 97 | ) 98 | SELECT 99 | t.id AS id, 100 | t.id AS task_id, 101 | t.uuid AS uuid, 102 | t.model AS model, 103 | t.method AS method, 104 | t.ref AS ref, 105 | t.queue AS queue, 106 | t.state AS state, 107 | t.started_date AS started_date, 108 | t.state_date AS state_date, 109 | t.started_age_seconds AS started_age_seconds, 110 | t.state_age_seconds AS state_age_seconds, 111 | t.started_age_minutes AS started_age_minutes, 112 | t.state_age_minutes AS state_age_minutes, 113 | t.started_age_hours AS started_age_hours, 114 | t.state_age_hours AS state_age_hours, 115 | t.stuck AS stuck, 116 | t.handle_stuck AS handle_stuck, 117 | t.handle_stuck_by_cron AS handle_stuck_by_cron 118 | FROM 119 | tasks_stuck AS t 120 | WHERE 121 | t.stuck = True 122 | """ 123 | return query_str 124 | 125 | def init(self): 126 | try: 127 | tools.drop_view_if_exists(self.env.cr, self._table) 128 | self.env.cr.execute("""CREATE or REPLACE VIEW %s as (%s)""" % (self._table, self._query())) 129 | except ValueError as e: 130 | msg = 'UPDATE the "celery" module. Required an initial data-import. Caught Exception: {exc}'.format(exc=e) 131 | _logger.critical(msg) 132 | -------------------------------------------------------------------------------- /celery/report/celery_stuck_task_report_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | celery.stuck.task.report.tree 8 | celery.stuck.task.report 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Celery Stuck Tasks 31 | celery.stuck.task.report 32 | tree,form 33 | 34 | 35 | 36 | celery.stuck.task.report.search 37 | celery.stuck.task.report 38 | search 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Flag/update task: Stuck 62 | celery.handle.stuck.task 63 | form 64 | 65 | new 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /celery/security/celery_security.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Celery 9 | Celery (Distributed Task Queue) 10 | 100 11 | 12 | 13 | 14 | Celery Manager 15 | 16 | 17 | 18 | 19 | 20 | Celery RPC 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /celery/security/ir_model_access.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | celery.task: Celery Manager 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | celery.task: Celery RPC 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | celery.requeue.task: Celery Manager 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | celery.cancel.task: Celery Manager 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | celery.handle.stuck.task: Celery Manager 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | celery.task.setting: Celery Manager 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | celery.queue: Celery RPC 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | celery.queue: Celery Manager 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | celery.task.setting.queue: Celery RPC 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | celery.task.setting_queue: Celery Manager 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | celery.stuck.task.report: Celery Manager 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /celery/static/description/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/banner.png -------------------------------------------------------------------------------- /celery/static/description/banner.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/banner.xcf -------------------------------------------------------------------------------- /celery/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/icon.png -------------------------------------------------------------------------------- /celery/static/description/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/icon.xcf -------------------------------------------------------------------------------- /celery/static/description/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Celery: Distributed Task Queue

4 |

Odoo integration of the Python #1 asynchronous task queue

5 | 6 |
7 | Features: 8 |
    9 |
  • 10 | 11 | Put model-methods on the Celery Task Queue. 12 |
  • 13 |
  • 14 | 15 | Monitor and manage the Task Queue in Odoo. 16 |
  • 17 |
  • 18 | 19 | All Exceptions are catched and available as State=Failure with Exception message/trace shown. 20 |
  • 21 |
  • 22 | 23 | Requeue of Failed and Pending (stale) tasks. 24 |
  • 25 |
  • 26 | 27 | No complex installation and setup requirements are needed, except the Celery setup. 28 |
  • 29 |
30 |
31 | 32 |
33 |

Check out the links below, for more info about Celery.

34 |
35 | Celery website

36 | http://celeryproject.org 37 |
38 |
39 | Celery installation

40 | http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html 41 |
42 |
43 |
44 | 45 |
46 |
47 |

Put a model-method on the Celery Task Queue

48 |

Just Python code to call from an Odoo model

49 |
50 |
51 |

Example

52 |

Model and method:

53 |
    54 |
  • model: "celery.example"
  • 55 |
  • method: "schedule_import"
  • 56 |
57 | 58 |

Celery Options:

59 |

Shall be provided to kwargs of the call_task method

60 |
 61 | celery = {
 62 |     'queue': 'high.priority',
 63 |     'countdown': 5,
 64 |     'retry': True,
 65 |     'retry_countdown_setting': 'MUL_RETRIES_SECS',
 66 |     'retry_countdown_multiply_retries_seconds': 60,
 67 |     'retry_policy': {
 68 |         'interval_start': 30
 69 |     }
 70 | }
71 |

Calling the task:

72 |
self.env["celery.task"].call_task("celery.example", "schedule_import", celery=celery)
73 |
74 | 75 |
76 |

Celery Options

77 |

All Celery options are optional (not required).

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
OptionDescriptionCelery Documentation
queueName of the Celery/worker queue, the task shall be routed to.Routing Tasks: http://docs.celeryproject.org/en/latest/userguide/routing.html
countdownThe countdown is a shortcut to set ETA by seconds into the future.ETA and Countdown: http://docs.celeryproject.org/en/latest/userguide/calling.html#eta-and-countdown
retrySet to True to enable the retry of sending task messages.Message Sending Retry: http://docs.celeryproject.org/en/latest/userguide/calling.html#message-sending-retry
retry_countdown_settingSpecify whether and how to increase the Retry Countdown by: 105 |
    106 |
  • ADD_SECS:
    107 | countdown = countdown + retry_countdown_add_seconds
  • 108 |
  • MUL_RETRIES:
    109 | countdown = countdown * request.retries
  • 110 |
  • MUL_RETRIES_SECS:
    111 | countdown = request.retries * retry_countdown_multiply_retries_seconds
  • 112 |
113 |
This is a custom option which affects the Celery countdown option.
retry_countdown_add_secondsSpecify the seconds (integer field) to add to the Retry Countdown.This is a custom option which affects the Celery countdown option.
retry_countdown_multiply_retries_secondsSpecify the seconds (integer field) to multiply with request retries, which becomes the Retry Countdown.This is a custom option which affects the Celery countdown option.
retry_policyOptions when retrying publishing a task message in the case of connection loss or other connection errors.Message Sending Retry: http://docs.celeryproject.org/en/latest/userguide/calling.html#message-sending-retry
134 |
135 | 136 |
137 |

Extra kwargs

138 |

Extra kwargs are optional (not required).

139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 157 | 158 | 159 | 160 |
KwargDescription
transaction_strategySpecifies when the task shall apply (ORM create and send to Celery MQ): 148 |
    149 |
  • after_commit:
    150 | Apply task after commit of the main/caller transaction (default setting). 151 |
  • 152 |
  • immediate:
    153 | Apply task immediately from the main/caller transaction, even if it ain't committed yet. 154 |
  • 155 |
156 |
161 |
162 |
163 | 164 |
165 |
166 |

Monitor and control the Celery Task Queue

167 |
168 |

List of queued tasks

169 | 170 |
171 |
172 |

Task Failure info

173 | 174 |
175 |
176 |

Tasks waiting for Retry

177 | 178 |
179 |
180 |

Task waiting for Retry with Failure info

181 | 182 |
183 |
184 |

Requeue a Failed Task

185 | 186 |
187 |
188 |

Requeue multiple Failed Tasks

189 | 190 |
191 |
192 |
193 | 194 |
195 |
196 |

Installation and configuration

197 |
198 |

Celery and Message broker

199 |
200 |
201 | Celery Installation

202 | http://docs.celeryproject.org/en/latest/getting-started/first-steps-with-celery.html 203 |
204 |
205 |
206 |
207 |

Odoo configuration

208 |
All you need is to determine the Odoo Celery user and setup the credentials for XML-RPC authentication.
209 |

This enables the Celery process to authenticate in Odoo, by the XML-RPC server.

210 |

To support different kind of deployment and build tools, the credentials can either be setup as: 211 |

    212 |
  • 213 |

    (OS) Environment variables of the user running the Odoo server process: ODOO_CELERY_USER and ODOO_CELERY_PASSWORD

    214 |
  • 215 |
  • 216 |

    (OS) Environment variables for third-party broker: ODOO_CELERY_BROKER, ODOO_CELERY_BROKER_HEARTBEAT, ODOO_CELERY_WORKER_PREFETCH_MULTIPLIER

    217 |
  • 218 |
  • 219 |

    Put in the odoo.conf file, e.g: 220 |

    221 | celery_user = Odoo-User
    222 | celery_password = Odoo-User-Password
    223 |

    See example.odoo.conf, visit link: https://github.com/novacode-nl/odoo-celery/blob/14.0/celery/example.odoo.conf

    224 |
  • 225 |
  • 226 |

    Put in the odoo.conf file, under section [celery] e.g: 227 |

    228 | [celery]
    229 | user = Odoo-User
    230 | password = Odoo-User-Password
    231 |

    See example_section_celery.odoo.conf, visit link: https://github.com/novacode-nl/odoo-celery/blob/14.0/celery/example_section_celery.odoo.conf

    232 |
  • 233 |
234 |
235 |

Start the Celery (Odoo) worker server

236 |

Check the Celery Documentation for more ways to start and manage the server/proces. E.g. by Supervisor

237 |

The Celery (Odoo) worker => Python file odoo.py, which is located directly under the "celery" module directory.

238 | 239 |

Start the worker (default/celery queue) on command line, whereas "odoo" references to the Python file:

240 |
# celery -A odoo worker --loglevel=info
241 | 242 |

Start a worker as a named queue:

243 |
# celery -A odoo worker --loglevel=info -Q high.priority -n high.priority
244 |
245 |
246 |
247 | 248 |
249 |
250 |

"Celery Example" module

251 |

Demo of 2 implemented example tasks, queued by 2 buttons

252 |
253 |

Check out the "Celery Example" module

254 |
    255 |
  • After installation, go to the menu "Celery / Example Task".
  • 256 |
  • Click button(s) "Queue create Line" shown in screensshot, which puts a task on the queue.
  • 257 |
  • Check the queue (menu: Celery / Tasks). Check the form of the Example record - new Lines had been created.
  • 258 |
259 |
260 | Celery Examples module

261 | https://apps.odoo.com/apps/modules/11.0/celery_example/ 262 |
263 | 264 | 265 |
266 |
267 |
268 | 269 |
270 |
271 |

Changelog

272 |

0.27

273 |
    274 |
  • 275 | Improvement: Add tracking for field exc_info (Exception Info).
    276 | (GitHub PR: https://github.com/novacode-nl/odoo-celery/pull/38) 277 |
  • 278 |
  • 279 | Imrpvovement: Implement broker connection based on env.vars
    280 | (GitHub PR: https://github.com/novacode-nl/odoo-celery/pull/41) 281 |
  • 282 |
283 |

0.26

284 |
    285 |
  • 286 | Fix: AccessDenied error is raised if no password was passed to the function, but this exception was not imported.
    287 | (GitHub PR: https://github.com/novacode-nl/odoo-celery/pull/37) 288 |
  • 289 |
290 |

0.25

291 |
    292 |
  • 293 | Fix relating to 0.21 (support inactive Celery User). Inactive user not allowed anymore, since recent Odoo 14.0
    294 | Override the res.users access check method: only for Celery user (from config) remove the active check.
  • 295 |
296 |

0.24

297 |
    298 |
  • 299 | Fix crash (upon install, update) due to faulty config of Window Action "Flag/update task: Stuck" (action_celery_task_set_state_stuck) - introduced by porting to Odoo 14.0. 300 |
  • 301 |
302 |

0.23

303 |
    304 |
  • 305 | Fix cancellation of stucked tasks(s) wasn't possible. 306 |
  • 307 |
  • 308 | Fix requeue of stucked tasks(s) wasn't possible. 309 |
  • 310 |
311 |

0.22

312 |
    313 |
  • 314 | Upon processing task rpc_run_task (method), only update the related record fields (res_model, res_ids) if these have a value set in the method its result.
    315 | This enables the API to store these related record fields upfront, when the task is created. 316 |
  • 317 |
318 |

0.21

319 |
    320 |
  • Some security hardening: support inactive Celery User, which refuses regular (web) authentication/login.
  • 321 |
322 |

0.20

323 |
    324 |
  • 325 | [FIX] In call_task() the search of celery.task.setting results in Access Denied by ACLs for operation: read, model: celery.task.setting.
    326 | Solved by calling the celery.task.setting search() with sudo().
  • 327 |
328 |

0.19

329 |
    330 |
  • [FIX] Related record: res_ids (now ListSerialized field).
  • 331 |
  • Also change kwargs field to sensible class(name): TaskSerialized to KwargsSerialized.
  • 332 |
333 |

0.18

334 |
    335 |
  • Configurable (database) transaction strategy, when the task shall apply (ORM create and send to Celery MQ).
  • 336 |
  • From now on - by default a task shall apply after commit of the main/caller transaction, instead of immediately in the main/caller transaction.
  • 337 |
338 |

0.17

339 |
    340 |
  • Task scheduling added - being able to schedule tasks in a specified time interval or certain day(s) of the week.
  • 341 |
  • A new task state - Scheduled, is handled by an Odoo cron job - "Celery: Handle Scheduled Tasks".
  • 342 |
343 |

0.16

344 |
    345 |
  • Configurable celery queues added to task settings.
  • 346 |
347 |

0.15

348 |
    349 |
  • Scheduled (cron) cleanup of tasks - with optionally specifying/overriding: (1) timedelta (days=90, hours, minutes, seconds), (2) states to keep and (3) batch_size=100.
  • 350 |
  • Create database index for the State Time (state_date) field.
  • 351 |
352 |

0.14

353 |
    354 |
  • Create database index for the Reference field (ref).
  • 355 |
356 |

0.13

357 |
    358 |
  • Get XML-RPC URL from ir.config.parameter (celery.celery_base_url) by Settings.
  • 359 |
360 |

0.12

361 |
    362 |
  • Also support to get the Celery (Odoo) user and password from the odoo.conf file, under section [options] too.
  • 363 |
364 |

0.11

365 |
    366 |
  • Put task into a new state "Retrying", right before the Retry starts.
  • 367 | 368 |
369 |

0.10

370 |
    371 |
  • Renamed task retry settings: 'MUL_RETR' to 'MUL_RETRIES', 'MUL_RETR_SECS' to 'MUL_RETRIES_SECS'.
  • 372 | 373 |
374 |

0.9

375 |
    376 |
  • 377 | Hide (Odoo) password in the "retry" payload used by the MQ broker for XML-RPC authentication.
    378 | GitHub issue: https://github.com/novacode-nl/odoo-celery/issues/17 379 |
  • 380 |
381 |

0.8

382 |
    383 |
  • Fix task retry countdown/interval ignored. For more info see https://github.com/novacode-nl/odoo-celery/issues/14
  • 384 |
  • Add task retry countdown settings, to increase countdown during retries ('ADD_SECS', 'MUL_RETRY', 'MUL_RETRY_SECS').
  • 385 |
  • Search view of stuck tasks: (1) Add/show field reference (2) search-view with filters and grouping.
  • 386 |
  • Track task changes of fields model, method, handle_stuck
  • 387 |
  • Disable create/copy in the task form-view.
  • 388 |
389 |

0.7

390 |
    391 |
  • Task Reference, which serves: 392 |
      393 |
    • Easier searching for tasks by Reference.
    • 394 |
    • Check (ORM query) before call_task() call, to prevent redundant creation of recurring Pending tasks.
    • 395 |
    396 |
  • 397 |
398 |

0.6

399 | Introduction of "Stuck" tasks. A stuck task could be caused, for example by: a stopped/restarted Celery worker, message broker, or server. 400 |
    401 |
  • Settings: to specify when a task (model, method) is Stuck after seconds from Started or Retry (states).
  • 402 |
  • Stuck Tasks Report: which shows Stuck (not completed) tasks.
  • 403 |
  • Cron or manually put tasks in "Stuck" state.
  • 404 |
  • Ability to cancel (Pending or Stuck) tasks, which never completed.
  • 405 |
  • Track and messaging about state change. Chatter on form view.
  • 406 |
407 |

0.5

408 |
    409 |
  • Routing tasks to specific (named) queues.
  • 410 |
411 |

0.4

412 |
    413 |
  • FIX: Store task state (Started, Retry) before execution.
  • 414 |
415 |

0.3

416 |
    417 |
  • 418 | Hide (Odoo) password in payload used by the broker for XML-RPC authentication.
    419 | GitHub issue: https://github.com/novacode-nl/odoo-celery/issues/4 420 |
  • 421 |
422 |

0.2

423 |
    424 |
  • Task state information.
  • 425 |
426 |

0.1

427 |
    428 |
  • Initial version.
  • 429 |
430 |
431 |
432 | -------------------------------------------------------------------------------- /celery/static/description/odoo-example-task-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-example-task-form.png -------------------------------------------------------------------------------- /celery/static/description/odoo-example-task-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-example-task-list.png -------------------------------------------------------------------------------- /celery/static/description/odoo-requeue-multiple-tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-requeue-multiple-tasks.png -------------------------------------------------------------------------------- /celery/static/description/odoo-requeue-single-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-requeue-single-task.png -------------------------------------------------------------------------------- /celery/static/description/odoo-task-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-task-failure.png -------------------------------------------------------------------------------- /celery/static/description/odoo-task-queue-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-task-queue-failure.png -------------------------------------------------------------------------------- /celery/static/description/odoo-task-queue-retry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-task-queue-retry.png -------------------------------------------------------------------------------- /celery/static/description/odoo-task-retry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery/static/description/odoo-task-retry.png -------------------------------------------------------------------------------- /celery/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import test_celery_task 5 | -------------------------------------------------------------------------------- /celery/tests/test_celery_task.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import json 5 | import uuid 6 | import pytz 7 | 8 | from odoo import fields 9 | from odoo.exceptions import UserError, ValidationError 10 | from odoo.tools.misc import mute_logger 11 | from odoo.tests.common import TransactionCase 12 | 13 | from ..models.celery_task import CeleryTask, STATE_PENDING, STATE_STARTED, STATE_RETRY 14 | 15 | 16 | class TestCeleryTask(TransactionCase): 17 | 18 | def setUp(self): 19 | super(TestCeleryTask, self).setUp() 20 | 21 | def test_unlink_started_task(self): 22 | """ Unlink STARTED task """ 23 | 24 | Task = self.env['celery.task'] 25 | vals = { 26 | 'uuid': str(uuid.uuid4()), 27 | 'user_id': self.env.user.id, 28 | 'model': 'celery.task', 29 | 'method': 'dummy_method', 30 | } 31 | task = Task.create(vals) 32 | 33 | task.state = STATE_STARTED 34 | with self.assertRaisesRegex(UserError, 'You cannot delete a running task'), mute_logger('odoo.sql_db'): 35 | task.unlink() 36 | 37 | def test_unlink_retry_task(self): 38 | """ Unlink RETRY task """ 39 | 40 | Task = self.env['celery.task'] 41 | vals = { 42 | 'uuid': str(uuid.uuid4()), 43 | 'user_id': self.env.user.id, 44 | 'model': 'celery.task', 45 | 'method': 'dummy_method', 46 | } 47 | task = Task.create(vals) 48 | 49 | task.state = STATE_STARTED 50 | with self.assertRaisesRegex(UserError, 'You cannot delete a running task'), mute_logger('odoo.sql_db'): 51 | task.unlink() 52 | 53 | def test_write_task_update_celery_kwargs(self): 54 | """ Write task (Celery param fields) update Celery kwargs """ 55 | 56 | Task = self.env['celery.task'] 57 | 58 | vals = { 59 | 'uuid': str(uuid.uuid4()), 60 | 'user_id': self.env.user.id, 61 | 'model': 'celery.task', 62 | 'method': 'dummy_method', 63 | 'kwargs': {'celery': {'retry': False,'countdown': 3}} 64 | } 65 | task = Task.create(vals) 66 | task.write({ 67 | 'retry': True, 68 | 'max_retries': 5, 69 | 'countdown': 10}) 70 | 71 | kwargs = json.loads(task.kwargs) 72 | 73 | self.assertTrue(kwargs['celery']['retry']) 74 | self.assertEqual(kwargs['celery']['max_retries'], 5) 75 | self.assertEqual(kwargs['celery']['countdown'], 10) 76 | 77 | def test_scheduled_task(self): 78 | """ Creates a task setting scheduling the tasks to a time-window between 23:30 and 23:36 UTC """ 79 | 80 | self.env['res.users'].sudo().browse(self.env.uid).tz = 'UTC' 81 | vals = { 82 | 'model': 'celery.task', 83 | 'method': 'dummy_method_schedule', 84 | 'schedule': True, 85 | 'schedule_hours_from': 23.5, 86 | 'schedule_hours_to': 23.6, 87 | } 88 | task_setting = self.env['celery.task.setting'].create(vals) 89 | 90 | now = fields.datetime.now().replace(tzinfo=pytz.UTC) 91 | scheduled_date = self.env['celery.task'].check_schedule_needed(task_setting) 92 | 93 | if now.hour == 23 and now.minute > 30 and now.minute < 36: 94 | self.assertEqual(scheduled_date, False) 95 | else: 96 | self.assertEqual(scheduled_date.hour, 23) 97 | self.assertEqual(scheduled_date.minute, 30) 98 | -------------------------------------------------------------------------------- /celery/views/celery_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 17 | 22 | 23 | 24 | 28 | 33 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /celery/views/celery_queue_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.queue.tree 9 | celery.queue 10 | tree 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | celery.queue.form 22 | celery.queue 23 | form 24 | 25 |
26 | 27 |
28 | 31 |
32 | 33 |
34 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 |
51 |
52 |
53 | 54 | 55 | 56 | celery.queue.kanban 57 | celery.queue 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 80 |
81 |
82 |

83 |
84 |
85 | 86 |
87 | 88 |
89 | Total tasks: ( percent of all tasks) 90 |
91 |
92 | Pending tasks: 93 |
94 |
95 | Added in the last 24h: 96 |
97 |
98 | Succeded in the last 24h: 99 |
100 |
101 | Failed in the last 24h: 102 |
103 |
104 | 105 |
106 |
107 |
108 |
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | celery.queue 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Celery Queues 129 | celery.queue 130 | kanban,tree,form 131 | {'compute_queue_stats': True} 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /celery/views/celery_task_setting_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.task.setting.tree 9 | celery.task.setting 10 | tree 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | celery.task.setting.form 27 | celery.task.setting 28 | form 29 | 30 |
31 | 32 |
33 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |

A task seems Stuck when it's still in state Started or Retry, after certain elapsed seconds.

65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 |
106 | 107 | 108 |
109 |
110 |
111 |
112 | 113 | 114 | celery.task.setting 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Celery Task Settings 131 | celery.task.setting 132 | tree,form 133 | {'search_default_all': 1} 134 | 135 | 136 |
137 |
138 | -------------------------------------------------------------------------------- /celery/views/celery_task_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.task.tree 9 | celery.task 10 | tree 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | celery.task.form 31 | celery.task 32 | form 33 | 34 |
35 |
36 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 71 |
72 |

No Retry/Failure has occurred on this moment.

73 |

Update the information by refreshing this view (button above).

74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 91 | 92 | 96 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 | 117 | 118 |
119 |
120 |
121 |
122 | 123 | 124 | celery.task.search 125 | celery.task 126 | search 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 138 | 140 | 142 | 144 | 146 | 148 | 150 | 152 | 153 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | Celery Tasks 177 | celery.task 178 | tree,form 179 | 180 | 181 |
182 |
183 | -------------------------------------------------------------------------------- /celery/views/res_config_settings_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | res.config.settings.celery.view.form 8 | res.config.settings 9 | 10 | 11 | 12 |
13 |

Celery

14 |
15 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /celery/wizard/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import celery_requeue_task 5 | from . import celery_cancel_task 6 | from . import celery_handle_stuck_task 7 | -------------------------------------------------------------------------------- /celery/wizard/celery_cancel_task.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models 5 | 6 | from ..models.celery_task import STATES_TO_CANCEL 7 | 8 | 9 | class CancelTask(models.TransientModel): 10 | _name = 'celery.cancel.task' 11 | _description = 'Celery Cancel Task Wizard' 12 | 13 | @api.model 14 | def _default_task_ids(self): 15 | states_to_cancel = self.env['celery.task']._states_to_cancel() 16 | res = False 17 | context = self.env.context 18 | if (context.get('active_model') == 'celery.task' and 19 | context.get('active_ids')): 20 | task_ids = context['active_ids'] 21 | res = self.env['celery.task'].search([ 22 | ('id', 'in', context['active_ids']), 23 | '|', 24 | ('state', 'in', states_to_cancel), 25 | ('stuck', '=', True) 26 | ]).ids 27 | return res 28 | 29 | task_ids = fields.Many2many('celery.task', string='Tasks', default=_default_task_ids) 30 | 31 | def action_cancel(self): 32 | self.task_ids.action_cancel() 33 | return {'type': 'ir.actions.act_window_close'} 34 | -------------------------------------------------------------------------------- /celery/wizard/celery_cancel_task_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Cancel Task 9 | celery.cancel.task 10 | 11 |
12 | 13 | 14 | 15 |
16 |
19 |
20 |
21 |
22 | 23 | 24 | Cancel Task 25 | celery.cancel.task 26 | form 27 | 28 | new 29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /celery/wizard/celery_handle_stuck_task.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models 5 | 6 | from ..models.celery_task import STATES_TO_STUCK 7 | 8 | 9 | class CeleryHandleStuckJask(models.TransientModel): 10 | _name = 'celery.handle.stuck.task' 11 | _description = 'Handle Stuck Task' 12 | 13 | @api.model 14 | def _default_task_ids(self): 15 | res = False 16 | context = self.env.context 17 | if (context.get('active_model') == 'celery.stuck.task.report' and 18 | context.get('active_ids')): 19 | task_ids = context['active_ids'] 20 | res = self.env['celery.task'].search([ 21 | ('id', 'in', context['active_ids']), 22 | ('state', 'in', STATES_TO_STUCK)]).ids 23 | return res 24 | 25 | task_ids = fields.Many2many( 26 | 'celery.task', string='Tasks', default=_default_task_ids, 27 | domain=[('state', 'in', STATES_TO_STUCK)]) 28 | 29 | def action_handle_stuck_tasks(self): 30 | self.task_ids.action_stuck() 31 | return {'type': 'ir.actions.act_window_close'} 32 | -------------------------------------------------------------------------------- /celery/wizard/celery_handle_stuck_task_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Celery Handle Stuck Task 9 | celery.handle.stuck.task 10 | 11 |
12 | 13 | 14 | 15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /celery/wizard/celery_requeue_task.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from odoo import api, fields, models 5 | 6 | from ..models.celery_task import STATES_TO_REQUEUE 7 | 8 | 9 | class RequeueTask(models.TransientModel): 10 | _name = 'celery.requeue.task' 11 | _description = 'Celery Requeue Tasks Wizard' 12 | 13 | @api.model 14 | def _default_task_ids(self): 15 | res = False 16 | context = self.env.context 17 | if (context.get('active_model') == 'celery.task' and 18 | context.get('active_ids')): 19 | task_ids = context['active_ids'] 20 | res = self.env['celery.task'].search([ 21 | ('id', 'in', context['active_ids']), 22 | '|', 23 | ('state', 'in', STATES_TO_REQUEUE), 24 | ('stuck', '=', True) 25 | ]).ids 26 | return res 27 | 28 | task_ids = fields.Many2many( 29 | 'celery.task', string='Tasks', default=_default_task_ids, 30 | domain=['|', ('stuck', '=', True), ('state', 'in', STATES_TO_REQUEUE)]) 31 | 32 | def action_requeue(self): 33 | self.task_ids.action_requeue() 34 | return {'type': 'ir.actions.act_window_close'} 35 | -------------------------------------------------------------------------------- /celery/wizard/celery_requeue_task_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Requeue Tasks 9 | celery.requeue.task 10 | 11 |
12 | 13 | 14 | 15 |
16 |
19 |
20 |
21 |
22 | 23 | 24 | Requeue Task 25 | celery.requeue.task 26 | form 27 | 28 | new 29 | 30 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /celery_example/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import models 5 | -------------------------------------------------------------------------------- /celery_example/__manifest__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | { 4 | 'name': 'Celery Examples', 5 | 'summary': 'Celery Example tasks ready to run from Odoo.', 6 | 'category': 'Extra Tools', 7 | 'version': '0.2', 8 | 'description': """Put example tasks on the Celery Task Queue.""", 9 | 'author': 'Nova Code', 10 | 'website': 'https://www.novacode.nl', 11 | 'license': "LGPL-3", 12 | 'depends': ['celery'], 13 | 'data': [ 14 | 'data/celery_example_data.xml', 15 | 'security/ir_model_access.xml', 16 | 'views/celery_example_views.xml' 17 | ], 18 | 'images': [ 19 | 'static/description/banner.png', 20 | ], 21 | 'installable': True, 22 | 'application' : False, 23 | } 24 | -------------------------------------------------------------------------------- /celery_example/data/celery_example_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | Example Task 9 | 10 | 11 | 12 | Celery Example: Schedule example 13 | 14 | code 15 | model._cron_schedule_example() 16 | 17 | 18 | 1 19 | hours 20 | -1 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /celery_example/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | from . import celery_example 5 | -------------------------------------------------------------------------------- /celery_example/models/celery_example.py: -------------------------------------------------------------------------------- 1 | # Copyright Nova Code (http://www.novacode.nl) 2 | # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) 3 | 4 | import logging 5 | import time 6 | 7 | from odoo import api, fields, models, _ 8 | from odoo.addons.celery.models.celery_task import RETRY_COUNTDOWN_MULTIPLY_RETRIES 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | 13 | class CeleryExample(models.Model): 14 | _name = 'celery.example' 15 | _description = 'Celery Example' 16 | 17 | name = fields.Char(default='Celery Example', required=True) 18 | lines = fields.One2many('celery.example.line', 'example_id', string='Lines') 19 | 20 | def action_task_with_reference(self): 21 | celery = { 22 | 'countdown': 10, 'retry': True, 23 | 'retry_policy': {'max_retries': 2, 'interval_start': 2} 24 | } 25 | celery_task_vals = { 26 | 'ref': 'celery.example.task_with_reference' 27 | } 28 | self.env["celery.task"].call_task("celery.example", "task_with_reference", example_id=self.id, celery_task_vals=celery_task_vals, celery=celery) 29 | 30 | def action_task_immediate(self): 31 | celery = { 32 | 'countdown': 10, 'retry': True, 33 | 'retry_policy': {'max_retries': 2, 'interval_start': 2} 34 | } 35 | celery_task_vals = { 36 | 'ref': 'celery.example.task_immediate' 37 | } 38 | self.env["celery.task"].call_task( 39 | "celery.example", "task_immediate", 40 | example_id=self.id, 41 | celery_task_vals=celery_task_vals, 42 | celery=celery, 43 | transaction_strategy='immediate') 44 | 45 | def action_task_with_error(self): 46 | celery = { 47 | 'countdown': 2, 48 | 'retry': True, 49 | 'max_retries': 4, 50 | 'retry_countdown_setting': 'MUL_RETRIES_SECS', 51 | 'retry_countdown_multiply_retries_seconds': 5, 52 | 'retry_policy': {'interval_start': 2} 53 | } 54 | celery_task_vals = { 55 | 'ref': 'celery.example.task_with_error' 56 | } 57 | self.env["celery.task"].call_task("celery.example", "task_with_error", example_id=self.id, celery=celery) 58 | 59 | def action_task_queue_default(self): 60 | celery = { 61 | 'countdown': 3, 'retry': True, 62 | 'retry_policy': {'max_retries': 2, 'interval_start': 2} 63 | } 64 | self.env["celery.task"].call_task("celery.example", "task_queue_default", example_id=self.id, celery=celery) 65 | 66 | def action_task_queue_high(self): 67 | celery = { 68 | 'queue': 'high.priority', 'countdown': 2, 'retry': True, 69 | 'retry_policy': {'max_retries': 2, 'interval_start': 2} 70 | } 71 | self.env["celery.task"].call_task("celery.example", "task_queue_high", example_id=self.id, celery=celery) 72 | 73 | def action_task_queue_low(self): 74 | celery = { 75 | 'queue': 'low.priority', 'countdown': 2, 'retry': True, 76 | 'retry_policy': {'max_retries': 2, 'interval_start': 2} 77 | } 78 | self.env["celery.task"].call_task("celery.example", "task_queue_low", example_id=self.id, celery=celery) 79 | 80 | @api.model 81 | def task_with_reference(self, task_uuid, **kwargs): 82 | task = 'task_with_reference' 83 | example_id = kwargs.get('example_id') 84 | res = self.env['celery.example.line'].create({ 85 | 'name': task, 86 | 'example_id': example_id 87 | }) 88 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 89 | _logger.info(msg) 90 | return {'result': msg, 'res_model': 'celery.example.line', 'res_ids': [res.id]} 91 | 92 | @api.model 93 | def task_immediate(self, task_uuid, **kwargs): 94 | task = 'task_immediate' 95 | example_id = kwargs.get('example_id') 96 | self.env['celery.example.line'].create({ 97 | 'name': task, 98 | 'example_id': example_id 99 | }) 100 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 101 | _logger.info(msg) 102 | return msg 103 | 104 | @api.model 105 | def task_with_error(self, task_uuid, **kwargs): 106 | task = 'task_with_error' 107 | _logger.critical('RETRY of %s' % task) 108 | 109 | example_id = kwargs.get('example_id') 110 | self.env['celery.example.line'].create({ 111 | 'example_id': example_id 112 | }) 113 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 114 | _logger.info(msg) 115 | return msg 116 | 117 | @api.model 118 | def task_queue_default(self, task_uuid, **kwargs): 119 | task = 'task_queue_default' 120 | example_id = kwargs.get('example_id') 121 | self.env['celery.example.line'].create({ 122 | 'name': task, 123 | 'example_id': example_id 124 | }) 125 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 126 | _logger.info(msg) 127 | return msg 128 | 129 | @api.model 130 | def task_queue_high(self, task_uuid, **kwargs): 131 | time.sleep(2) 132 | task = 'task_queue_high' 133 | example_id = kwargs.get('example_id') 134 | self.env['celery.example.line'].create({ 135 | 'name': task, 136 | 'example_id': example_id 137 | }) 138 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 139 | _logger.info(msg) 140 | return msg 141 | 142 | @api.model 143 | def task_queue_low(self, task_uuid, **kwargs): 144 | time.sleep(5) 145 | 146 | task = 'task_queue_low' 147 | example_id = kwargs.get('example_id') 148 | self.env['celery.example.line'].create({ 149 | 'name': task, 150 | 'example_id': example_id 151 | }) 152 | msg = 'CELERY called task: model [%s] and method [%s].' % (self._name, task) 153 | _logger.info(msg) 154 | return msg 155 | 156 | def _cron_schedule_example(self): 157 | self.env['celery.task'].call_task(self._name, 'schedule_cron_example') 158 | 159 | @api.model 160 | def schedule_cron_example(self, task_uuid, **kwargs): 161 | self.env['celery.task'].call_task( 162 | self._name, 'run_cron_example') 163 | 164 | msg = 'Schedule Cron Example' 165 | _logger.critical(msg) 166 | return {'result': msg} 167 | 168 | @api.model 169 | def run_cron_example(self, task_uuid, **kwargs): 170 | msg = 'Run Cron Example' 171 | _logger.critical(msg) 172 | return {'result': msg} 173 | 174 | def refresh_view(self): 175 | return True 176 | 177 | 178 | class CeleryExampleLine(models.Model): 179 | _name = 'celery.example.line' 180 | _description = 'Celery Example Line' 181 | 182 | name = fields.Char(required=True) 183 | example_id = fields.Many2one('celery.example', string='Example', required=True, ondelete='cascade') 184 | -------------------------------------------------------------------------------- /celery_example/security/ir_model_access.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.example: Celery Manager 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | celery.example: Celery RPC 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | celery.example.line: Celery Manager 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | celery.example.line: Celery RPC 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /celery_example/static/description/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery_example/static/description/banner.png -------------------------------------------------------------------------------- /celery_example/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/novacode-nl/odoo-celery/ec02f2e37c724665b26b5023ef8f4ac0463ae1b9/celery_example/static/description/icon.png -------------------------------------------------------------------------------- /celery_example/views/celery_example_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | celery.example.tree 9 | celery.example 10 | 11 | 12 | 13 | 14 | 32 |
33 |

34 |
35 |