-
Notifications
You must be signed in to change notification settings - Fork 0
OD-1593/queue/sequence groups #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 12.0
Are you sure you want to change the base?
Changes from all commits
4c2024d
8b643bc
8bd124e
e2f25a4
342ddd7
15588cc
eeb31b6
9b98f1b
a472ae5
3151ffa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -55,14 +55,15 @@ class DelayableRecordset(object): | |||||||||||
|
|
||||||||||||
| def __init__(self, recordset, priority=None, eta=None, | ||||||||||||
| max_retries=None, description=None, channel=None, | ||||||||||||
| identity_key=None): | ||||||||||||
| identity_key=None, sequence_rule_ids=None): | ||||||||||||
| self.recordset = recordset | ||||||||||||
| self.priority = priority | ||||||||||||
| self.eta = eta | ||||||||||||
| self.max_retries = max_retries | ||||||||||||
| self.description = description | ||||||||||||
| self.channel = channel | ||||||||||||
| self.identity_key = identity_key | ||||||||||||
| self.sequence_rule_ids = sequence_rule_ids | ||||||||||||
|
|
||||||||||||
| def __getattr__(self, name): | ||||||||||||
| if name in self.recordset: | ||||||||||||
|
|
@@ -87,7 +88,8 @@ def delay(*args, **kwargs): | |||||||||||
| eta=self.eta, | ||||||||||||
| description=self.description, | ||||||||||||
| channel=self.channel, | ||||||||||||
| identity_key=self.identity_key) | ||||||||||||
| identity_key=self.identity_key, | ||||||||||||
| sequence_rule_ids=self.sequence_rule_ids) | ||||||||||||
| return delay | ||||||||||||
|
|
||||||||||||
| def __str__(self): | ||||||||||||
|
|
@@ -241,6 +243,10 @@ class Job(object): | |||||||||||
| be added to a channel if the existing job with the same key is not yet | ||||||||||||
| started or executed. | ||||||||||||
|
|
||||||||||||
| .. attribute::sequence_rule_ids | ||||||||||||
|
|
||||||||||||
| Reference to rules to process jobs sequentially. | ||||||||||||
|
|
||||||||||||
| """ | ||||||||||||
| @classmethod | ||||||||||||
| def load(cls, env, job_uuid): | ||||||||||||
|
|
@@ -272,7 +278,8 @@ def _load_from_db_record(cls, job_db_record): | |||||||||||
| job_ = cls(method, args=args, kwargs=kwargs, | ||||||||||||
| priority=stored.priority, eta=eta, job_uuid=stored.uuid, | ||||||||||||
| description=stored.name, channel=stored.channel, | ||||||||||||
| identity_key=stored.identity_key) | ||||||||||||
| identity_key=stored.identity_key, | ||||||||||||
| sequence_rule_ids=stored.sequence_rule_ids) | ||||||||||||
|
|
||||||||||||
| if stored.date_created: | ||||||||||||
| job_.date_created = stored.date_created | ||||||||||||
|
|
@@ -296,6 +303,7 @@ def _load_from_db_record(cls, job_db_record): | |||||||||||
| if stored.company_id: | ||||||||||||
| job_.company_id = stored.company_id.id | ||||||||||||
| job_.identity_key = stored.identity_key | ||||||||||||
| job_.sequence_rule_ids = stored.sequence_rule_ids if stored.sequence_rule_ids else None | ||||||||||||
| return job_ | ||||||||||||
|
|
||||||||||||
| def job_record_with_same_identity_key(self): | ||||||||||||
|
|
@@ -310,7 +318,7 @@ def job_record_with_same_identity_key(self): | |||||||||||
| @classmethod | ||||||||||||
| def enqueue(cls, func, args=None, kwargs=None, | ||||||||||||
| priority=None, eta=None, max_retries=None, description=None, | ||||||||||||
| channel=None, identity_key=None): | ||||||||||||
| channel=None, identity_key=None, sequence_rule_ids=None): | ||||||||||||
| """Create a Job and enqueue it in the queue. Return the job uuid. | ||||||||||||
|
|
||||||||||||
| This expects the arguments specific to the job to be already extracted | ||||||||||||
|
|
@@ -323,7 +331,8 @@ def enqueue(cls, func, args=None, kwargs=None, | |||||||||||
| new_job = cls(func=func, args=args, | ||||||||||||
| kwargs=kwargs, priority=priority, eta=eta, | ||||||||||||
| max_retries=max_retries, description=description, | ||||||||||||
| channel=channel, identity_key=identity_key) | ||||||||||||
| channel=channel, identity_key=identity_key, | ||||||||||||
| sequence_rule_ids=sequence_rule_ids) | ||||||||||||
| if new_job.identity_key: | ||||||||||||
| existing = new_job.job_record_with_same_identity_key() | ||||||||||||
| if existing: | ||||||||||||
|
|
@@ -354,7 +363,8 @@ def db_record_from_uuid(env, job_uuid): | |||||||||||
| def __init__(self, func, | ||||||||||||
| args=None, kwargs=None, priority=None, | ||||||||||||
| eta=None, job_uuid=None, max_retries=None, | ||||||||||||
| description=None, channel=None, identity_key=None): | ||||||||||||
| description=None, channel=None, identity_key=None, | ||||||||||||
| sequence_rule_ids=None): | ||||||||||||
| """ Create a Job | ||||||||||||
|
|
||||||||||||
| :param func: function to execute | ||||||||||||
|
|
@@ -378,6 +388,7 @@ def __init__(self, func, | |||||||||||
| :param identity_key: A hash to uniquely identify a job, or a function | ||||||||||||
| that returns this hash (the function takes the job | ||||||||||||
| as argument) | ||||||||||||
| :param sequence_rule_ids: Reference to rules to process jobs sequentially. | ||||||||||||
| :param env: Odoo Environment | ||||||||||||
| :type env: :class:`odoo.api.Environment` | ||||||||||||
| """ | ||||||||||||
|
|
@@ -454,6 +465,37 @@ def __init__(self, func, | |||||||||||
| self._eta = None | ||||||||||||
| self.eta = eta | ||||||||||||
| self.channel = channel | ||||||||||||
| # Handle automatic.workflow.job, normally it is a normal model name. | ||||||||||||
| # In practice only _validate_invoice_job is called through automatic.workflow.job | ||||||||||||
| # This allows the user to select the field on account.invoice, instead of having | ||||||||||||
| # to pick automatic.workflow.job, which doesn't have the fields of the invoice model. | ||||||||||||
| if self.model_name == 'automatic.workflow.job': | ||||||||||||
| model_name = 'account.invoice' | ||||||||||||
| record_ids = self.env[model_name].sudo().browse(self.args) | ||||||||||||
| else: | ||||||||||||
| model_name = self.model_name | ||||||||||||
| record_ids = self.recordset | ||||||||||||
| sequence_rules = env['queue.sequence.rule'].sudo().search([('model_id.model', '=', model_name)]) | ||||||||||||
| if sequence_rules: | ||||||||||||
| self.sequence_rule_ids = [(6, 0, sequence_rules.ids)] | ||||||||||||
| # Change the following when implementing multiple rules per model | ||||||||||||
| self.rule_name = sequence_rules[0].name | ||||||||||||
| if len(sequence_rules) > 1: | ||||||||||||
| _logger.warning('More than one sequence rule defined for %s', | ||||||||||||
| self.model_name) | ||||||||||||
| if len(record_ids) > 1: | ||||||||||||
| _logger.warning( | ||||||||||||
| 'Applying sequence rule for job %s failed because it has multiple related ' | ||||||||||||
| 'records.', self.uuid | ||||||||||||
| ) | ||||||||||||
| else: | ||||||||||||
| for rule in sequence_rules: | ||||||||||||
| value = str(record_ids[rule.field_id.name]) | ||||||||||||
| self.rule_value = value | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just asking to get a better grasp on it, still in the process of understanding this flow - in this case the value of a different rule can be set then the rule name set above (first one taken), is this expected or it doesn't matter when we have multiple ones?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only one rule is allowed per model: queue/queue_job/models/queue_sequence_rule.py Lines 37 to 41 in 3151ffa
I left some stubs to allow for multiple rules per model, as I had started with that approach. I mid-way decided not to implement multiple rules per model because it would make the solution more complex than what we need, especially considering not making the job queue any slower. |
||||||||||||
| else: | ||||||||||||
| self.sequence_rule_ids = None | ||||||||||||
| self.rule_name = None | ||||||||||||
| self.rule_value = None | ||||||||||||
|
|
||||||||||||
| def perform(self): | ||||||||||||
| """Execute the job. | ||||||||||||
|
|
@@ -496,6 +538,7 @@ def store(self): | |||||||||||
| 'date_done': False, | ||||||||||||
| 'eta': False, | ||||||||||||
| 'identity_key': False, | ||||||||||||
| 'sequence_rule_ids': self.sequence_rule_ids if self.sequence_rule_ids else None, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if self.date_enqueued: | ||||||||||||
|
|
@@ -522,6 +565,8 @@ def store(self): | |||||||||||
| 'model_name': self.model_name, | ||||||||||||
| 'method_name': self.method_name, | ||||||||||||
| 'record_ids': self.recordset.ids, | ||||||||||||
| 'rule_name': self.rule_name, | ||||||||||||
| 'rule_value': self.rule_value, | ||||||||||||
| 'args': self.args, | ||||||||||||
| 'kwargs': self.kwargs, | ||||||||||||
| }) | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| from . import base | ||
| from . import queue_job | ||
| from . import queue_sequence_rule |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # (c) 2022 Vanmoof <https://vanmoof.com> | ||
| # License LGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) | ||
|
|
||
| from odoo import _, api, fields, models | ||
|
|
||
|
|
||
| class QueueSequenceRule(models.Model): | ||
| """ | ||
| A model that defines rules for grouping and running queue jobs sequentially. | ||
|
|
||
| A queue job is considered part of a sequence group if the value of the field specified in the `field_id` | ||
| attribute is the same as the value of the field on the related record. | ||
|
|
||
| Example: | ||
| - If the `field_id` attribute is set to 'journal_id', jobs will be grouped and run sequentially | ||
| if the invoices they are processing have the same journal. | ||
|
|
||
| Don't confuse this with sequential queues. Those are simply channels with capacity 1, while this allows | ||
| channels with a larger capacity and can conditionally run jobs in sequence. | ||
|
|
||
| Attributes: | ||
| name (str): The name of the rule. | ||
| model_id (many2one): The name of the model for which the rule applies. | ||
| field_id (many2one): The name of the field on the model used to group jobs. | ||
| """ | ||
| _name = 'queue.sequence.rule' | ||
| _description = 'Sequence rules for queue jobs' | ||
|
|
||
| name = fields.Char(string='Name', required=True) | ||
| active = fields.Boolean(default=True) | ||
| model_id = fields.Many2one( | ||
| comodel_name='ir.model', string='Model', required=True | ||
| ) | ||
| field_id = fields.Many2one(comodel_name='ir.model.fields', string='Field', required=True, | ||
| domain="[('model_id', '=', model_id)]") | ||
|
|
||
| _sql_constraints = [ | ||
| ( | ||
| "uniq_model_id", | ||
| "UNIQUE(model_id)", | ||
| ("Only one rule per model allowed"), | ||
| ), | ||
| ( | ||
| "uniq_name", | ||
| "UNIQUE(name)", | ||
| ("Rule name must be unique"), | ||
| ), | ||
| ] | ||
|
|
||
| @api.onchange('model_id') | ||
| def _onchange_model_id(self): | ||
| self.field_id = False | ||
|
|
||
| @api.constrains('model_id', 'field_id') | ||
| def _check_field_belongs_to_model(self): | ||
| for record in self: | ||
| if record.field_id.model_id != record.model_id: | ||
| raise ValueError(_('The selected field must belong to the selected model.')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I did not add a dependency on this module because this won't break this module. Inheriting the Job class in a custom module for this did not seem reasonable for this kind of change. Albeit, this is a shim.