Skip to content
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

New module odoosh_queue_job for running vanilla queue_job on odoo.sh #562

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions odoosh_queue_job/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#### Functionality

This module allows safe usage of OCA's queue_job on odoo.sh with multiple http workers.

In short: It disables the jobrunner on cron workers and uses a leader election system on
the http workers so only one jobrunner is active at any given time.

#### Usage

1. Add this repository as a submodule in your odoo.sh instance

2. Include the module in the server_wide_modules config parameter of your odoo.conf file

```
[options]
server_wide_modules=web,odoosh_queue_job
```

3. Do not forget to add your host in the same file for each environment

```
[queue_job]
scheme=https
host=your-odoo-sh-branch-domain.odoo.com
port=443
```

4. Restart odoo

```
odoosh-restart http
```
12 changes: 12 additions & 0 deletions odoosh_queue_job/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import logging

_logger = logging.getLogger(__name__)

try:
from odoo.addons.queue_job import jobrunner
from odoo.addons.queue_job.jobrunner.channels import NOT_DONE

# Only import monkeypatch if the jobrunner is available
from . import monkeypatch
except Exception as ex:
_logger.error("Could not initialize - %s", ex)

Check warning on line 12 in odoosh_queue_job/__init__.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/__init__.py#L11-L12

Added lines #L11 - L12 were not covered by tests
9 changes: 9 additions & 0 deletions odoosh_queue_job/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Queue job for odoo.sh",
"author": "Odoo Community Association (OCA), Pledra",
"license": "AGPL-3",
"website": "https://github.com/OCA/queue",
"category": "Generic Modules",
"version": "16.0.0.0.0",
"depends": ["base", "queue_job"],
}
183 changes: 183 additions & 0 deletions odoosh_queue_job/monkeypatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import logging
import time
import uuid
from contextlib import closing

import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT

from odoo.tools import config

from odoo.addons.queue_job import jobrunner
from odoo.addons.queue_job.jobrunner.channels import NOT_DONE

LEADER_CHECK_DELAY = 10

_logger = logging.getLogger(__name__)

# Keeping the old reference to the original function
original__start_runner_thread = jobrunner._start_runner_thread
original__connection_info_for = jobrunner.runner._connection_info_for
ERROR_RECOVERY_DELAY = jobrunner.runner.ERROR_RECOVERY_DELAY
Database = jobrunner.runner.Database


def _start_runner_thread(self):
"""
Prevent jobrunner from initializing on odoo.sh cron workers
"""

# Odoo.sh cron workers always have limit_time_real_cron and
# limit_time_real_cron set to 0 so we use this to identify them
if config["limit_time_real_cron"] == 0 and config["limit_time_real"] == 0:
_logger.info("Odoo.sh cron worker detected, stopping jobrunner")
return
original__start_runner_thread(self)

Check warning on line 35 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L33-L35

Added lines #L33 - L35 were not covered by tests


def _connection_info_for(db_name, uuid=False):
"""Inherit method to add the application_name to the connection info"""
connection_info = original__connection_info_for(db_name)

Check warning on line 40 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L40

Added line #L40 was not covered by tests
if uuid:
connection_info["application_name"] = "jobrunner_%s" % uuid
return connection_info

Check warning on line 43 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L42-L43

Added lines #L42 - L43 were not covered by tests


# DATABASE Class methods modified
def _init__(self, db_name):
"""Overriding Database __init__ to add a uuid to the connection info"""

self.db_name = db_name

Check warning on line 50 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L50

Added line #L50 was not covered by tests
# Upstream code
# connection_info = _connection_info_for(db_name)

# Pledra customization starts here
self.uuid = str(uuid.uuid4())
connection_info = _connection_info_for(db_name, self.uuid)

Check warning on line 56 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L55-L56

Added lines #L55 - L56 were not covered by tests
# Pledra customization ends here
self.conn = psycopg2.connect(**connection_info)
_logger.info("jobrunner initialized with uuid %s", self.uuid)
self.conn.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
self.has_queue_job = self._has_queue_job()

Check warning on line 61 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L58-L61

Added lines #L58 - L61 were not covered by tests
# Upstream code
# if self.has_queue_job:
# self._initialize()


def check_leader(self):
"""Method to check if the current jobrunner is the leader"""
with closing(self.conn.cursor()) as cr:
cr.execute(

Check warning on line 70 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L70

Added line #L70 was not covered by tests
"""
SELECT substring(application_name FROM 'jobrunner_(.*)')
FROM pg_stat_activity
WHERE application_name LIKE 'jobrunner_%'
ORDER BY backend_start
LIMIT 1;
"""
)
leader_uuid = cr.fetchone()[0]

Check warning on line 79 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L79

