diff --git a/README.md b/README.md index cbbb7f2f7fb..c67867ef405 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ addon | version | maintainers | summary [base_technical_user](base_technical_user/) | 17.0.1.0.0 | | Add a technical user parameter on the company [base_view_inheritance_extension](base_view_inheritance_extension/) | 17.0.1.1.0 | | Adds more operators for view inheritance [base_write_diff](base_write_diff/) | 17.0.1.0.0 | | Prevents updates on fields whose values won't change anyway +[bus_alt_connection](bus_alt_connection/) | 17.0.1.0.0 | | Needed when using PgBouncer as a connection pooler [database_cleanup](database_cleanup/) | 17.0.1.2.2 | | Database cleanup [dbfilter_from_header](dbfilter_from_header/) | 17.0.1.0.0 | | Filter databases with HTTP headers [fetchmail_attach_from_folder](fetchmail_attach_from_folder/) | 17.0.1.0.0 | NL66278 | Attach mails in an IMAP folder to existing objects diff --git a/bus_alt_connection/README.rst b/bus_alt_connection/README.rst new file mode 100644 index 00000000000..8582cda2193 --- /dev/null +++ b/bus_alt_connection/README.rst @@ -0,0 +1,190 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================== +Bus Alt Connection +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9092d793f2788f2bbcdc84ddcf93925fd13f90c570b2b79eeb92e304bcff5c9b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/17.0/bus_alt_connection + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-17-0/server-tools-17-0-bus_alt_connection + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module makes it possible to use +`PgBouncer `__ as a connection pooler for +odoo. + +Why isn't odoo's connection pooling good enough? +------------------------------------------------ + +Odoo's builtin connection pooling works at process level: each Odoo +process has its own +`ConnectionPool `__, +limited to ``db_maxconn``. + +It does the job of re-using open connections available in the pool. But +it never closes these connections, `unless reaching +db_maxconn `__. + +In practice, we observe that each odoo worker will end up with up to 3 +open connection in its pool. With 10 http workers, that's up to 30 +connection continuously open just for one single instance. + +Here comes PgBouncer +-------------------- + +PgBouncer will help to limit this number of open connections, by sharing +a pool of connections at the instance level, between all workers. Odoo +workers will still have up to 3 open connections, but these will be +connections to PgBouncer, that on its side will close unnecessary +connections to pg. + +This has proven to help performances on Odoo deployments with multiple +instances. + +It allows you to define how resources should be shared, according to +your priorities, e.g. : + +- key odoo instance on host A can open up to 30 connections +- while odoo instance on host B, dedicated to reports, can open up to 10 + connections only + +And most importantly, it helps you to ensure that ``max_connections`` +will never be reached on pg server side. + +Why is this module needed? +-------------------------- + +When configuring PgBouncer, you can choose between 2 transaction pooling +modes: + +- pool_mode = session +- pool_mode = transaction + +If we choose pool_mode = session, then one server connection will be +tied to a given odoo process until its death, which is exactly what +we're trying to change. Thus, to release the server connection once the +transaction is complete, we use pool_mode = transaction. + +This works fine, except for Odoo's longpolling features that relies on +the +`LISTEN/NOTIFY `__ +mechanism from pg, which is `not +compatible `__ with that +mode. + +To be more precise, NOTIFY statements are properly transfered by +PgBouncer in that mode; only the LISTEN statement isn't (because it +needs to keep the server connection open). + +So for the unique "listening" connection per instance that requires this +statement +(`here `__), +we need odoo to connect directly to the pg server, bypassing PgBouncer. + +That's what this module implements, by overriding the relevant method of +the +`Dispatcher `__. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +You don't need to install this module in the database(s) to enable it. + +But you need to load it server-wide: + +- By starting Odoo with ``--load=web,bus_alt_connection`` +- Or by updating its configuration file: + +.. code:: ini + + [options] + (...) + server_wide_modules = web,bus_alt_connection + +Configuration +============= + +You need to define how to connect directly to the database: + +- Either by defining environment variables: + + - ``IMDISPATCHER_DB_HOST=db-01`` + - ``IMDISPATCHER_DB_PORT=5432`` + +- Or in Odoo's configuration file: + +.. code:: ini + + [options] + (...) + imdispatcher_db_host = db-01 + imdispatcher_db_port = 5432 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Trobz + +Contributors +------------ + +- Nils Hamerlinck +- Ioan Galan + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/bus_alt_connection/__init__.py b/bus_alt_connection/__init__.py new file mode 100644 index 00000000000..1bcfe0e2f29 --- /dev/null +++ b/bus_alt_connection/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Trobz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import models diff --git a/bus_alt_connection/__manifest__.py b/bus_alt_connection/__manifest__.py new file mode 100644 index 00000000000..69be7f6c6f6 --- /dev/null +++ b/bus_alt_connection/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2019 Trobz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Bus Alt Connection", + "summary": "Needed when using PgBouncer as a connection pooler", + "version": "17.0.1.0.0", + "author": "Trobz,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "category": "Extra Tools", + "license": "AGPL-3", + "depends": ["bus"], + "installable": True, + "auto_install": False, +} diff --git a/bus_alt_connection/i18n/bus_alt_connection.pot b/bus_alt_connection/i18n/bus_alt_connection.pot new file mode 100644 index 00000000000..716a0702d88 --- /dev/null +++ b/bus_alt_connection/i18n/bus_alt_connection.pot @@ -0,0 +1,13 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" diff --git a/bus_alt_connection/models/__init__.py b/bus_alt_connection/models/__init__.py new file mode 100644 index 00000000000..3dc9bd99358 --- /dev/null +++ b/bus_alt_connection/models/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Trobz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import bus diff --git a/bus_alt_connection/models/bus.py b/bus_alt_connection/models/bus.py new file mode 100644 index 00000000000..310748466e5 --- /dev/null +++ b/bus_alt_connection/models/bus.py @@ -0,0 +1,67 @@ +# Copyright 2019 Trobz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json +import logging +import os +import selectors + +import psycopg2 + +import odoo +from odoo.tools import config + +import odoo.addons.bus.models.bus +from odoo.addons.bus.models.bus import TIMEOUT, hashable, stop_event + +_logger = logging.getLogger(__name__) + + +def _connection_info_for(db_name): + db_or_uri, connection_info = odoo.sql_db.connection_info_for(db_name) + + for p in ("host", "port"): + cfg = os.environ.get("ODOO_IMDISPATCHER_DB_%s" % p.upper()) or config.get( + "imdispatcher_db_" + p + ) + if cfg: + connection_info[p] = cfg + return connection_info + + +class ImDispatch(odoo.addons.bus.models.bus.ImDispatch): + def loop(self): + """Dispatch postgres notifications to the relevant + polling threads/greenlets""" + connection_info = _connection_info_for("postgres") + _logger.info( + "Bus.loop listen imbus on db postgres " "(via %(host)s:%(port)s)", + connection_info, + ) + conn = psycopg2.connect(**connection_info) + with conn.cursor() as cr, selectors.DefaultSelector() as sel: + cr.execute("listen imbus") + conn.commit() + sel.register(conn, selectors.EVENT_READ) + while not stop_event.is_set(): + if sel.select(TIMEOUT): + conn.poll() + channels = [] + while conn.notifies: + channels.extend(json.loads(conn.notifies.pop().payload)) + # relay notifications to websockets that have + # subscribed to the corresponding channels. + websockets = set() + for channel in channels: + websockets.update( + self._channels_to_ws.get(hashable(channel), []) + ) + for websocket in websockets: + websocket.trigger_notification_dispatching() + + +odoo.addons.bus.models.bus.ImDispatch = ImDispatch +dispatch = ImDispatch() +odoo.addons.bus.models.bus.dispatch = dispatch +odoo.addons.bus.models.ir_websocket.dispatch = dispatch +odoo.addons.bus.websocket.dispatch = dispatch diff --git a/bus_alt_connection/pyproject.toml b/bus_alt_connection/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/bus_alt_connection/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/bus_alt_connection/readme/CONFIGURE.md b/bus_alt_connection/readme/CONFIGURE.md new file mode 100644 index 00000000000..06479bff9af --- /dev/null +++ b/bus_alt_connection/readme/CONFIGURE.md @@ -0,0 +1,15 @@ +You need to define how to connect directly to the database: + +- Either by defining environment variables: + + > - `IMDISPATCHER_DB_HOST=db-01` + > - `IMDISPATCHER_DB_PORT=5432` + +- Or in Odoo's configuration file: + +``` ini +[options] +(...) +imdispatcher_db_host = db-01 +imdispatcher_db_port = 5432 +``` diff --git a/bus_alt_connection/readme/CONTRIBUTORS.md b/bus_alt_connection/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..ba8a581494e --- /dev/null +++ b/bus_alt_connection/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Nils Hamerlinck \<\> +- Ioan Galan \<\> diff --git a/bus_alt_connection/readme/DESCRIPTION.md b/bus_alt_connection/readme/DESCRIPTION.md new file mode 100644 index 00000000000..1784d3fb88e --- /dev/null +++ b/bus_alt_connection/readme/DESCRIPTION.md @@ -0,0 +1,71 @@ +This module makes it possible to use +[PgBouncer](https://pgbouncer.github.io/) as a connection pooler for +odoo. + +## Why isn't odoo's connection pooling good enough? + +Odoo's builtin connection pooling works at process level: each Odoo +process has its own +[ConnectionPool](https://github.com/odoo/odoo/blob/12.0/odoo/sql_db.py#L525), +limited to `db_maxconn`. + +It does the job of re-using open connections available in the pool. But +it never closes these connections, [unless reaching +db_maxconn](https://github.com/odoo/odoo/blob/12.0/odoo/sql_db.py#L593). + +In practice, we observe that each odoo worker will end up with up to 3 +open connection in its pool. With 10 http workers, that's up to 30 +connection continuously open just for one single instance. + +## Here comes PgBouncer + +PgBouncer will help to limit this number of open connections, by sharing +a pool of connections at the instance level, between all workers. Odoo +workers will still have up to 3 open connections, but these will be +connections to PgBouncer, that on its side will close unnecessary +connections to pg. + +This has proven to help performances on Odoo deployments with multiple +instances. + +It allows you to define how resources should be shared, according to +your priorities, e.g. : + +- key odoo instance on host A can open up to 30 connections +- while odoo instance on host B, dedicated to reports, can open up to 10 + connections only + +And most importantly, it helps you to ensure that `max_connections` will +never be reached on pg server side. + +## Why is this module needed? + +When configuring PgBouncer, you can choose between 2 transaction pooling +modes: + +- pool_mode = session +- pool_mode = transaction + +If we choose pool_mode = session, then one server connection will be +tied to a given odoo process until its death, which is exactly what +we're trying to change. Thus, to release the server connection once the +transaction is complete, we use pool_mode = transaction. + +This works fine, except for Odoo's longpolling features that relies on +the +[LISTEN/NOTIFY](https://www.postgresql.org/docs/9.6/static/sql-notify.html) +mechanism from pg, which is [not +compatible](https://wiki.postgresql.org/wiki/PgBouncer) with that mode. + +To be more precise, NOTIFY statements are properly transfered by +PgBouncer in that mode; only the LISTEN statement isn't (because it +needs to keep the server connection open). + +So for the unique "listening" connection per instance that requires this +statement +([here](https://github.com/odoo/odoo/blob/12.0/addons/bus/models/bus.py#L166)), +we need odoo to connect directly to the pg server, bypassing PgBouncer. + +That's what this module implements, by overriding the relevant method of +the +[Dispatcher](https://github.com/odoo/odoo/blob/12.0/addons/bus/models/bus.py#L105). diff --git a/bus_alt_connection/readme/INSTALL.md b/bus_alt_connection/readme/INSTALL.md new file mode 100644 index 00000000000..f5532609239 --- /dev/null +++ b/bus_alt_connection/readme/INSTALL.md @@ -0,0 +1,12 @@ +You don't need to install this module in the database(s) to enable it. + +But you need to load it server-wide: + +- By starting Odoo with `--load=web,bus_alt_connection` +- Or by updating its configuration file: + +``` ini +[options] +(...) +server_wide_modules = web,bus_alt_connection +``` diff --git a/bus_alt_connection/static/description/icon.png b/bus_alt_connection/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/bus_alt_connection/static/description/icon.png differ diff --git a/bus_alt_connection/static/description/index.html b/bus_alt_connection/static/description/index.html new file mode 100644 index 00000000000..7e5a997513d --- /dev/null +++ b/bus_alt_connection/static/description/index.html @@ -0,0 +1,526 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Bus Alt Connection

+ +

Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runboat

+

This module makes it possible to use +PgBouncer as a connection pooler for +odoo.

+
+

Why isn’t odoo’s connection pooling good enough?

+

Odoo’s builtin connection pooling works at process level: each Odoo +process has its own +ConnectionPool, +limited to db_maxconn.

+

It does the job of re-using open connections available in the pool. But +it never closes these connections, unless reaching +db_maxconn.

+

In practice, we observe that each odoo worker will end up with up to 3 +open connection in its pool. With 10 http workers, that’s up to 30 +connection continuously open just for one single instance.

+
+
+

Here comes PgBouncer

+

PgBouncer will help to limit this number of open connections, by sharing +a pool of connections at the instance level, between all workers. Odoo +workers will still have up to 3 open connections, but these will be +connections to PgBouncer, that on its side will close unnecessary +connections to pg.

+

This has proven to help performances on Odoo deployments with multiple +instances.

+

It allows you to define how resources should be shared, according to +your priorities, e.g. :

+
    +
  • key odoo instance on host A can open up to 30 connections
  • +
  • while odoo instance on host B, dedicated to reports, can open up to 10 +connections only
  • +
+

And most importantly, it helps you to ensure that max_connections +will never be reached on pg server side.

+
+
+

Why is this module needed?

+

When configuring PgBouncer, you can choose between 2 transaction pooling +modes:

+
    +
  • pool_mode = session
  • +
  • pool_mode = transaction
  • +
+

If we choose pool_mode = session, then one server connection will be +tied to a given odoo process until its death, which is exactly what +we’re trying to change. Thus, to release the server connection once the +transaction is complete, we use pool_mode = transaction.

+

This works fine, except for Odoo’s longpolling features that relies on +the +LISTEN/NOTIFY +mechanism from pg, which is not +compatible with that +mode.

+

To be more precise, NOTIFY statements are properly transfered by +PgBouncer in that mode; only the LISTEN statement isn’t (because it +needs to keep the server connection open).

+

So for the unique “listening” connection per instance that requires this +statement +(here), +we need odoo to connect directly to the pg server, bypassing PgBouncer.

+

That’s what this module implements, by overriding the relevant method of +the +Dispatcher.

+

Table of contents

+ +
+

Installation

+

You don’t need to install this module in the database(s) to enable it.

+

But you need to load it server-wide:

+
    +
  • By starting Odoo with --load=web,bus_alt_connection
  • +
  • Or by updating its configuration file:
  • +
+
+[options]
+(...)
+server_wide_modules = web,bus_alt_connection
+
+
+
+

Configuration

+

You need to define how to connect directly to the database:

+
    +
  • Either by defining environment variables:

    +
    +
      +
    • IMDISPATCHER_DB_HOST=db-01
    • +
    • IMDISPATCHER_DB_PORT=5432
    • +
    +
    +
  • +
  • Or in Odoo’s configuration file:

    +
  • +
+
+[options]
+(...)
+imdispatcher_db_host = db-01
+imdispatcher_db_port = 5432
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+ +
+
+

Authors

+
    +
  • Trobz
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/bus_alt_connection/tests/__init__.py b/bus_alt_connection/tests/__init__.py new file mode 100644 index 00000000000..378f73032e3 --- /dev/null +++ b/bus_alt_connection/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_bus_alt_connection diff --git a/bus_alt_connection/tests/test_bus_alt_connection.py b/bus_alt_connection/tests/test_bus_alt_connection.py new file mode 100644 index 00000000000..363d68879a0 --- /dev/null +++ b/bus_alt_connection/tests/test_bus_alt_connection.py @@ -0,0 +1,206 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import threading +from unittest import mock + +from odoo.tests.common import TransactionCase + +from ..models import bus as bus_alt + + +class _FakeNotify: + def __init__(self, payload): + self.payload = payload + + +class _FakeCursor: + def __init__(self): + self.execute = mock.Mock() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + +class _FakeConnection: + def __init__(self, notifies=None): + self._cursor = _FakeCursor() + self.commit = mock.Mock() + self.poll = mock.Mock() + self.notifies = list(notifies or []) + + def cursor(self): + return self._cursor + + +class _FakeSelector: + def __init__(self, select_returns=None, on_select=None): + self._select_returns = list(select_returns or []) + self._on_select = on_select + self.register = mock.Mock() + self.select_calls = 0 + + def select(self, timeout=None): + self.select_calls += 1 + if self._on_select: + self._on_select(timeout) + if self._select_returns: + return self._select_returns.pop(0) + return [] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + +class TestBusAltConnection(TransactionCase): + def test01_connection_info_config_overrides(self): + with mock.patch( + "odoo.addons.bus_alt_connection.models.bus.odoo.sql_db.connection_info_for" + ) as connection_info_for, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.config.get" + ) as config_get: + connection_info_for.return_value = ( + None, + { + "host": "pgbouncer", + "port": 6432, + "database": "postgres", + "user": "odoo", + }, + ) + config_get.side_effect = lambda key: { + "imdispatcher_db_host": "direct-db", + "imdispatcher_db_port": 5432, + }.get(key) + info = bus_alt._connection_info_for("postgres") + assert info["host"] == "direct-db" + assert info["port"] == 5432 + assert info["database"] == "postgres" + + def test02_connection_info_env_takes_precedence(self): + with mock.patch.dict( + bus_alt.os.environ, + { + "ODOO_IMDISPATCHER_DB_HOST": "env-db", + "ODOO_IMDISPATCHER_DB_PORT": "15432", + }, + clear=False, + ), mock.patch( + "odoo.addons.bus_alt_connection.models.bus.odoo.sql_db.connection_info_for" + ) as connection_info_for, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.config.get" + ) as config_get: + connection_info_for.return_value = ( + None, + { + "host": "pgbouncer", + "port": 6432, + "database": "postgres", + "user": "odoo", + }, + ) + config_get.side_effect = lambda key: { + "imdispatcher_db_host": "direct-db", + "imdispatcher_db_port": 5432, + }.get(key) + info = bus_alt._connection_info_for("postgres") + assert info["host"] == "env-db" + # env vars come as strings + assert info["port"] == "15432" + + def test03_connection_info_no_override_keeps_original(self): + with mock.patch.dict( + bus_alt.os.environ, + { + "ODOO_IMDISPATCHER_DB_HOST": "", + "ODOO_IMDISPATCHER_DB_PORT": "", + }, + clear=False, + ), mock.patch( + "odoo.addons.bus_alt_connection.models.bus.odoo.sql_db.connection_info_for" + ) as connection_info_for, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.config.get" + ) as config_get: + connection_info_for.return_value = ( + None, + { + "host": "pgbouncer", + "port": 6432, + "database": "postgres", + "user": "odoo", + }, + ) + config_get.return_value = None + info = bus_alt._connection_info_for("postgres") + assert info["host"] == "pgbouncer" + assert info["port"] == 6432 + + def test04_loop_dispatches_unique_websockets(self): + stop_event = threading.Event() + ws1 = mock.Mock() + ws2 = mock.Mock() + channel_1 = ("db", "ch1") + channel_2 = ("db", "ch2") + notifies = [ + _FakeNotify('[["db", "ch1"]]'), + _FakeNotify('[["db", "ch1"], ["db", "ch2"]]'), + ] + fake_conn = _FakeConnection(notifies=notifies) + fake_selector = _FakeSelector( + select_returns=[[object()]], + on_select=lambda _timeout: stop_event.set(), + ) + dispatch = bus_alt.ImDispatch() + dispatch._channels_to_ws = { + bus_alt.hashable(channel_1): [ws1], + bus_alt.hashable(channel_2): [ws1, ws2], + } + with mock.patch( + "odoo.addons.bus_alt_connection.models.bus.stop_event", stop_event + ), mock.patch( + "odoo.addons.bus_alt_connection.models.bus._connection_info_for" + ) as connection_info_for, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.psycopg2.connect" + ) as connect, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.selectors.DefaultSelector" + ) as DefaultSelector: + connection_info_for.return_value = {"host": "h", "port": 1} + connect.return_value = fake_conn + DefaultSelector.return_value = fake_selector + + dispatch.loop() + + fake_conn._cursor.execute.assert_called_once_with("listen imbus") + fake_conn.commit.assert_called_once() + fake_conn.poll.assert_called_once() + ws1.trigger_notification_dispatching.assert_called_once() + ws2.trigger_notification_dispatching.assert_called_once() + + def test05_loop_no_activity_does_not_poll(self): + stop_event = threading.Event() + fake_conn = _FakeConnection(notifies=[]) + fake_selector = _FakeSelector( + select_returns=[[]], + on_select=lambda _timeout: stop_event.set(), + ) + dispatch = bus_alt.ImDispatch() + dispatch._channels_to_ws = {} + with mock.patch( + "odoo.addons.bus_alt_connection.models.bus.stop_event", stop_event + ), mock.patch( + "odoo.addons.bus_alt_connection.models.bus._connection_info_for" + ) as connection_info_for, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.psycopg2.connect" + ) as connect, mock.patch( + "odoo.addons.bus_alt_connection.models.bus.selectors.DefaultSelector" + ) as DefaultSelector: + connection_info_for.return_value = {"host": "h", "port": 1} + connect.return_value = fake_conn + DefaultSelector.return_value = fake_selector + dispatch.loop() + fake_conn.poll.assert_not_called() diff --git a/pandoc-3.1.12.2-1-amd64.deb b/pandoc-3.1.12.2-1-amd64.deb new file mode 100644 index 00000000000..8766f9f6f7a Binary files /dev/null and b/pandoc-3.1.12.2-1-amd64.deb differ diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index aab5f0b23c0..001cb4eac3a 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-server-tools" -version = "17.0.20260415.0" +version = "17.0.20260521.0" dependencies = [ "odoo-addon-attachment_logging>=17.0dev,<17.1dev", "odoo-addon-attachment_queue>=17.0dev,<17.1dev", @@ -22,6 +22,7 @@ dependencies = [ "odoo-addon-base_technical_user>=17.0dev,<17.1dev", "odoo-addon-base_view_inheritance_extension>=17.0dev,<17.1dev", "odoo-addon-base_write_diff>=17.0dev,<17.1dev", + "odoo-addon-bus_alt_connection>=17.0dev,<17.1dev", "odoo-addon-database_cleanup>=17.0dev,<17.1dev", "odoo-addon-dbfilter_from_header>=17.0dev,<17.1dev", "odoo-addon-fetchmail_attach_from_folder>=17.0dev,<17.1dev",