723 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			723 lines
		
	
	
		
			23 KiB
		
	
	
	
		
			Python
		
	
	
# -*- coding: utf-8 -*-
 | 
						|
# Copyright 2013-2016 Camptocamp
 | 
						|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
 | 
						|
 | 
						|
import inspect
 | 
						|
import functools
 | 
						|
import logging
 | 
						|
import uuid
 | 
						|
import sys
 | 
						|
from datetime import datetime, timedelta
 | 
						|
 | 
						|
import odoo
 | 
						|
 | 
						|
from .exception import (NoSuchJobError,
 | 
						|
                        FailedJobError,
 | 
						|
                        RetryableJobError)
 | 
						|
 | 
						|
PENDING = 'pending'
 | 
						|
ENQUEUED = 'enqueued'
 | 
						|
DONE = 'done'
 | 
						|
STARTED = 'started'
 | 
						|
FAILED = 'failed'
 | 
						|
 | 
						|
STATES = [(PENDING, 'Pending'),
 | 
						|
          (ENQUEUED, 'Enqueued'),
 | 
						|
          (STARTED, 'Started'),
 | 
						|
          (DONE, 'Done'),
 | 
						|
          (FAILED, 'Failed')]
 | 
						|
 | 
						|
DEFAULT_PRIORITY = 10  # used by the PriorityQueue to sort the jobs
 | 
						|
DEFAULT_MAX_RETRIES = 5
 | 
						|
RETRY_INTERVAL = 10 * 60  # seconds
 | 
						|
 | 
						|
_logger = logging.getLogger(__name__)
 | 
						|
 | 
						|
 | 
						|