Added line #L79 was not covered by tests
if leader_uuid != self.uuid:
_logger.info(

Check warning on line 81 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L81

Added line #L81 was not covered by tests
"jobrunner %s: not leader of %s. leader: %s. sleeping %s sec.",
self.uuid,
self.db_name,
leader_uuid,
LEADER_CHECK_DELAY,
)

return False
_logger.info(

Check warning on line 90 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L89-L90

Added lines #L89 - L90 were not covered by tests
"jobrunner %s is the leader of db %s",
self.uuid,
self.db_name,
)
return True
return False

Check warning on line 96 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L95-L96

Added lines #L95 - L96 were not covered by tests


# QueueJobRunner class methods modified
def setup_databases(self):
"""Method split from the initialize_database for already created jobs"""
for db in self.db_by_name.values():
if not db.has_queue_job:
continue
db._initialize()

Check warning on line 105 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L104-L105

Added lines #L104 - L105 were not covered by tests
with db.select_jobs("state in %s", (NOT_DONE,)) as cr:
for job_data in cr:
self.channel_manager.notify(db.db_name, *job_data)
_logger.info("queue job runner ready for db %s", db.db_name)

Check warning on line 109 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L108-L109

Added lines #L108 - L109 were not covered by tests


def initialize_databases(self):
for db_name in self.get_db_names():
db = Database(db_name)

Check warning on line 114 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L114

Added line #L114 was not covered by tests
if db.has_queue_job:
self.db_by_name[db_name] = db

Check warning on line 116 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L116

Added line #L116 was not covered by tests
# Upstream code
# with db.select_jobs("state in %s", (NOT_DONE,)) as cr:
# for job_data in cr:
# self.channel_manager.notify(db.db_name, *job_data)
# _logger.info("queue job runner ready for db %s", db.db_name)


def db_check_leader(self):
leader = False

Check warning on line 125 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L125

Added line #L125 was not covered by tests
for db in self.db_by_name.values():
leader = db.check_leader()

Check warning on line 127 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L127

Added line #L127 was not covered by tests
if leader:
break
return leader

Check warning on line 130 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L129-L130

Added lines #L129 - L130 were not covered by tests


def run(self):
_logger.info("starting")

Check warning on line 134 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L134

Added line #L134 was not covered by tests
while not self._stop:
# outer loop does exception recovery
try:
_logger.info("initializing database connections")

Check warning on line 138 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L137-L138

Added lines #L137 - L138 were not covered by tests
# TODO: how to detect new databases or databases
# on which queue_job is installed after server start?

# Pledra Cust starts here
self.initialize_databases()

Check warning on line 143 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L143

Added line #L143 was not covered by tests
while not self._stop:
leader = self.db_check_leader()

Check warning on line 145 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L145

Added line #L145 was not covered by tests
if not leader:
time.sleep(LEADER_CHECK_DELAY)
continue

Check warning on line 148 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L147-L148

Added lines #L147 - L148 were not covered by tests
else:
break
self.setup_databases()

Check warning on line 151 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L150-L151

Added lines #L150 - L151 were not covered by tests
# Pledra Cust ends here
_logger.info("database connections ready")

Check warning on line 153 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L153

Added line #L153 was not covered by tests
# inner loop does the normal processing
while not self._stop:
self.process_notifications()
self.run_jobs()
self.wait_notification()

Check warning on line 158 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L156-L158

Added lines #L156 - L158 were not covered by tests
except KeyboardInterrupt:
self.stop()

Check warning on line 160 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L160

Added line #L160 was not covered by tests
except InterruptedError:
# Interrupted system call, i.e. KeyboardInterrupt during select
self.stop()
except Exception:
_logger.exception(

Check warning on line 165 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L163-L165

Added lines #L163 - L165 were not covered by tests
"exception: sleeping %ds and retrying", ERROR_RECOVERY_DELAY
)
self.close_databases()
time.sleep(ERROR_RECOVERY_DELAY)
self.close_databases(remove_jobs=False)
_logger.info("stopped")

Check warning on line 171 in odoosh_queue_job/monkeypatch.py

View check run for this annotation

Codecov / codecov/patch

odoosh_queue_job/monkeypatch.py#L168-L171

Added lines #L168 - L171 were not covered by tests


jobrunner._start_runner_thread = _start_runner_thread
jobrunner.runner._connection_info_for = _connection_info_for

jobrunner.runner.Database.__init__ = _init__
jobrunner.runner.Database.check_leader = check_leader

jobrunner.runner.QueueJobRunner.run = run
jobrunner.runner.QueueJobRunner.initialize_databases = initialize_databases
jobrunner.runner.QueueJobRunner.db_check_leader = db_check_leader
jobrunner.runner.QueueJobRunner.setup_databases = setup_databases
1 change: 1 addition & 0 deletions setup/odoosh_queue_job/odoo/addons/odoosh_queue_job
6 changes: 6 additions & 0 deletions setup/odoosh_queue_job/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
Loading