Skip to content
Open
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
5 changes: 2 additions & 3 deletions queue_job/jobrunner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import logging
from threading import Thread
import time

from odoo.service import server
from odoo.tools import config
Expand Down Expand Up @@ -40,7 +39,7 @@ def __init__(self):

def run(self):
# sleep a bit to let the workers start at ease
time.sleep(START_DELAY)
self.runner.sleep(START_DELAY)
self.runner.run()

def stop(self):
Expand Down Expand Up @@ -70,7 +69,7 @@ def process_work(self):
self.runner = QueueJobRunner.from_environ_or_config()
self._recover = False
_logger.debug("WorkerJobRunner (%s) starting up", self.pid)
time.sleep(START_DELAY)
self.runner.sleep(START_DELAY)
self.runner.run()

def signal_time_expired_handler(self, n, stack):
Expand Down
14 changes: 12 additions & 2 deletions queue_job/jobrunner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ def __init__(

self.db_by_name = {}
self._stop = False
self._stop_event = threading.Event()
self._stop_pipe = os.pipe()

def __del__(self):
Expand Down Expand Up @@ -811,9 +812,18 @@ def wait_notification(self):
continue
key.fileobj.poll()

def sleep(self, seconds):
"""Wait for ``seconds``, or less if :meth:`stop` is called meanwhile.

A plain ``time.sleep`` would hold the thread, and so the server
shutdown that joins it, for up to the whole delay.
"""
self._stop_event.wait(seconds)

def stop(self):
_logger.info("graceful stop requested")
self._stop = True
self._stop_event.set()
# wakeup the select() in wait_notification
os.write(self._stop_pipe[1], b".")

Expand Down Expand Up @@ -845,12 +855,12 @@ def run(self):
ERROR_RECOVERY_DELAY,
)
self.close_databases()
time.sleep(ERROR_RECOVERY_DELAY)
self.sleep(ERROR_RECOVERY_DELAY)
except Exception:
_logger.exception(
"exception: sleeping %ds and retrying", ERROR_RECOVERY_DELAY
)
self.close_databases()
time.sleep(ERROR_RECOVERY_DELAY)
self.sleep(ERROR_RECOVERY_DELAY)
self.close_databases(remove_jobs=False)
_logger.info("stopped")
35 changes: 34 additions & 1 deletion queue_job/tests/test_runner_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import doctest
import os
import threading
from unittest import mock

from odoo.tests import BaseCase, tagged
from odoo.tools import mute_logger

from odoo.addons.queue_job.jobrunner import runner
from odoo.addons.queue_job.jobrunner import START_DELAY, QueueJobRunnerThread, runner

# pylint: disable=odoo-addons-relative-import
# we are testing, we want to test as we were an external consumer of the API
Expand Down Expand Up @@ -112,6 +113,38 @@ def test_runner_file_closed_write_descriptor(self):
self.assertFalse(self._is_open_file_descriptor(read_fd))
self.assertFalse(self._is_open_file_descriptor(write_fd))

def test_runner_stop_interrupts_the_recovery_delay(self):
"""A runner that lost the master election stops without sleeping it out.

The server joins the runner thread on shutdown, so a stop waiting for
ERROR_RECOVERY_DELAY held the whole process for up to 5 seconds.
"""
a_runner = runner.QueueJobRunner.from_environ_or_config()
election_lost = threading.Event()

def initialize_databases():
election_lost.set()
raise runner.MasterElectionLost("held by another runner")

with mock.patch.object(
a_runner, "initialize_databases", side_effect=initialize_databases
):
thread = threading.Thread(target=a_runner.run, daemon=True)
thread.start()
self.addCleanup(thread.join)
self.assertTrue(election_lost.wait(timeout=5))
a_runner.stop()
thread.join(timeout=runner.ERROR_RECOVERY_DELAY / 2)
self.assertFalse(thread.is_alive())

def test_runner_thread_stop_interrupts_the_start_delay(self):
thread = QueueJobRunnerThread()
thread.start()
self.addCleanup(thread.join)
thread.stop()
thread.join(timeout=START_DELAY / 2)
self.assertFalse(thread.is_alive())

def test_max_capacity_from_env(self):
with (
mock.patch.dict(
Expand Down
Loading