class DelayableRecordset(object):
 | 
						|
    """ Allow to delay a method for a recordset
 | 
						|
 | 
						|
    Usage::
 | 
						|
 | 
						|
        delayable = DelayableRecordset(recordset, priority=20)
 | 
						|
        delayable.method(args, kwargs)
 | 
						|
 | 
						|
    ``method`` must be a method of the recordset's Model, decorated with
 | 
						|
    :func:`~odoo.addons.queue_job.job.job`.
 | 
						|
 | 
						|
    The method call will be processed asynchronously in the job queue, with
 | 
						|
    the passed arguments.
 | 
						|
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, recordset, priority=None, eta=None,
 | 
						|
                 max_retries=None, description=None, channel=None):
 | 
						|
        self.recordset = recordset
 | 
						|
        self.priority = priority
 | 
						|
        self.eta = eta
 | 
						|
        self.max_retries = max_retries
 | 
						|
        self.description = description
 | 
						|
        self.channel = channel
 | 
						|
 | 
						|
    def __getattr__(self, name):
 | 
						|
        if name in self.recordset:
 | 
						|
            raise AttributeError(
 | 
						|
                'only methods can be delayed (%s called on %s)' %
 | 
						|
                (name, self.recordset)
 | 
						|
            )
 | 
						|
        recordset_method = getattr(self.recordset, name)
 | 
						|
        if not getattr(recordset_method, 'delayable', None):
 | 
						|
            raise AttributeError(
 | 
						|
                'method %s on %s is not allowed to be delayed, '
 | 
						|
                'it should be decorated with odoo.addons.queue_job.job.job' %
 | 
						|
                (name, self.recordset)
 | 
						|
            )
 | 
						|
 | 
						|
        def delay(*args, **kwargs):
 | 
						|
            return Job.enqueue(recordset_method,
 | 
						|
                               args=args,
 | 
						|
                               kwargs=kwargs,
 | 
						|
                               priority=self.priority,
 | 
						|
                               max_retries=self.max_retries,
 | 
						|
                               eta=self.eta,
 | 
						|
                               description=self.description,
 | 
						|
                               channel=self.channel)
 | 
						|
        return delay
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return "DelayableRecordset(%s%s)" % (
 | 
						|
            self.recordset._name,
 | 
						|
            getattr(self.recordset, '_ids', "")
 | 
						|
        )
 | 
						|
 | 
						|
    __repr__ = __str__
 | 
						|
 | 
						|
 | 
						|
class Job(object):
 | 
						|
    """ A Job is a task to execute.
 | 
						|
 | 
						|
    .. attribute:: uuid
 | 
						|
 | 
						|
        Id (UUID) of the job.
 | 
						|
 | 
						|
    .. attribute:: state
 | 
						|
 | 
						|
        State of the job, can pending, enqueued, started, done or failed.
 | 
						|
        The start state is pending and the final state is done.
 | 
						|
 | 
						|
    .. attribute:: retry
 | 
						|
 | 
						|
        The current try, starts at 0 and each time the job is executed,
 | 
						|
        it increases by 1.
 | 
						|
 | 
						|
    .. attribute:: max_retries
 | 
						|
 | 
						|
        The maximum number of retries allowed before the job is
 | 
						|
        considered as failed.
 | 
						|
 | 
						|
    .. attribute:: args
 | 
						|
 | 
						|
        Arguments passed to the function when executed.
 | 
						|
 | 
						|
    .. attribute:: kwargs
 | 
						|
 | 
						|
        Keyword arguments passed to the function when executed.
 | 
						|
 | 
						|
    .. attribute:: description
 | 
						|
 | 
						|
        Human description of the job.
 | 
						|
 | 
						|
    .. attribute:: func
 | 
						|
 | 
						|
        The python function itself.
 | 
						|
 | 
						|
    .. attribute:: model_name
 | 
						|
 | 
						|
        Odoo model on which the job will run.
 | 
						|
 | 
						|
    .. attribute:: priority
 | 
						|
 | 
						|
        Priority of the job, 0 being the higher priority.
 | 
						|
 | 
						|
    .. attribute:: date_created
 | 
						|
 | 
						|
        Date and time when the job was created.
 | 
						|
 | 
						|
    .. attribute:: date_enqueued
 | 
						|
 | 
						|
        Date and time when the job was enqueued.
 | 
						|
 | 
						|
    .. attribute:: date_started
 | 
						|
 | 
						|
        Date and time when the job was started.
 | 
						|
 | 
						|
    .. attribute:: date_done
 | 
						|
 | 
						|
        Date and time when the job was done.
 | 
						|
 | 
						|
    .. attribute:: result
 | 
						|
 | 
						|
        A description of the result (for humans).
 | 
						|
 | 
						|
    .. attribute:: exc_info
 | 
						|
 | 
						|
        Exception information (traceback) when the job failed.
 | 
						|
 | 
						|
    .. attribute:: user_id
 | 
						|
 | 
						|
        Odoo user id which created the job
 | 
						|
 | 
						|
    .. attribute:: eta
 | 
						|
 | 
						|
        Estimated Time of Arrival of the job. It will not be executed
 | 
						|
        before this date/time.
 | 
						|
 | 
						|
    .. attribute:: recordset
 | 
						|
 | 
						|
        Model recordset when we are on a delayed Model method
 | 
						|
 | 
						|
    .. attribute::channel
 | 
						|
 | 
						|
        The complete name of the channel to use to process the job. If
 | 
						|
        provided it overrides the one defined on the job's function.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def load(cls, env, job_uuid):
 | 
						|
        """ Read a job from the Database"""
 | 
						|
        stored = cls.db_record_from_uuid(env, job_uuid)
 | 
						|
        if not stored:
 | 
						|
            raise NoSuchJobError(
 | 
						|
                'Job %s does no longer exist in the storage.' % job_uuid)
 | 
						|
 | 
						|
        args = stored.args
 | 
						|
        kwargs = stored.kwargs
 | 
						|
        method_name = stored.method_name
 | 
						|
 | 
						|
        model = env[stored.model_name]
 | 
						|
        recordset = model.browse(stored.record_ids)
 | 
						|
        method = getattr(recordset, method_name)
 | 
						|
 | 
						|
        dt_from_string = odoo.fields.Datetime.from_string
 | 
						|
        eta = None
 | 
						|
        if stored.eta:
 | 
						|
            eta = dt_from_string(stored.eta)
 | 
						|
 | 
						|
        job_ = cls(method, args=args, kwargs=kwargs,
 | 
						|
                   priority=stored.priority, eta=eta, job_uuid=stored.uuid,
 | 
						|
                   description=stored.name, channel=stored.channel)
 | 
						|
 | 
						|
        if stored.date_created:
 | 
						|
            job_.date_created = dt_from_string(stored.date_created)
 | 
						|
 | 
						|
        if stored.date_enqueued:
 | 
						|
            job_.date_enqueued = dt_from_string(stored.date_enqueued)
 | 
						|
 | 
						|
        if stored.date_started:
 | 
						|
            job_.date_started = dt_from_string(stored.date_started)
 | 
						|
 | 
						|
        if stored.date_done:
 | 
						|
            job_.date_done = dt_from_string(stored.date_done)
 | 
						|
 | 
						|
        job_.state = stored.state
 | 
						|
        job_.result = stored.result if stored.result else None
 | 
						|
        job_.exc_info = stored.exc_info if stored.exc_info else None
 | 
						|
        job_.user_id = stored.user_id.id if stored.user_id else None
 | 
						|
        job_.model_name = stored.model_name if stored.model_name else None
 | 
						|
        job_.retry = stored.retry
 | 
						|
        job_.max_retries = stored.max_retries
 | 
						|
        if stored.company_id:
 | 
						|
            job_.company_id = stored.company_id.id
 | 
						|
        return job_
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def enqueue(cls, func, args=None, kwargs=None,
 | 
						|
                priority=None, eta=None, max_retries=None, description=None,
 | 
						|
                channel=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
 | 
						|
        from the ones to pass to the job function.
 | 
						|
 | 
						|
        """
 | 
						|
        new_job = cls(func=func, args=args,
 | 
						|
                      kwargs=kwargs, priority=priority, eta=eta,
 | 
						|
                      max_retries=max_retries, description=description,
 | 
						|
                      channel=channel)
 | 
						|
        new_job.store()
 | 
						|
        _logger.debug(
 | 
						|
            "enqueued %s:%s(*%r, **%r) with uuid: %s",
 | 
						|
            new_job.recordset,
 | 
						|
            new_job.method_name,
 | 
						|
            new_job.args,
 | 
						|
            new_job.kwargs,
 | 
						|
            new_job.uuid
 | 
						|
        )
 | 
						|
        return new_job
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def db_record_from_uuid(env, job_uuid):
 | 
						|
        model = env['queue.job'].sudo()
 | 
						|
        record = model.search([('uuid', '=', job_uuid)], limit=1)
 | 
						|
        return record.with_env(env)
 | 
						|
 | 
						|
    def __init__(self, func,
 | 
						|
                 args=None, kwargs=None, priority=None,
 | 
						|
                 eta=None, job_uuid=None, max_retries=None,
 | 
						|
                 description=None, channel=None):
 | 
						|
        """ Create a Job
 | 
						|
 | 
						|
        :param func: function to execute
 | 
						|
        :type func: function
 | 
						|
        :param args: arguments for func
 | 
						|
        :type args: tuple
 | 
						|
        :param kwargs: keyworkd arguments for func
 | 
						|
        :type kwargs: dict
 | 
						|
        :param priority: priority of the job,
 | 
						|
                         the smaller is the higher priority
 | 
						|
        :type priority: int
 | 
						|
        :param eta: the job can be executed only after this datetime
 | 
						|
                           (or now + timedelta)
 | 
						|
        :type eta: datetime or timedelta
 | 
						|
        :param job_uuid: UUID of the job
 | 
						|
        :param max_retries: maximum number of retries before giving up and set
 | 
						|
            the job state to 'failed'. A value of 0 means infinite retries.
 | 
						|
        :param description: human description of the job. If None, description
 | 
						|
            is computed from the function doc or name
 | 
						|
        :param channel: The complete channel name to use to process the job.
 | 
						|
        :param env: Odoo Environment
 | 
						|
        :type env: :class:`odoo.api.Environment`
 | 
						|
        """
 | 
						|
        if args is None:
 | 
						|
            args = ()
 | 
						|
        if isinstance(args, list):
 | 
						|
            args = tuple(args)
 | 
						|
        assert isinstance(args, tuple), "%s: args are not a tuple" % args
 | 
						|
        if kwargs is None:
 | 
						|
            kwargs = {}
 | 
						|
 | 
						|
        assert isinstance(kwargs, dict), "%s: kwargs are not a dict" % kwargs
 | 
						|
 | 
						|
        if not _is_model_method(func):
 | 
						|
            raise TypeError("Job accepts only methods of Models")
 | 
						|
 | 
						|
        recordset = func.__self__
 | 
						|
        env = recordset.env
 | 
						|
        self.model_name = recordset._name
 | 
						|
        self.method_name = func.__name__
 | 
						|
        self.recordset = recordset
 | 
						|
 | 
						|
        self.env = env
 | 
						|
        self.job_model = self.env['queue.job']
 | 
						|
        self.job_model_name = 'queue.job'
 | 
						|
 | 
						|
        self.state = PENDING
 | 
						|
 | 
						|
        self.retry = 0
 | 
						|
        if max_retries is None:
 | 
						|
            self.max_retries = DEFAULT_MAX_RETRIES
 | 
						|
        else:
 | 
						|
            self.max_retries = max_retries
 | 
						|
 | 
						|
        self._uuid = job_uuid
 | 
						|
 | 
						|
        self.args = args
 | 
						|
        self.kwargs = kwargs
 | 
						|
 | 
						|
        self.priority = priority
 | 
						|
        if self.priority is None:
 | 
						|
            self.priority = DEFAULT_PRIORITY
 | 
						|
 | 
						|
        self.date_created = datetime.now()
 | 
						|
        self._description = description
 | 
						|
 | 
						|
        self.date_enqueued = None
 | 
						|
        self.date_started = None
 | 
						|
        self.date_done = None
 | 
						|
 | 
						|
        self.result = None
 | 
						|
        self.exc_info = None
 | 
						|
 | 
						|
        self.user_id = env.uid
 | 
						|
        if 'company_id' in env.context:
 | 
						|
            company_id = env.context['company_id']
 | 
						|
        else:
 | 
						|
            company_model = env['res.company']
 | 
						|
            company_model = company_model.sudo(self.user_id)
 | 
						|
            company_id = company_model._company_default_get(
 | 
						|
                object='queue.job',
 | 
						|
                field='company_id'
 | 
						|
            ).id
 | 
						|
        self.company_id = company_id
 | 
						|
        self._eta = None
 | 
						|
        self.eta = eta
 | 
						|
        self.channel = channel
 | 
						|
 | 
						|
    def perform(self):
 | 
						|
        """ Execute the job.
 | 
						|
 | 
						|
        The job is executed with the user which has initiated it.
 | 
						|
        """
 | 
						|
        self.retry += 1
 | 
						|
        try:
 | 
						|
            self.result = self.func(*tuple(self.args), **self.kwargs)
 | 
						|
        except RetryableJobError as err:
 | 
						|
            if err.ignore_retry:
 | 
						|
                self.retry -= 1
 | 
						|
                raise
 | 
						|
            elif not self.max_retries:  # infinite retries
 | 
						|
                raise
 | 
						|
            elif self.retry >= self.max_retries:
 | 
						|
                type_, value, traceback = sys.exc_info()
 | 
						|
                # change the exception type but keep the original
 | 
						|
                # traceback and message:
 | 
						|
                # http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/
 | 
						|
                new_exc = FailedJobError("Max. retries (%d) reached: %s" %
 | 
						|
                                         (self.max_retries, value or type_)
 | 
						|
                                         )
 | 
						|
                raise new_exc from err
 | 
						|
            raise
 | 
						|
        return self.result
 | 
						|
 | 
						|
    def store(self):
 | 
						|
        """ Store the Job """
 | 
						|
        vals = {'state': self.state,
 | 
						|
                'priority': self.priority,
 | 
						|
                'retry': self.retry,
 | 
						|
                'max_retries': self.max_retries,
 | 
						|
                'exc_info': self.exc_info,
 | 
						|
                'user_id': self.user_id or self.env.uid,
 | 
						|
                'company_id': self.company_id,
 | 
						|
                'result': str(self.result) if self.result else False,
 | 
						|
                'date_enqueued': False,
 | 
						|
                'date_started': False,
 | 
						|
                'date_done': False,
 | 
						|
                'eta': False,
 | 
						|
                }
 | 
						|
 | 
						|
        dt_to_string = odoo.fields.Datetime.to_string
 | 
						|
        if self.date_enqueued:
 | 
						|
            vals['date_enqueued'] = dt_to_string(self.date_enqueued)
 | 
						|
        if self.date_started:
 | 
						|
            vals['date_started'] = dt_to_string(self.date_started)
 | 
						|
        if self.date_done:
 | 
						|
            vals['date_done'] = dt_to_string(self.date_done)
 | 
						|
        if self.eta:
 | 
						|
            vals['eta'] = dt_to_string(self.eta)
 | 
						|
 | 
						|
        db_record = self.db_record()
 | 
						|
        if db_record:
 | 
						|
            db_record.write(vals)
 | 
						|
        else:
 | 
						|
            date_created = dt_to_string(self.date_created)
 | 
						|
            # The following values must never be modified after the
 | 
						|
            # creation of the job
 | 
						|
            vals.update({'uuid': self.uuid,
 | 
						|
                         'name': self.description,
 | 
						|
                         'date_created': date_created,
 | 
						|
                         'model_name': self.model_name,
 | 
						|
                         'method_name': self.method_name,
 | 
						|
                         'record_ids': self.recordset.ids,
 | 
						|
                         'args': self.args,
 | 
						|
                         'kwargs': self.kwargs,
 | 
						|
                         })
 | 
						|
            # it the channel is not specified, lets the job_model compute
 | 
						|
            # the right one to use
 | 
						|
            if self.channel:
 | 
						|
                vals.update({'channel': self.channel})
 | 
						|
 | 
						|
            self.env[self.job_model_name].sudo().create(vals)
 | 
						|
 | 
						|
    def db_record(self):
 | 
						|
        return self.db_record_from_uuid(self.env, self.uuid)
 | 
						|
 | 
						|
    @property
 | 
						|
    def func(self):
 | 
						|
        recordset = self.recordset.with_context(job_uuid=self.uuid)
 | 
						|
        recordset = recordset.sudo(self.user_id)
 | 
						|
        return getattr(recordset, self.method_name)
 | 
						|
 | 
						|
    @property
 | 
						|
    def description(self):
 | 
						|
        if self._description:
 | 
						|
            return self._description
 | 
						|
        elif self.func.__doc__:
 | 
						|
            return self.func.__doc__.splitlines()[0].strip()
 | 
						|
        else:
 | 
						|
            return '%s.%s' % (self.model_name, self.func.__name__)
 | 
						|
 | 
						|
    @property
 | 
						|
    def uuid(self):
 | 
						|
        """Job ID, this is an UUID """
 | 
						|
        if self._uuid is None:
 | 
						|
            self._uuid = str(uuid.uuid4())
 | 
						|
        return self._uuid
 | 
						|
 | 
						|
    @property
 | 
						|
    def eta(self):
 | 
						|
        return self._eta
 | 
						|
 | 
						|
    @eta.setter
 | 
						|
    def eta(self, value):
 | 
						|
        if not value:
 | 
						|
            self._eta = None
 | 
						|
        elif isinstance(value, timedelta):
 | 
						|
            self._eta = datetime.now() + value
 | 
						|
        elif isinstance(value, int):
 | 
						|
            self._eta = datetime.now() + timedelta(seconds=value)
 | 
						|
        else:
 | 
						|
            self._eta = value
 | 
						|
 | 
						|
    def set_pending(self, result=None, reset_retry=True):
 | 
						|
        self.state = PENDING
 | 
						|
        self.date_enqueued = None
 | 
						|
        self.date_started = None
 | 
						|
        if reset_retry:
 | 
						|
            self.retry = 0
 | 
						|
        if result is not None:
 | 
						|
            self.result = result
 | 
						|
 | 
						|
    def set_enqueued(self):
 | 
						|
        self.state = ENQUEUED
 | 
						|
        self.date_enqueued = datetime.now()
 | 
						|
        self.date_started = None
 | 
						|
 | 
						|
    def set_started(self):
 | 
						|
        self.state = STARTED
 | 
						|
        self.date_started = datetime.now()
 | 
						|
 | 
						|
    def set_done(self, result=None):
 | 
						|
        self.state = DONE
 | 
						|
        self.exc_info = None
 | 
						|
        self.date_done = datetime.now()
 | 
						|
        if result is not None:
 | 
						|
            self.result = result
 | 
						|
 | 
						|
    def set_failed(self, exc_info=None):
 | 
						|
        self.state = FAILED
 | 
						|
        if exc_info is not None:
 | 
						|
            self.exc_info = exc_info
 | 
						|
 | 
						|
    def __repr__(self):
 | 
						|
        return '<Job %s, priority:%d>' % (self.uuid, self.priority)
 | 
						|
 | 
						|
    def _get_retry_seconds(self, seconds=None):
 | 
						|
        retry_pattern = self.func.retry_pattern
 | 
						|
        if not seconds and retry_pattern:
 | 
						|
            # ordered from higher to lower count of retries
 | 
						|
            patt = sorted(retry_pattern.items(), key=lambda t: t[0])
 | 
						|
            seconds = RETRY_INTERVAL
 | 
						|
            for retry_count, postpone_seconds in patt:
 | 
						|
                if self.retry >= retry_count:
 | 
						|
                    seconds = postpone_seconds
 | 
						|
                else:
 | 
						|
                    break
 | 
						|
        elif not seconds:
 | 
						|
            seconds = RETRY_INTERVAL
 | 
						|
        return seconds
 | 
						|
 | 
						|
    def postpone(self, result=None, seconds=None):
 | 
						|
        """ Write an estimated time arrival to n seconds
 | 
						|
        later than now. Used when an retryable exception
 | 
						|
        want to retry a job later. """
 | 
						|
        eta_seconds = self._get_retry_seconds(seconds)
 | 
						|
        self.eta = timedelta(seconds=eta_seconds)
 | 
						|
        self.exc_info = None
 | 
						|
        if result is not None:
 | 
						|
            self.result = result
 | 
						|
 | 
						|
    def related_action(self):
 | 
						|
        if not hasattr(self.func, 'related_action'):
 | 
						|
            return None
 | 
						|
        if not self.func.related_action:
 | 
						|
            return None
 | 
						|
        if not isinstance(self.func.related_action, str):
 | 
						|
            raise ValueError('related_action must be the name of the '
 | 
						|
                             'method on queue.job as string')
 | 
						|
        action = getattr(self.db_record(), self.func.related_action)
 | 
						|
        return action(**self.func.kwargs)
 | 
						|
 | 
						|
 | 
						|
def _is_model_method(func):
 | 
						|
    return (inspect.ismethod(func) and
 | 
						|
            isinstance(func.__self__.__class__, odoo.models.MetaModel))
 | 
						|
 | 
						|
 | 
						|
def job(func=None, default_channel='root', retry_pattern=None):
 | 
						|
    """ Decorator for jobs.
 | 
						|
 | 
						|
    Optional argument:
 | 
						|
 | 
						|
    :param default_channel: the channel wherein the job will be assigned. This
 | 
						|
                            channel is set at the installation of the module
 | 
						|
                            and can be manually changed later using the views.
 | 
						|
    :param retry_pattern: The retry pattern to use for postponing a job.
 | 
						|
                          If a job is postponed and there is no eta
 | 
						|
                          specified, the eta will be determined from the
 | 
						|
                          dict in retry_pattern. When no retry pattern
 | 
						|
                          is provided, jobs will be retried after
 | 
						|
                          :const:`RETRY_INTERVAL` seconds.
 | 
						|
    :type retry_pattern: dict(retry_count,retry_eta_seconds)
 | 
						|
 | 
						|
    Indicates that a method of a Model can be delayed in the Job Queue.
 | 
						|
 | 
						|
    When a method has the ``@job`` decorator, its calls can then be delayed
 | 
						|
    with::
 | 
						|
 | 
						|
        recordset.with_delay(priority=10).the_method(args, **kwargs)
 | 
						|
 | 
						|
    Where ``the_method`` is the method decorated with ``@job``. Its arguments
 | 
						|
    and keyword arguments will be kept in the Job Queue for its asynchronous
 | 
						|
    execution.
 | 
						|
 | 
						|
    ``default_channel`` indicates in which channel the job must be executed
 | 
						|
 | 
						|
    ``retry_pattern`` is a dict where keys are the count of retries and the
 | 
						|
    values are the delay to postpone a job.
 | 
						|
 | 
						|
    Example:
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        class ProductProduct(models.Model):
 | 
						|
            _inherit = 'product.product'
 | 
						|
 | 
						|
            @api.multi
 | 
						|
            @job
 | 
						|
            def export_one_thing(self, one_thing):
 | 
						|
                # work
 | 
						|
                # export one_thing
 | 
						|
 | 
						|
        # [...]
 | 
						|
 | 
						|
        env['a.model'].export_one_thing(the_thing_to_export)
 | 
						|
        # => normal and synchronous function call
 | 
						|
 | 
						|
        env['a.model'].with_delay().export_one_thing(the_thing_to_export)
 | 
						|
        # => the job will be executed as soon as possible
 | 
						|
 | 
						|
        delayable = env['a.model'].with_delay(priority=30, eta=60*60*5)
 | 
						|
        delayable.export_one_thing(the_thing_to_export)
 | 
						|
        # => the job will be executed with a low priority and not before a
 | 
						|
        # delay of 5 hours from now
 | 
						|
 | 
						|
        @job(default_channel='root.subchannel')
 | 
						|
        def export_one_thing(one_thing):
 | 
						|
            # work
 | 
						|
            # export one_thing
 | 
						|
 | 
						|
        @job(retry_pattern={1: 10 * 60,
 | 
						|
                            5: 20 * 60,
 | 
						|
                            10: 30 * 60,
 | 
						|
                            15: 12 * 60 * 60})
 | 
						|
        def retryable_example():
 | 
						|
            # 5 first retries postponed 10 minutes later
 | 
						|
            # retries 5 to 10 postponed 20 minutes later
 | 
						|
            # retries 10 to 15 postponed 30 minutes later
 | 
						|
            # all subsequent retries postponed 12 hours later
 | 
						|
            raise RetryableJobError('Must be retried later')
 | 
						|
 | 
						|
        env['a.model'].with_delay().retryable_example()
 | 
						|
 | 
						|
 | 
						|
    See also: :py:func:`related_action` a related action can be attached
 | 
						|
    to a job
 | 
						|
 | 
						|
    """
 | 
						|
    if func is None:
 | 
						|
        return functools.partial(job, default_channel=default_channel,
 | 
						|
                                 retry_pattern=retry_pattern)
 | 
						|
 | 
						|
    def delay_from_model(*args, **kwargs):
 | 
						|
        raise AttributeError(
 | 
						|
            "method.delay() can no longer be used, the general form is "
 | 
						|
            "env['res.users'].with_delay().method()"
 | 
						|
            )
 | 
						|
 | 
						|
    assert default_channel == 'root' or default_channel.startswith('root.'), (
 | 
						|
        "The channel path must start by 'root'")
 | 
						|
    assert retry_pattern is None or isinstance(retry_pattern, dict), (
 | 
						|
        "retry_pattern must be a dict"
 | 
						|
    )
 | 
						|
 | 
						|
    delay_func = delay_from_model
 | 
						|
 | 
						|
    func.delayable = True
 | 
						|
    func.delay = delay_func
 | 
						|
    func.retry_pattern = retry_pattern
 | 
						|
    func.default_channel = default_channel
 | 
						|
    return func
 | 
						|
 | 
						|
 | 
						|
def related_action(action=None, **kwargs):
 | 
						|
    """ Attach a *Related Action* to a job.
 | 
						|
 | 
						|
    A *Related Action* will appear as a button on the Odoo view.
 | 
						|
    The button will execute the action, usually it will open the
 | 
						|
    form view of the record related to the job.
 | 
						|
 | 
						|
    The ``action`` must be a method on the `queue.job` model.
 | 
						|
 | 
						|
    Example usage:
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        class QueueJob(models.Model):
 | 
						|
            _inherit = 'queue.job'
 | 
						|
 | 
						|
            @api.multi
 | 
						|
            def related_action_partner(self):
 | 
						|
                self.ensure_one()
 | 
						|
                model = self.model_name
 | 
						|
                partner = self.env[model].browse(self.record_ids)
 | 
						|
                # possibly get the real ID if partner_id is a binding ID
 | 
						|
                action = {
 | 
						|
                    'name': _("Partner"),
 | 
						|
                    'type': 'ir.actions.act_window',
 | 
						|
                    'res_model': model,
 | 
						|
                    'view_type': 'form',
 | 
						|
                    'view_mode': 'form',
 | 
						|
                    'res_id': partner.id,
 | 
						|
                }
 | 
						|
                return action
 | 
						|
 | 
						|
        class ResPartner(models.Model):
 | 
						|
            _inherit = 'res.partner'
 | 
						|
 | 
						|
            @api.multi
 | 
						|
            @job
 | 
						|
            @related_action(action='related_action_partner')
 | 
						|
            def export_partner(self):
 | 
						|
                # ...
 | 
						|
 | 
						|
    The kwargs are transmitted to the action:
 | 
						|
 | 
						|
    .. code-block:: python
 | 
						|
 | 
						|
        class QueueJob(models.Model):
 | 
						|
            _inherit = 'queue.job'
 | 
						|
 | 
						|
            @api.multi
 | 
						|
            def related_action_product(self, extra_arg=1):
 | 
						|
                assert extra_arg == 2
 | 
						|
                model = self.model_name
 | 
						|
                ...
 | 
						|
 | 
						|
        class ProductProduct(models.Model):
 | 
						|
            _inherit = 'product.product'
 | 
						|
 | 
						|
            @api.multi
 | 
						|
            @job
 | 
						|
            @related_action(action='related_action_product', extra_arg=2)
 | 
						|
            def export_product(self):
 | 
						|
                # ...
 | 
						|
 | 
						|
    """
 | 
						|
    def decorate(func):
 | 
						|
        func.related_action = action
 | 
						|
        func.kwargs = kwargs
 | 
						|
        return func
 | 
						|
    return decorate
 |