Skip to content

Commit 499be9c

Browse files
committed
Merge pull request #52 from acsone/8.0-jobrunner-sbi
[8.0] job runner
2 parents 3055ea0 + 2193b7c commit 499be9c

26 files changed

+1610
-9
lines changed

connector/AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
* Leonardo Pistone at Camptocamp
1010
* David Béal at Akretion (tiny change)
1111
* Christophe Combelles at Anybox (french translation)
12+
* Stéphane Bidoul at Acsone (job runner)

connector/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
from . import connector
66
from . import producer
77
from . import checkpoint
8+
from . import controllers
9+
from . import jobrunner

connector/__openerp__.py

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
'category': 'Generic Modules',
2929
'depends': ['mail'
3030
],
31+
'external_dependencies': {'python': ['requests'
32+
],
33+
},
3134
'data': ['security/connector_security.xml',
3235
'security/ir.model.access.csv',
3336
'queue/model_view.xml',

connector/connector.py

+4
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ def install_in_connector():
8383
install_in_connector()
8484

8585

86+
def is_module_installed(pool, module_name):
87+
return bool(pool.get('%s.installed' % module_name))
88+
89+
8690
def get_openerp_module(cls_or_func):
8791
""" For a top level function or class, returns the
8892
name of the OpenERP module where it lives.

connector/connector_menu.xml

+12
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,24 @@
1313
name="Queue"
1414
parent="menu_connector_root"/>
1515

16+
<menuitem id="menu_queue_job_channel"
17+
action="action_queue_job_channel"
18+
sequence="12"
19+
parent="menu_queue"/>
20+
21+
<menuitem id="menu_queue_job_function"
22+
action="action_queue_job_function"
23+
sequence="14"
24+
parent="menu_queue"/>
25+
1626
<menuitem id="menu_queue_worker"
1727
action="action_queue_worker"
28+
sequence="16"
1829
parent="menu_queue"/>
1930

2031
<menuitem id="menu_queue_job"
2132
action="action_queue_job"
33+
sequence="18"
2234
parent="menu_queue"/>
2335

2436
<menuitem id="menu_checkpoint"

connector/controllers/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import main

connector/controllers/main.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import logging
2+
import traceback
3+
from cStringIO import StringIO
4+
5+
from psycopg2 import OperationalError
6+
7+
import openerp
8+
from openerp import http
9+
from openerp.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY
10+
11+
from ..session import ConnectorSessionHandler
12+
from ..queue.job import (OpenERPJobStorage,
13+
ENQUEUED)
14+
from ..exception import (NoSuchJobError,
15+
NotReadableJobError,
16+
RetryableJobError,
17+
FailedJobError,
18+
NothingToDoJob)
19+
20+
_logger = logging.getLogger(__name__)
21+
22+
PG_RETRY = 5 # seconds
23+
24+
25+
# TODO: perhaps the notion of ConnectionSession is less important
26+
# now that we are running jobs inside a normal Odoo worker
27+
28+
29+
class RunJobController(http.Controller):
30+
31+
job_storage_class = OpenERPJobStorage
32+
33+
def _load_job(self, session, job_uuid):
34+
""" Reload a job from the backend """
35+
try:
36+
job = self.job_storage_class(session).load(job_uuid)
37+
except NoSuchJobError:
38+
# just skip it
39+
job = None
40+
except NotReadableJobError:
41+
_logger.exception('Could not read job: %s', job_uuid)
42+
raise
43+
return job
44+
45+
@http.route('/connector/runjob', type='http', auth='none')
46+
def runjob(self, db, job_uuid, **kw):
47+
48+
session_hdl = ConnectorSessionHandler(db,
49+
openerp.SUPERUSER_ID)
50+
51+
def retry_postpone(job, message, seconds=None):
52+
with session_hdl.session() as session:
53+
job.postpone(result=message, seconds=seconds)
54+
job.set_pending(self)
55+
self.job_storage_class(session).store(job)
56+
57+
with session_hdl.session() as session:
58+
job = self._load_job(session, job_uuid)
59+
if job is None:
60+
return ""
61+
62+
try:
63+
# if the job has been manually set to DONE or PENDING,
64+
# or if something tries to run a job that is not enqueued
65+
# before its execution, stop
66+
if job.state != ENQUEUED:
67+
_logger.warning('job %s is in state %s '
68+
'instead of enqueued in /runjob',
69+
job.state, job_uuid)
70+
return
71+
72+
with session_hdl.session() as session:
73+
# TODO: set_started should be done atomically with
74+
# update queue_job set=state=started
75+
# where state=enqueid and id=
76+
job.set_started()
77+
self.job_storage_class(session).store(job)
78+
79+
_logger.debug('%s started', job)
80+
with session_hdl.session() as session:
81+
job.perform(session)
82+
job.set_done()
83+
self.job_storage_class(session).store(job)
84+
_logger.debug('%s done', job)
85+
86+
except NothingToDoJob as err:
87+
if unicode(err):
88+
msg = unicode(err)
89+
else:
90+
msg = None
91+
job.cancel(msg)
92+
with session_hdl.session() as session:
93+
self.job_storage_class(session).store(job)
94+
95+
except RetryableJobError as err:
96+
# delay the job later, requeue
97+
retry_postpone(job, unicode(err))
98+
_logger.debug('%s postponed', job)
99+
100+
except OperationalError as err:
101+
# Automatically retry the typical transaction serialization errors
102+
if err.pgcode not in PG_CONCURRENCY_ERRORS_TO_RETRY:
103+
raise
104+
retry_postpone(job, unicode(err), seconds=PG_RETRY)
105+
_logger.debug('%s OperationalError, postponed', job)
106+
107+
except (FailedJobError, Exception):
108+
buff = StringIO()
109+
traceback.print_exc(file=buff)
110+
_logger.error(buff.getvalue())
111+
112+
job.set_failed(exc_info=buff.getvalue())
113+
with session_hdl.session() as session:
114+
self.job_storage_class(session).store(job)
115+
raise
116+
117+
return ""

connector/doc/api/api_channels.rst

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
########
2+
Channels
3+
########
4+
5+
This is the API documentation for the job channels and the
6+
scheduling mechanisms of the job runner.
7+
8+
These classes are not intended for use by module developers.
9+
10+
.. automodule:: connector.jobrunner.channels
11+
:members:
12+
:undoc-members:
13+
:show-inheritance:
14+

connector/doc/api/api_runner.rst

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
##########
2+
Job Runner
3+
##########
4+
5+
This is the API documentation for the job runner.
6+
7+
These classes are not intended for use by module developers.
8+
9+
.. automodule:: connector.jobrunner.runner
10+
:members:
11+
:undoc-members:
12+
:show-inheritance:
13+

connector/doc/guides/jobrunner.rst

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.. _jobrunner:
2+
3+
4+
#######################################
5+
Configuring channels and the job runner
6+
#######################################
7+
8+
.. automodule:: connector.jobrunner.runner
9+
10+
What is a channel?
11+
------------------
12+
13+
.. autoclass:: connector.jobrunner.channels.Channel
14+
15+
How to configure Channels?
16+
--------------------------
17+
18+
The ``ODOO_CONNECTOR_CHANNELS`` environment variable must be
19+
set before starting Odoo in order to enable the job runner
20+
and configure the capacity of the channels.
21+
22+
The general syntax is ``channel(.subchannel)*(:capacity(:key(=value)?)*)?,...``.
23+
24+
Intermediate subchannels which are not configured explicitly are autocreated
25+
with an unlimited capacity (except the root channel which if not configured gets
26+
a default capacity of 1).
27+
28+
Example ``ODOO_CONNECTOR_CHANNELS``:
29+
30+
* ``root:4``: allow up to 4 concurrent jobs in the root channel.
31+
* ``root:4,root.sub:2``: allow up to 4 concurrent jobs in the root channel and
32+
up to 2 concurrent jobs in the channel named ``root.sub``.
33+
* ``sub:2``: the same.

connector/doc/guides/multiprocessing.rst

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
.. _multiprocessing:
22

33

4-
######################################
5-
Use the connector with multiprocessing
6-
######################################
4+
##############################################
5+
Use the connector with multiprocessing workers
6+
##############################################
7+
8+
.. note:: In a future version, workers will be deprecated
9+
in favor of the newer job runner which is more efficient and
10+
supports job channels. You should try the job runner first
11+
and fall back to using workers in case the runner does not
12+
work (sic) for you, in which case we will very much appreciate
13+
a github issue describing the problems you encountered.
714

815
When Odoo is launched with 1 process, the jobs worker will run
916
threaded in the same process.

connector/doc/index.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ability to be extended with additional modules for new features or
1616
customizations.
1717

1818
The development of Odoo Connector has been started by `Camptocamp`_ and is now
19-
maintained by `Camptocamp`_, `Akretion`_ and several :ref:`contributors`.
19+
maintained by `Camptocamp`_, `Akretion`_, `Acsone`_ and several :ref:`contributors`.
2020

2121
*Subscribe to the* `project's mailing list`_
2222

@@ -39,6 +39,7 @@ Core Features
3939
.. _Odoo: http://www.odoo.com
4040
.. _Camptocamp: http://www.camptocamp.com
4141
.. _Akretion: http://www.akretion.com
42+
.. _Acsone: http://www.acsone.eu
4243
.. _`source code is available on GitHub`: https://github.com/OCA/connector
4344
.. _`AGPL version 3`: http://www.gnu.org/licenses/agpl-3.0.html
4445
.. _`project's mailing list`: https://launchpad.net/~openerp-connector-community
@@ -112,6 +113,7 @@ Developer's guide
112113
guides/code_overview.rst
113114
guides/concepts.rst
114115
guides/bootstrap_connector.rst
116+
guides/jobrunner.rst
115117
guides/multiprocessing.rst
116118

117119
API Reference
@@ -130,6 +132,8 @@ API Reference
130132
api/api_backend_adapter.rst
131133
api/api_queue.rst
132134
api/api_exception.rst
135+
api/api_channels.rst
136+
api/api_runner.rst
133137

134138
******************
135139
Indices and tables

connector/exception.py

+4
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,7 @@ class IDMissingInBackend(JobError):
7575

7676
class ManyIDSInBackend(JobError):
7777
"""Unique key exists many times in backend"""
78+
79+
80+
class ChannelNotFound(ConnectorException):
81+
""" A channel could not be found """

connector/jobrunner/__init__.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf-8 -*-
2+
##############################################################################
3+
#
4+
# This file is part of connector, an Odoo module.
5+
#
6+
# Author: Stéphane Bidoul <stephane.bidoul@acsone.eu>
7+
# Copyright (c) 2015 ACSONE SA/NV (<http://acsone.eu>)
8+
#
9+
# connector is free software: you can redistribute it and/or
10+
# modify it under the terms of the GNU Affero General Public License
11+
# as published by the Free Software Foundation, either version 3 of
12+
# the License, or (at your option) any later version.
13+
#
14+
# connector is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU Affero General Public License for more details.
18+
#
19+
# You should have received a copy of the
20+
# GNU Affero General Public License
21+
# along with connector.
22+
# If not, see <http://www.gnu.org/licenses/>.
23+
#
24+
##############################################################################
25+
26+
import logging
27+
import os
28+
from threading import Thread
29+
import time
30+
31+
from openerp.service import server
32+
from openerp.tools import config
33+
34+
from .runner import ConnectorRunner
35+
36+
_logger = logging.getLogger(__name__)
37+
38+
START_DELAY = 5
39+
40+
41+
# Here we monkey patch the Odoo server to start the job runner thread
42+
# in the main server process (and not in forked workers). This is
43+
# very easy to deploy as we don't need another startup script.
44+
# The drawback is that it is not possible to extend the Odoo
45+
# server command line arguments, so we resort to environment variables
46+
# to configure the runner (channels mostly).
47+
48+
49+
enable = os.environ.get('ODOO_CONNECTOR_CHANNELS')
50+
51+
52+
def run():
53+
# sleep a bit to let the workers start at ease
54+
time.sleep(START_DELAY)
55+
port = os.environ.get('ODOO_CONNECTOR_PORT') or config['xmlrpc_port']
56+
channels = os.environ.get('ODOO_CONNECTOR_CHANNELS')
57+
runner = ConnectorRunner(port or 8069, channels or 'root:1')
58+
runner.run_forever()
59+
60+
61+
orig_prefork_start = server.PreforkServer.start
62+
orig_threaded_start = server.ThreadedServer.start
63+
orig_gevent_start = server.GeventServer.start
64+
65+
66+
def prefork_start(server, *args, **kwargs):
67+
res = orig_prefork_start(server, *args, **kwargs)
68+
if enable and not config['stop_after_init']:
69+
_logger.info("starting jobrunner thread (in prefork server)")
70+
thread = Thread(target=run)
71+
thread.daemon = True
72+
thread.start()
73+
return res
74+
75+
76+
def threaded_start(server, *args, **kwargs):
77+
res = orig_threaded_start(server, *args, **kwargs)
78+
if enable and not config['stop_after_init']:
79+
_logger.info("starting jobrunner thread (in threaded server)")
80+
thread = Thread(target=run)
81+
thread.daemon = True
82+
thread.start()
83+
return res
84+
85+
86+
def gevent_start(server, *args, **kwargs):
87+
res = orig_gevent_start(server, *args, **kwargs)
88+
if enable and not config['stop_after_init']:
89+
_logger.info("starting jobrunner thread (in gevent server)")
90+
# TODO: gevent spawn?
91+
raise RuntimeError("not implemented")
92+
return res
93+
94+
95+
server.PreforkServer.start = prefork_start
96+
server.ThreadedServer.start = threaded_start
97+
server.GeventServer.start = gevent_start

0 commit comments

Comments
 (0)