mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-21 15:32:02 +02:00
19.0 vanilla
This commit is contained in:
parent
0a7ae8db93
commit
991d2234ca
416 changed files with 646602 additions and 300844 deletions
|
|
@ -1,6 +1,8 @@
|
|||
#-----------------------------------------------------------
|
||||
# Threaded, Gevent and Prefork Servers
|
||||
#-----------------------------------------------------------
|
||||
import contextlib
|
||||
import collections
|
||||
import datetime
|
||||
import errno
|
||||
import logging
|
||||
|
|
@ -15,12 +17,12 @@ import subprocess
|
|||
import sys
|
||||
import threading
|
||||
import time
|
||||
import contextlib
|
||||
from email.utils import parsedate_to_datetime
|
||||
from collections import deque
|
||||
from io import BytesIO
|
||||
|
||||
import psutil
|
||||
import werkzeug.serving
|
||||
from werkzeug .urls import uri_to_iri
|
||||
|
||||
if os.name == 'posix':
|
||||
# Unix only for workers
|
||||
|
|
@ -52,18 +54,26 @@ try:
|
|||
except ImportError:
|
||||
setproctitle = lambda x: None
|
||||
|
||||
import odoo
|
||||
from odoo.modules import get_modules
|
||||
from odoo import api, sql_db
|
||||
from odoo.modules.registry import Registry
|
||||
from odoo.release import nt_service_name
|
||||
from odoo.tools import config
|
||||
from odoo.tools import config, gc, osutil, OrderedSet, profiler
|
||||
from odoo.tools.cache import log_ormcache_stats
|
||||
from odoo.tools.misc import stripped_sys_argv, dumpstacks
|
||||
from .db import list_dbs
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
SLEEP_INTERVAL = 60 # 1 min
|
||||
|
||||
|
||||
# A global-ish object, each thread/worker uses its own
|
||||
thread_local = threading.local()
|
||||
|
||||
# the model and method name that was called via rpc, for logging
|
||||
thread_local.rpc_model_method = ''
|
||||
|
||||
|
||||
def memory_info(process):
|
||||
"""
|
||||
:return: the relevant memory usage according to the OS in bytes.
|
||||
|
|
@ -80,6 +90,7 @@ def set_limit_memory_hard():
|
|||
if platform.system() != 'Linux':
|
||||
return
|
||||
limit_memory_hard = config['limit_memory_hard']
|
||||
import odoo # for eventd
|
||||
if odoo.evented and config['limit_memory_hard_gevent']:
|
||||
limit_memory_hard = config['limit_memory_hard_gevent']
|
||||
if limit_memory_hard:
|
||||
|
|
@ -95,6 +106,11 @@ def empty_pipe(fd):
|
|||
if e.errno not in [errno.EAGAIN]:
|
||||
raise
|
||||
|
||||
|
||||
def cron_database_list():
|
||||
return config['db_name'] or list_dbs(True)
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Werkzeug WSGI servers patched
|
||||
#----------------------------------------------------------
|
||||
|
|
@ -111,7 +127,7 @@ class BaseWSGIServerNoBind(LoggingBaseWSGIServerMixIn, werkzeug.serving.BaseWSGI
|
|||
use this class, sets the socket and calls the process_request() manually
|
||||
"""
|
||||
def __init__(self, app):
|
||||
werkzeug.serving.BaseWSGIServer.__init__(self, "127.0.0.1", 0, app)
|
||||
werkzeug.serving.BaseWSGIServer.__init__(self, "127.0.0.1", 0, app, handler=CommonRequestHandler)
|
||||
# Directly close the socket. It will be replaced by WorkerHTTP when processing requests
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
|
|
@ -120,16 +136,42 @@ class BaseWSGIServerNoBind(LoggingBaseWSGIServerMixIn, werkzeug.serving.BaseWSGI
|
|||
# dont listen as we use PreforkServer#socket
|
||||
pass
|
||||
|
||||
class CommonRequestHandler(werkzeug.serving.WSGIRequestHandler):
|
||||
def log_request(self, code = "-", size = "-"):
|
||||
try:
|
||||
path = uri_to_iri(self.path)
|
||||
fragment = thread_local.rpc_model_method
|
||||
if fragment:
|
||||
path += '#' + fragment
|
||||
msg = f"{self.command} {path} {self.request_version}"
|
||||
except AttributeError:
|
||||
# path isn't set if the requestline was bad
|
||||
msg = self.requestline
|
||||
|
||||
class RequestHandler(werkzeug.serving.WSGIRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._sent_date_header = None
|
||||
self._sent_server_header = None
|
||||
super().__init__(*args, **kwargs)
|
||||
code = str(code)
|
||||
|
||||
if code[0] == "1": # 1xx - Informational
|
||||
msg = werkzeug.serving._ansi_style(msg, "bold")
|
||||
elif code == "200": # 2xx - Success
|
||||
pass
|
||||
elif code == "304": # 304 - Resource Not Modified
|
||||
msg = werkzeug.serving._ansi_style(msg, "cyan")
|
||||
elif code[0] == "3": # 3xx - Redirection
|
||||
msg = werkzeug.serving._ansi_style(msg, "green")
|
||||
elif code == "404": # 404 - Resource Not Found
|
||||
msg = werkzeug.serving._ansi_style(msg, "yellow")
|
||||
elif code[0] == "4": # 4xx - Client Error
|
||||
msg = werkzeug.serving._ansi_style(msg, "bold", "red")
|
||||
else: # 5xx, or any other response
|
||||
msg = werkzeug.serving._ansi_style(msg, "bold", "magenta")
|
||||
|
||||
self.log("info", '"%s" %s %s', msg, code, size)
|
||||
|
||||
|
||||
class RequestHandler(CommonRequestHandler):
|
||||
def setup(self):
|
||||
# timeout to avoid chrome headless preconnect during tests
|
||||
if config['test_enable'] or config['test_file']:
|
||||
if config['test_enable']:
|
||||
self.timeout = 5
|
||||
# flag the current thread as handling a http request
|
||||
super(RequestHandler, self).setup()
|
||||
|
|
@ -155,33 +197,6 @@ class RequestHandler(werkzeug.serving.WSGIRequestHandler):
|
|||
# Do not keep processing requests.
|
||||
self.close_connection = True
|
||||
return
|
||||
|
||||
if keyword.casefold() == 'date':
|
||||
if self._sent_date_header is None:
|
||||
self._sent_date_header = value
|
||||
elif self._sent_date_header == value:
|
||||
return # don't send the same header twice
|
||||
else:
|
||||
sent_datetime = parsedate_to_datetime(self._sent_date_header)
|
||||
new_datetime = parsedate_to_datetime(value)
|
||||
if sent_datetime == new_datetime:
|
||||
return # don't send the same date twice (differ in format)
|
||||
if abs((sent_datetime - new_datetime).total_seconds()) <= 1:
|
||||
return # don't send the same date twice (jitter of 1 second)
|
||||
_logger.warning(
|
||||
"sending two different Date response headers: %r vs %r",
|
||||
self._sent_date_header, value)
|
||||
|
||||
if keyword.casefold() == 'server':
|
||||
if self._sent_server_header is None:
|
||||
self._sent_server_header = value
|
||||
elif self._sent_server_header == value:
|
||||
return # don't send the same header twice
|
||||
else:
|
||||
_logger.warning(
|
||||
"sending two different Server response headers: %r vs %r",
|
||||
self._sent_server_header, value)
|
||||
|
||||
super().send_header(keyword, value)
|
||||
|
||||
def end_headers(self, *a, **kw):
|
||||
|
|
@ -295,6 +310,7 @@ class FSWatcherBase(object):
|
|||
class FSWatcherWatchdog(FSWatcherBase):
|
||||
def __init__(self):
|
||||
self.observer = Observer()
|
||||
import odoo.addons # noqa: PLC0415
|
||||
for path in odoo.addons.__path__:
|
||||
_logger.info('Watching addons folder %s', path)
|
||||
self.observer.schedule(self, path, recursive=True)
|
||||
|
|
@ -321,6 +337,7 @@ class FSWatcherInotify(FSWatcherBase):
|
|||
inotify.adapters._LOGGER.setLevel(logging.ERROR)
|
||||
# recreate a list as InotifyTrees' __init__ deletes the list's items
|
||||
paths_to_watch = []
|
||||
import odoo.addons # noqa: PLC0415
|
||||
for path in odoo.addons.__path__:
|
||||
paths_to_watch.append(path)
|
||||
_logger.info('Watching addons folder %s', path)
|
||||
|
|
@ -489,7 +506,8 @@ class ThreadedServer(CommonServer):
|
|||
# just a bit prevents they all poll the database at the exact
|
||||
# same time. This is known as the thundering herd effect.
|
||||
|
||||
from odoo.addons.base.models.ir_cron import ir_cron
|
||||
from odoo.addons.base.models.ir_cron import IrCron # noqa: PLC0415
|
||||
|
||||
def _run_cron(cr):
|
||||
pg_conn = cr._cnx
|
||||
# LISTEN / NOTIFY doesn't work in recovery mode
|
||||
|
|
@ -500,25 +518,54 @@ class ThreadedServer(CommonServer):
|
|||
else:
|
||||
_logger.warning("PG cluster in recovery mode, cron trigger not activated")
|
||||
cr.commit()
|
||||
check_all_time = 0.0 # last time that we listed databases, initialized far in the past
|
||||
all_db_names = []
|
||||
alive_time = time.monotonic()
|
||||
while config['limit_time_worker_cron'] <= 0 or (time.monotonic() - alive_time) <= config['limit_time_worker_cron']:
|
||||
select.select([pg_conn], [], [], SLEEP_INTERVAL + number)
|
||||
time.sleep(number / 100)
|
||||
pg_conn.poll()
|
||||
try:
|
||||
pg_conn.poll()
|
||||
except Exception:
|
||||
if pg_conn.closed:
|
||||
# connection closed, just exit the loop
|
||||
return
|
||||
raise
|
||||
notified = OrderedSet(
|
||||
notif.payload
|
||||
for notif in pg_conn.notifies
|
||||
if notif.channel == 'cron_trigger'
|
||||
)
|
||||
pg_conn.notifies.clear() # free resources
|
||||
|
||||
if time.time() - SLEEP_INTERVAL > check_all_time:
|
||||
# check all databases
|
||||
# last time we checked them was `now - SLEEP_INTERVAL`
|
||||
check_all_time = time.time()
|
||||
# process notified databases first, then the other ones
|
||||
all_db_names = OrderedSet(cron_database_list())
|
||||
db_names = [
|
||||
*(db for db in notified if db in all_db_names),
|
||||
*(db for db in all_db_names if db not in notified),
|
||||
]
|
||||
else:
|
||||
# restrict to notified databases only
|
||||
db_names = notified.intersection(all_db_names)
|
||||
if not db_names:
|
||||
continue
|
||||
|
||||
_logger.debug('cron%d polling for jobs (notified: %s)', number, notified)
|
||||
for db_name in db_names:
|
||||
thread = threading.current_thread()
|
||||
thread.start_time = time.time()
|
||||
try:
|
||||
IrCron._process_jobs(db_name)
|
||||
except Exception:
|
||||
_logger.warning('cron%d encountered an Exception:', number, exc_info=True)
|
||||
thread.start_time = None
|
||||
|
||||
registries = odoo.modules.registry.Registry.registries
|
||||
_logger.debug('cron%d polling for jobs', number)
|
||||
for db_name, registry in registries.d.items():
|
||||
if registry.ready:
|
||||
thread = threading.current_thread()
|
||||
thread.start_time = time.time()
|
||||
try:
|
||||
ir_cron._process_jobs(db_name)
|
||||
except Exception:
|
||||
_logger.warning('cron%d encountered an Exception:', number, exc_info=True)
|
||||
thread.start_time = None
|
||||
while True:
|
||||
conn = odoo.sql_db.db_connect('postgres')
|
||||
conn = sql_db.db_connect('postgres')
|
||||
with contextlib.closing(conn.cursor()) as cr:
|
||||
_run_cron(cr)
|
||||
cr._cnx.close()
|
||||
|
|
@ -532,18 +579,12 @@ class ThreadedServer(CommonServer):
|
|||
threads it spawns are not marked daemon).
|
||||
|
||||
"""
|
||||
# Force call to strptime just before starting the cron thread
|
||||
# to prevent time.strptime AttributeError within the thread.
|
||||
# See: http://bugs.python.org/issue7980
|
||||
datetime.datetime.strptime('2012-01-01', '%Y-%m-%d')
|
||||
for i in range(odoo.tools.config['max_cron_threads']):
|
||||
def target():
|
||||
self.cron_thread(i)
|
||||
t = threading.Thread(target=target, name="odoo.service.cron.cron%d" % i)
|
||||
for i in range(config['max_cron_threads']):
|
||||
t = threading.Thread(target=self.cron_thread, args=(i,), name=f"odoo.service.cron.cron{i}")
|
||||
t.daemon = True
|
||||
t.type = 'cron'
|
||||
t.start()
|
||||
_logger.debug("cron%d started!" % i)
|
||||
_logger.debug("cron%d started!", i)
|
||||
|
||||
def http_spawn(self):
|
||||
self.httpd = ThreadedWSGIServerReloadable(self.interface, self.port, self.app)
|
||||
|
|
@ -564,12 +605,12 @@ class ThreadedServer(CommonServer):
|
|||
signal.signal(signal.SIGXCPU, self.signal_handler)
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
signal.signal(signal.SIGUSR2, log_ormcache_stats)
|
||||
elif os.name == 'nt':
|
||||
import win32api
|
||||
win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1)
|
||||
|
||||
test_mode = config['test_enable'] or config['test_file']
|
||||
if test_mode or (config['http_enable'] and not stop):
|
||||
if config['test_enable'] or (config['http_enable'] and not stop):
|
||||
# some tests need the http daemon to be available...
|
||||
self.http_spawn()
|
||||
|
||||
|
|
@ -606,7 +647,7 @@ class ThreadedServer(CommonServer):
|
|||
thread.join(0.05)
|
||||
time.sleep(0.05)
|
||||
|
||||
odoo.sql_db.close_all()
|
||||
sql_db.close_all()
|
||||
|
||||
_logger.debug('--')
|
||||
logging.shutdown()
|
||||
|
|
@ -625,7 +666,7 @@ class ThreadedServer(CommonServer):
|
|||
if config['test_enable']:
|
||||
from odoo.tests.result import _logger as logger # noqa: PLC0415
|
||||
with Registry.registries._lock:
|
||||
for db, registry in Registry.registries.d.items():
|
||||
for db, registry in Registry.registries.items():
|
||||
report = registry._assertion_report
|
||||
log = logger.error if not report.wasSuccessful() \
|
||||
else logger.warning if not report.testsRun \
|
||||
|
|
@ -758,6 +799,7 @@ class GeventServer(CommonServer):
|
|||
# Set process memory limit as an extra safeguard
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
signal.signal(signal.SIGUSR2, log_ormcache_stats)
|
||||
gevent.spawn(self.watchdog)
|
||||
|
||||
self.httpd = WSGIServer(
|
||||
|
|
@ -805,7 +847,7 @@ class PreforkServer(CommonServer):
|
|||
self.workers_cron = {}
|
||||
self.workers = {}
|
||||
self.generation = 0
|
||||
self.queue = []
|
||||
self.queue = collections.deque()
|
||||
self.long_polling_pid = None
|
||||
|
||||
def pipe_new(self):
|
||||
|
|
@ -868,13 +910,15 @@ class PreforkServer(CommonServer):
|
|||
def worker_kill(self, pid, sig):
|
||||
try:
|
||||
os.kill(pid, sig)
|
||||
if sig == signal.SIGKILL:
|
||||
self.worker_pop(pid)
|
||||
except OSError as e:
|
||||
if e.errno == errno.ESRCH:
|
||||
self.worker_pop(pid)
|
||||
|
||||
def process_signals(self):
|
||||
while len(self.queue):
|
||||
sig = self.queue.pop(0)
|
||||
while self.queue:
|
||||
sig = self.queue.popleft()
|
||||
if sig in [signal.SIGINT, signal.SIGTERM]:
|
||||
raise KeyboardInterrupt
|
||||
elif sig == signal.SIGHUP:
|
||||
|
|
@ -885,9 +929,9 @@ class PreforkServer(CommonServer):
|
|||
elif sig == signal.SIGQUIT:
|
||||
# dump stacks on kill -3
|
||||
dumpstacks()
|
||||
elif sig == signal.SIGUSR1:
|
||||
# log ormcache stats on kill -SIGUSR1
|
||||
log_ormcache_stats()
|
||||
elif sig in [signal.SIGUSR1, signal.SIGUSR2]:
|
||||
# log ormcache stats on kill -SIGUSR1 or kill -SIGUSR2
|
||||
log_ormcache_stats(sig)
|
||||
elif sig == signal.SIGTTIN:
|
||||
# increase number of workers
|
||||
self.population += 1
|
||||
|
|
@ -914,7 +958,7 @@ class PreforkServer(CommonServer):
|
|||
|
||||
def process_timeout(self):
|
||||
now = time.time()
|
||||
for (pid, worker) in self.workers.items():
|
||||
for (pid, worker) in list(self.workers.items()):
|
||||
if worker.watchdog_timeout is not None and \
|
||||
(now - worker.watchdog_time) >= worker.watchdog_timeout:
|
||||
_logger.error("%s (%s) timeout after %ss",
|
||||
|
|
@ -924,12 +968,29 @@ class PreforkServer(CommonServer):
|
|||
self.worker_kill(pid, signal.SIGKILL)
|
||||
|
||||
def process_spawn(self):
|
||||
# Before spawning any process, check the registry signaling
|
||||
registries = Registry.registries.snapshot
|
||||
|
||||
def check_registries():
|
||||
# check the registries on the first call only!
|
||||
if not registries:
|
||||
return
|
||||
for registry in registries.values():
|
||||
with registry.cursor() as cr:
|
||||
registry.check_signaling(cr)
|
||||
registries.clear()
|
||||
# Close all opened cursors
|
||||
sql_db.close_all()
|
||||
|
||||
if config['http_enable']:
|
||||
while len(self.workers_http) < self.population:
|
||||
check_registries()
|
||||
self.worker_spawn(WorkerHTTP, self.workers_http)
|
||||
if not self.long_polling_pid:
|
||||
check_registries()
|
||||
self.long_polling_spawn()
|
||||
while len(self.workers_cron) < config['max_cron_threads']:
|
||||
check_registries()
|
||||
self.worker_spawn(WorkerCron, self.workers_cron)
|
||||
|
||||
def sleep(self):
|
||||
|
|
@ -962,43 +1023,121 @@ class PreforkServer(CommonServer):
|
|||
signal.signal(signal.SIGTTOU, self.signal_handler)
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
signal.signal(signal.SIGUSR2, log_ormcache_stats)
|
||||
|
||||
if config['http_enable']:
|
||||
# listen to socket
|
||||
_logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port)
|
||||
family = socket.AF_INET
|
||||
if ':' in self.interface:
|
||||
family = socket.AF_INET6
|
||||
self.socket = socket.socket(family, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket.setblocking(0)
|
||||
self.socket.bind((self.interface, self.port))
|
||||
self.socket.listen(8 * self.population)
|
||||
if config.http_socket_activation:
|
||||
_logger.info('HTTP service (werkzeug) running through socket activation')
|
||||
else:
|
||||
_logger.info('HTTP service (werkzeug) running on %s:%s', self.interface, self.port)
|
||||
|
||||
if os.environ.get('ODOO_HTTP_SOCKET_FD'):
|
||||
# reload
|
||||
self.socket = socket.socket(fileno=int(os.environ.pop('ODOO_HTTP_SOCKET_FD')))
|
||||
elif config.http_socket_activation:
|
||||
# socket activation
|
||||
SD_LISTEN_FDS_START = 3
|
||||
self.socket = socket.fromfd(SD_LISTEN_FDS_START, socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
# default
|
||||
family = socket.AF_INET
|
||||
if ':' in self.interface:
|
||||
family = socket.AF_INET6
|
||||
self.socket = socket.socket(family, socket.SOCK_STREAM)
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.socket.setblocking(0)
|
||||
self.socket.bind((self.interface, self.port))
|
||||
self.socket.listen(8 * self.population)
|
||||
|
||||
def fork_and_reload(self):
|
||||
_logger.info("Reloading server")
|
||||
pid = os.fork()
|
||||
if pid != 0:
|
||||
# keep the http listening socket open during _reexec() to ensure uptime
|
||||
http_socket_fileno = self.socket.fileno()
|
||||
flags = fcntl.fcntl(http_socket_fileno, fcntl.F_GETFD)
|
||||
fcntl.fcntl(http_socket_fileno, fcntl.F_SETFD, flags & ~fcntl.FD_CLOEXEC)
|
||||
os.environ['ODOO_HTTP_SOCKET_FD'] = str(http_socket_fileno)
|
||||
os.environ['ODOO_READY_SIGHUP_PID'] = str(pid)
|
||||
_reexec() # stops execution
|
||||
|
||||
# child process handles old server shutdown
|
||||
_logger.info("Waiting for new server to start ...")
|
||||
phoenix_hatched = False
|
||||
|
||||
def sighup_handler(sig, frame):
|
||||
nonlocal phoenix_hatched
|
||||
phoenix_hatched = True
|
||||
|
||||
signal.signal(signal.SIGHUP, sighup_handler)
|
||||
|
||||
reload_timeout = time.monotonic() + 60
|
||||
while not phoenix_hatched and time.monotonic() < reload_timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
if not phoenix_hatched:
|
||||
_logger.error("Server reload timed out (check the updated code)")
|
||||
else:
|
||||
_logger.info("New server has started")
|
||||
|
||||
def stop_workers_gracefully(self):
|
||||
_logger.info("Stopping workers gracefully")
|
||||
|
||||
def stop(self, graceful=True):
|
||||
if self.long_polling_pid is not None:
|
||||
# FIXME make longpolling process handle SIGTERM correctly
|
||||
self.worker_kill(self.long_polling_pid, signal.SIGKILL)
|
||||
self.long_polling_pid = None
|
||||
|
||||
# Signal workers to finish their current workload then stop
|
||||
for pid in self.workers:
|
||||
self.worker_kill(pid, signal.SIGINT)
|
||||
|
||||
is_main_server = self.pid == os.getpid() # False if server reload, cannot reap children -> use psutil
|
||||
if not is_main_server:
|
||||
processes = {}
|
||||
for pid in self.workers:
|
||||
with contextlib.suppress(psutil.NoSuchProcess):
|
||||
processes[pid] = psutil.Process(pid)
|
||||
|
||||
self.beat = 0.1
|
||||
while self.workers:
|
||||
try:
|
||||
self.process_signals()
|
||||
except KeyboardInterrupt:
|
||||
_logger.info("Forced shutdown.")
|
||||
break
|
||||
|
||||
if is_main_server:
|
||||
self.process_zombie()
|
||||
else:
|
||||
for pid, proc in list(processes.items()):
|
||||
if not proc.is_running():
|
||||
self.worker_pop(pid)
|
||||
processes.pop(pid)
|
||||
|
||||
self.sleep()
|
||||
self.process_timeout()
|
||||
|
||||
def stop(self, graceful=True):
|
||||
global server_phoenix # noqa: PLW0603
|
||||
if server_phoenix:
|
||||
# PreforkServer reloads gracefully, disable outdated mechanism
|
||||
server_phoenix = False
|
||||
|
||||
self.fork_and_reload()
|
||||
self.stop_workers_gracefully()
|
||||
|
||||
_logger.info("Old server stopped")
|
||||
return
|
||||
|
||||
if self.socket:
|
||||
self.socket.close()
|
||||
if graceful:
|
||||
_logger.info("Stopping gracefully")
|
||||
super().stop()
|
||||
limit = time.time() + self.timeout
|
||||
for pid in self.workers:
|
||||
self.worker_kill(pid, signal.SIGINT)
|
||||
while self.workers and time.time() < limit:
|
||||
try:
|
||||
self.process_signals()
|
||||
except KeyboardInterrupt:
|
||||
_logger.info("Forced shutdown.")
|
||||
break
|
||||
self.process_zombie()
|
||||
time.sleep(0.1)
|
||||
self.stop_workers_gracefully()
|
||||
else:
|
||||
_logger.info("Stopping forcefully")
|
||||
for pid in self.workers:
|
||||
for pid in list(self.workers):
|
||||
self.worker_kill(pid, signal.SIGTERM)
|
||||
|
||||
def run(self, preload, stop):
|
||||
|
|
@ -1011,7 +1150,10 @@ class PreforkServer(CommonServer):
|
|||
return rc
|
||||
|
||||
# Empty the cursor pool, we dont want them to be shared among forked workers.
|
||||
odoo.sql_db.close_all()
|
||||
sql_db.close_all()
|
||||
|
||||
if os.environ.get('ODOO_READY_SIGHUP_PID'):
|
||||
os.kill(int(os.environ.pop('ODOO_READY_SIGHUP_PID')), signal.SIGHUP)
|
||||
|
||||
_logger.debug("Multiprocess starting")
|
||||
while 1:
|
||||
|
|
@ -1138,7 +1280,7 @@ class Worker(object):
|
|||
t.join()
|
||||
_logger.info("Worker (%s) exiting. request_count: %s, registry count: %s.",
|
||||
self.pid, self.request_count,
|
||||
len(odoo.modules.registry.Registry.registries))
|
||||
len(Registry.registries))
|
||||
self.stop()
|
||||
except Exception:
|
||||
_logger.exception("Worker (%s) Exception occurred, exiting...", self.pid)
|
||||
|
|
@ -1148,7 +1290,8 @@ class Worker(object):
|
|||
def _runloop(self):
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, {
|
||||
signal.SIGXCPU,
|
||||
signal.SIGINT, signal.SIGQUIT, signal.SIGUSR1,
|
||||
signal.SIGINT, signal.SIGQUIT,
|
||||
signal.SIGUSR1, signal.SIGUSR2,
|
||||
})
|
||||
try:
|
||||
while self.alive:
|
||||
|
|
@ -1211,15 +1354,15 @@ class WorkerCron(Worker):
|
|||
def __init__(self, multi):
|
||||
super(WorkerCron, self).__init__(multi)
|
||||
self.alive_time = time.monotonic()
|
||||
# process_work() below process a single database per call.
|
||||
# The variable db_index is keeping track of the next database to
|
||||
# process.
|
||||
self.db_index = 0
|
||||
self.watchdog_timeout = multi.cron_timeout # Use a distinct value for CRON Worker
|
||||
# process_work() below process a single database per call.
|
||||
# self.db_queue keeps track of the databases to process (in order, from left to right).
|
||||
self.db_queue: deque[str] = deque()
|
||||
self.db_count: int = 0
|
||||
|
||||
def sleep(self):
|
||||
# Really sleep once all the databases have been processed.
|
||||
if self.db_index == 0:
|
||||
if not self.db_queue:
|
||||
interval = SLEEP_INTERVAL + self.pid % 10 # chorus effect
|
||||
|
||||
# simulate interruptible sleep with select(wakeup_fd, timeout)
|
||||
|
|
@ -1240,35 +1383,45 @@ class WorkerCron(Worker):
|
|||
_logger.info('WorkerCron (%s) max age (%ss) reached.', self.pid, config['limit_time_worker_cron'])
|
||||
self.alive = False
|
||||
|
||||
def _db_list(self):
|
||||
if config['db_name']:
|
||||
db_names = config['db_name'].split(',')
|
||||
else:
|
||||
db_names = odoo.service.db.list_dbs(True)
|
||||
return db_names
|
||||
|
||||
def process_work(self):
|
||||
"""Process a single database."""
|
||||
_logger.debug("WorkerCron (%s) polling for jobs", self.pid)
|
||||
db_names = self._db_list()
|
||||
if len(db_names):
|
||||
self.db_index = (self.db_index + 1) % len(db_names)
|
||||
db_name = db_names[self.db_index]
|
||||
self.setproctitle(db_name)
|
||||
|
||||
from odoo.addons import base
|
||||
base.models.ir_cron.ir_cron._process_jobs(db_name)
|
||||
if not self.db_queue:
|
||||
# list databases
|
||||
db_names = OrderedSet(cron_database_list())
|
||||
pg_conn = self.dbcursor._cnx
|
||||
notified = OrderedSet(
|
||||
notif.payload
|
||||
for notif in pg_conn.notifies
|
||||
if notif.channel == 'cron_trigger'
|
||||
)
|
||||
pg_conn.notifies.clear() # free resources
|
||||
# add notified databases (in order) first in the queue
|
||||
self.db_queue.extend(db for db in notified if db in db_names)
|
||||
self.db_queue.extend(db for db in db_names if db not in notified)
|
||||
self.db_count = len(self.db_queue)
|
||||
if not self.db_count:
|
||||
return
|
||||
|
||||
# dont keep cursors in multi database mode
|
||||
if len(db_names) > 1:
|
||||
odoo.sql_db.close_db(db_name)
|
||||
# pop the leftmost element (because notified databases appear first)
|
||||
db_name = self.db_queue.popleft()
|
||||
self.setproctitle(db_name)
|
||||
|
||||
self.request_count += 1
|
||||
if self.request_count >= self.request_max and self.request_max < len(db_names):
|
||||
_logger.error("There are more dabatases to process than allowed "
|
||||
"by the `limit_request` configuration variable: %s more.",
|
||||
len(db_names) - self.request_max)
|
||||
else:
|
||||
self.db_index = 0
|
||||
from odoo.addons.base.models.ir_cron import IrCron # noqa: PLC0415
|
||||
IrCron._process_jobs(db_name)
|
||||
|
||||
# dont keep cursors in multi database mode
|
||||
if self.db_count > 1:
|
||||
sql_db.close_db(db_name)
|
||||
|
||||
self.request_count += 1
|
||||
if self.request_count >= self.request_max and self.request_max < self.db_count:
|
||||
_logger.error(
|
||||
"There are more dabatases to process than allowed "
|
||||
"by the `limit_request` configuration variable: %s more.",
|
||||
self.db_count - self.request_max,
|
||||
)
|
||||
|
||||
def start(self):
|
||||
os.nice(10) # mommy always told me to be nice with others...
|
||||
|
|
@ -1276,7 +1429,7 @@ class WorkerCron(Worker):
|
|||
if self.multi.socket:
|
||||
self.multi.socket.close()
|
||||
|
||||
dbconn = odoo.sql_db.db_connect('postgres')
|
||||
dbconn = sql_db.db_connect('postgres')
|
||||
self.dbcursor = dbconn.cursor()
|
||||
# LISTEN / NOTIFY doesn't work in recovery mode
|
||||
self.dbcursor.execute("SELECT pg_is_in_recovery()")
|
||||
|
|
@ -1299,23 +1452,25 @@ class WorkerCron(Worker):
|
|||
server = None
|
||||
server_phoenix = False
|
||||
|
||||
|
||||
def load_server_wide_modules():
|
||||
server_wide_modules = list(odoo.conf.server_wide_modules)
|
||||
server_wide_modules.extend(m for m in ('base', 'web') if m not in server_wide_modules)
|
||||
for m in server_wide_modules:
|
||||
try:
|
||||
odoo.modules.module.load_openerp_module(m)
|
||||
except Exception:
|
||||
msg = ''
|
||||
if m == 'web':
|
||||
msg = """
|
||||
The `web` module is provided by the addons found in the `openerp-web` project.
|
||||
Maybe you forgot to add those addons in your addons_path configuration."""
|
||||
_logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
|
||||
from odoo.modules.module import load_openerp_module # noqa: PLC0415
|
||||
with gc.disabling_gc():
|
||||
for m in config['server_wide_modules']:
|
||||
try:
|
||||
load_openerp_module(m)
|
||||
except Exception:
|
||||
msg = ''
|
||||
if m == 'web':
|
||||
msg = """
|
||||
The `web` module is provided by the addons found in the `openerp-web` project.
|
||||
Maybe you forgot to add those addons in your addons_path configuration."""
|
||||
_logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
|
||||
|
||||
|
||||
def _reexec(updated_modules=None):
|
||||
"""reexecute openerp-server process with (nearly) the same arguments"""
|
||||
if odoo.tools.osutil.is_running_as_nt_service():
|
||||
if osutil.is_running_as_nt_service():
|
||||
subprocess.call('net stop {0} && net start {0}'.format(nt_service_name), shell=True)
|
||||
exe = os.path.basename(sys.executable)
|
||||
args = stripped_sys_argv()
|
||||
|
|
@ -1327,74 +1482,56 @@ def _reexec(updated_modules=None):
|
|||
os.execve(sys.executable, args, os.environ)
|
||||
|
||||
|
||||
def load_test_file_py(registry, test_file):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from odoo.tests import loader # noqa: PLC0415
|
||||
from odoo.tests.suite import OdooSuite # noqa: PLC0415
|
||||
threading.current_thread().testing = True
|
||||
try:
|
||||
test_path, _ = os.path.splitext(os.path.abspath(test_file))
|
||||
for mod in [m for m in get_modules() if '%s%s%s' % (os.path.sep, m, os.path.sep) in test_file]:
|
||||
for mod_mod in loader.get_test_modules(mod):
|
||||
mod_path, _ = os.path.splitext(getattr(mod_mod, '__file__', ''))
|
||||
if test_path == config._normalize(mod_path):
|
||||
tests = loader.get_module_test_cases(mod_mod)
|
||||
suite = OdooSuite(tests)
|
||||
_logger.log(logging.INFO, 'running tests %s.', mod_mod.__name__)
|
||||
suite(registry._assertion_report)
|
||||
if not registry._assertion_report.wasSuccessful():
|
||||
_logger.error('%s: at least one error occurred in a test', test_file)
|
||||
return
|
||||
finally:
|
||||
threading.current_thread().testing = False
|
||||
|
||||
|
||||
def preload_registries(dbnames):
|
||||
""" Preload a registries, possibly run a test file."""
|
||||
# TODO: move all config checks to args dont check tools.config here
|
||||
dbnames = dbnames or []
|
||||
rc = 0
|
||||
|
||||
preload_profiler = contextlib.nullcontext()
|
||||
|
||||
for dbname in dbnames:
|
||||
if os.environ.get('ODOO_PROFILE_PRELOAD'):
|
||||
interval = float(os.environ.get('ODOO_PROFILE_PRELOAD_INTERVAL', '0.1'))
|
||||
collectors = [profiler.PeriodicCollector(interval=interval)]
|
||||
if os.environ.get('ODOO_PROFILE_PRELOAD_SQL'):
|
||||
collectors.append('sql')
|
||||
preload_profiler = profiler.Profiler(db=dbname, collectors=collectors)
|
||||
try:
|
||||
update_module = config['init'] or config['update']
|
||||
threading.current_thread().dbname = dbname
|
||||
registry = Registry.new(dbname, update_module=update_module)
|
||||
with preload_profiler:
|
||||
threading.current_thread().dbname = dbname
|
||||
update_from_config = update_module = config['init'] or config['update'] or config['reinit']
|
||||
if not update_module:
|
||||
with sql_db.db_connect(dbname).cursor() as cr:
|
||||
cr.execute("SELECT 1 FROM ir_module_module WHERE state IN ('to remove', 'to upgrade', 'to install') FETCH FIRST 1 ROW ONLY")
|
||||
update_module = bool(cr.rowcount)
|
||||
|
||||
# run test_file if provided
|
||||
if config['test_file']:
|
||||
test_file = config['test_file']
|
||||
if not os.path.isfile(test_file):
|
||||
_logger.warning('test file %s cannot be found', test_file)
|
||||
elif not test_file.endswith('py'):
|
||||
_logger.warning('test file %s is not a python file', test_file)
|
||||
else:
|
||||
_logger.info('loading test file %s', test_file)
|
||||
load_test_file_py(registry, test_file)
|
||||
registry = Registry.new(dbname, update_module=update_module, install_modules=config['init'], upgrade_modules=config['update'], reinit_modules=config['reinit'])
|
||||
|
||||
# run post-install tests
|
||||
if config['test_enable']:
|
||||
from odoo.tests import loader # noqa: PLC0415
|
||||
t0 = time.time()
|
||||
t0_sql = odoo.sql_db.sql_counter
|
||||
module_names = (registry.updated_modules if update_module else
|
||||
sorted(registry._init_modules))
|
||||
_logger.info("Starting post tests")
|
||||
tests_before = registry._assertion_report.testsRun
|
||||
post_install_suite = loader.make_suite(module_names, 'post_install')
|
||||
if post_install_suite.has_http_case():
|
||||
with registry.cursor() as cr:
|
||||
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
env['ir.qweb']._pregenerate_assets_bundles()
|
||||
result = loader.run_suite(post_install_suite, global_report=registry._assertion_report)
|
||||
registry._assertion_report.update(result)
|
||||
_logger.info("%d post-tests in %.2fs, %s queries",
|
||||
registry._assertion_report.testsRun - tests_before,
|
||||
time.time() - t0,
|
||||
odoo.sql_db.sql_counter - t0_sql)
|
||||
# run post-install tests
|
||||
if config['test_enable']:
|
||||
from odoo.tests import loader # noqa: PLC0415
|
||||
t0 = time.time()
|
||||
t0_sql = sql_db.sql_counter
|
||||
module_names = (registry.updated_modules if update_from_config else
|
||||
sorted(registry._init_modules))
|
||||
_logger.info("Starting post tests")
|
||||
tests_before = registry._assertion_report.testsRun
|
||||
post_install_suite = loader.make_suite(module_names, 'post_install')
|
||||
if post_install_suite.has_http_case():
|
||||
with registry.cursor() as cr:
|
||||
env = api.Environment(cr, api.SUPERUSER_ID, {})
|
||||
env['ir.qweb']._pregenerate_assets_bundles()
|
||||
result = loader.run_suite(post_install_suite, global_report=registry._assertion_report)
|
||||
registry._assertion_report.update(result)
|
||||
_logger.info("%d post-tests in %.2fs, %s queries",
|
||||
registry._assertion_report.testsRun - tests_before,
|
||||
time.time() - t0,
|
||||
sql_db.sql_counter - t0_sql)
|
||||
|
||||
registry._assertion_report.log_stats()
|
||||
if registry._assertion_report and not registry._assertion_report.wasSuccessful():
|
||||
rc += 1
|
||||
registry._assertion_report.log_stats()
|
||||
if registry._assertion_report and not registry._assertion_report.wasSuccessful():
|
||||
rc += 1
|
||||
except Exception:
|
||||
_logger.critical('Failed to initialize database `%s`.', dbname, exc_info=True)
|
||||
return -1
|
||||
|
|
@ -1406,11 +1543,12 @@ def start(preload=None, stop=False):
|
|||
global server
|
||||
|
||||
load_server_wide_modules()
|
||||
import odoo.http # noqa: PLC0415
|
||||
|
||||
if odoo.evented:
|
||||
server = GeventServer(odoo.http.root)
|
||||
elif config['workers']:
|
||||
if config['test_enable'] or config['test_file']:
|
||||
if config['test_enable']:
|
||||
_logger.warning("Unit testing in workers mode could fail; use --workers 0.")
|
||||
|
||||
server = PreforkServer(odoo.http.root)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue