diff --git a/README.md b/README.md index 7e50d183c1c..c5213eac7ff 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ addon | version | maintainers | summary [scheduler_error_mailer](scheduler_error_mailer/) | 17.0.1.0.0 | | Scheduler Error Mailer [sentry](sentry/) | 17.0.1.0.0 | [![barsi](https://github.com/barsi.png?size=30px)](https://github.com/barsi) [![naglis](https://github.com/naglis.png?size=30px)](https://github.com/naglis) [![versada](https://github.com/versada.png?size=30px)](https://github.com/versada) [![moylop260](https://github.com/moylop260.png?size=30px)](https://github.com/moylop260) [![fernandahf](https://github.com/fernandahf.png?size=30px)](https://github.com/fernandahf) | Report Odoo errors to Sentry [server_action_logging](server_action_logging/) | 17.0.1.0.0 | | Module that provides a logging mechanism for server actions +[session_db](session_db/) | 17.0.1.0.0 | [![sbidoul](https://github.com/sbidoul.png?size=30px)](https://github.com/sbidoul) | Store sessions in DB [tracking_manager](tracking_manager/) | 17.0.1.0.3 | [![Kev-Roche](https://github.com/Kev-Roche.png?size=30px)](https://github.com/Kev-Roche) [![sebastienbeau](https://github.com/sebastienbeau.png?size=30px)](https://github.com/sebastienbeau) | This module tracks all fields of a model, including one2many and many2many ones. [upgrade_analysis](upgrade_analysis/) | 17.0.1.0.0 | [![StefanRijnhart](https://github.com/StefanRijnhart.png?size=30px)](https://github.com/StefanRijnhart) [![legalsylvain](https://github.com/legalsylvain.png?size=30px)](https://github.com/legalsylvain) | Performs a difference analysis between modules installed on two different Odoo instances diff --git a/auditlog/i18n/es_AR.po b/auditlog/i18n/es_AR.po index 3d6681f5bf4..ef844203513 100644 --- a/auditlog/i18n/es_AR.po +++ b/auditlog/i18n/es_AR.po @@ -9,16 +9,16 @@ msgstr "" "Project-Id-Version: Odoo Server 9.0c\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-11-26 01:45+0000\n" -"PO-Revision-Date: 2023-01-01 21:45+0000\n" +"PO-Revision-Date: 2024-09-17 02:06+0000\n" "Last-Translator: Ignacio Buioli \n" -"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/" -"teams/23907/es_AR/)\n" +"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/teams/" +"23907/es_AR/)\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.14.1\n" +"X-Generator: Weblate 5.6.2\n" #. module: auditlog #: model:ir.model.fields,field_description:auditlog.field_auditlog_rule__action_id @@ -602,7 +602,7 @@ msgstr "Ver registros" #. module: auditlog #: model:ir.model.fields,help:auditlog.field_auditlog_rule__user_ids msgid "if no user is added then it will applicable for all users" -msgstr "" +msgstr "si no se agrega ningún usuario, se aplicará a todos los usuarios" #~ msgid "Last Modified on" #~ msgstr "Última modificación en" diff --git a/base_exception/i18n/es_AR.po b/base_exception/i18n/es_AR.po index a18ebde2fdf..4916956e390 100644 --- a/base_exception/i18n/es_AR.po +++ b/base_exception/i18n/es_AR.po @@ -9,16 +9,16 @@ msgstr "" "Project-Id-Version: Odoo Server 10.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-05-01 10:38+0000\n" -"PO-Revision-Date: 2023-01-16 03:17+0000\n" +"PO-Revision-Date: 2024-09-17 02:06+0000\n" "Last-Translator: Ignacio Buioli \n" -"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/" -"teams/23907/es_AR/)\n" +"Language-Team: Spanish (Argentina) (https://www.transifex.com/oca/teams/" +"23907/es_AR/)\n" "Language: es_AR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.14.1\n" +"X-Generator: Weblate 5.6.2\n" #. module: base_exception #: model_terms:ir.ui.view,arch_db:base_exception.view_exception_rule_form @@ -78,7 +78,7 @@ msgstr "" #. module: base_exception #: model_terms:ir.ui.view,arch_db:base_exception.view_exception_rule_confirm msgid "Close" -msgstr "" +msgstr "Cerrar" #. module: base_exception #: model:ir.model.fields,field_description:base_exception.field_exception_rule__create_uid diff --git a/session_db/README.rst b/session_db/README.rst new file mode 100644 index 00000000000..fc2b58dc81e --- /dev/null +++ b/session_db/README.rst @@ -0,0 +1,93 @@ +==================== +Store sessions in DB +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7f625ad3c63b7ad41974310cc447302e517eb7fe62290a60ceec8f0325b80853 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-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/session_db + :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-session_db + :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| + +Store sessions in a database instead of the filesystem. This simplifies +the configuration of horizontally scalable deployments, by avoiding the +need for a distributed filesystem to store the Odoo sessions. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Set this module in the server wide modules. + +Set a ``SESSION_DB_URI`` environment variable as a full postgresql +connection string, like ``postgres://user:passwd@server/db`` or ``db``. + +It is recommended to use a dedicated database for this module, and +possibly a dedicated postgres user for additional security. + +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 +------- + +* Odoo SA +* ACSONE SA/NV + +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. + +.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px + :target: https://github.com/sbidoul + :alt: sbidoul + +Current `maintainer `__: + +|maintainer-sbidoul| + +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/session_db/__init__.py b/session_db/__init__.py new file mode 100644 index 00000000000..d051c561ca8 --- /dev/null +++ b/session_db/__init__.py @@ -0,0 +1 @@ +from . import pg_session_store diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py new file mode 100644 index 00000000000..292ab2e741c --- /dev/null +++ b/session_db/__manifest__.py @@ -0,0 +1,8 @@ +{ + "name": "Store sessions in DB", + "version": "17.0.1.0.0", + "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", + "license": "LGPL-3", + "website": "https://github.com/OCA/server-tools", + "maintainers": ["sbidoul"], +} diff --git a/session_db/i18n/session_db.pot b/session_db/i18n/session_db.pot new file mode 100644 index 00000000000..716a0702d88 --- /dev/null +++ b/session_db/i18n/session_db.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/session_db/pg_session_store.py b/session_db/pg_session_store.py new file mode 100644 index 00000000000..ad47eb4fec0 --- /dev/null +++ b/session_db/pg_session_store.py @@ -0,0 +1,169 @@ +# Copyright (c) Odoo SA 2017 +# @author Nicolas Seinlet +# Copyright (c) ACSONE SA 2022 +# @author Stéphane Bidoul +import json +import logging +import os + +import psycopg2 + +import odoo +from odoo import http +from odoo.tools._vendor import sessions +from odoo.tools.func import lazy_property + +_logger = logging.getLogger(__name__) + +lock = None +if odoo.evented: + import gevent.lock + + lock = gevent.lock.RLock() +elif odoo.tools.config["workers"] == 0: + import threading + + lock = threading.RLock() + + +def with_lock(func): + def wrapper(*args, **kwargs): + try: + if lock is not None: + lock.acquire() + return func(*args, **kwargs) + finally: + if lock is not None: + lock.release() + + return wrapper + + +def with_cursor(func): + def wrapper(self, *args, **kwargs): + tries = 0 + while True: + tries += 1 + try: + self._ensure_connection() + return func(self, *args, **kwargs) + except (psycopg2.InterfaceError, psycopg2.OperationalError): + self._close_connection() + if tries > 4: + _logger.warning( + "session_db operation try %s/5 failed, aborting", tries + ) + raise + _logger.info("session_db operation try %s/5 failed, retrying", tries) + + return wrapper + + +class PGSessionStore(sessions.SessionStore): + def __init__(self, uri, session_class=None): + super().__init__(session_class) + self._uri = uri + self._cr = None + self._open_connection() + self._setup_db() + + def __del__(self): + self._close_connection() + + @with_lock + def _ensure_connection(self): + if self._cr is None: + self._open_connection() + + @with_lock + def _open_connection(self): + self._close_connection() + cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) + self._cr = cnx.cursor() + self._cr._cnx.autocommit = True + + @with_lock + def _close_connection(self): + """Return cursor to the pool.""" + if self._cr is not None: + try: + self._cr.close() + except Exception: # pylint: disable=except-pass + pass + self._cr = None + + @with_lock + @with_cursor + def _setup_db(self): + self._cr.execute( + """ + CREATE TABLE IF NOT EXISTS http_sessions ( + sid varchar PRIMARY KEY, + write_date timestamp without time zone NOT NULL, + payload text NOT NULL + ) + """ + ) + + @with_lock + @with_cursor + def save(self, session): + payload = json.dumps(dict(session)) + self._cr.execute( + """ + INSERT INTO http_sessions(sid, write_date, payload) + VALUES (%(sid)s, now() at time zone 'UTC', %(payload)s) + ON CONFLICT (sid) + DO UPDATE SET payload = %(payload)s, + write_date = now() at time zone 'UTC' + """, + dict(sid=session.sid, payload=payload), + ) + + @with_lock + @with_cursor + def delete(self, session): + self._cr.execute("DELETE FROM http_sessions WHERE sid=%s", (session.sid,)) + + @with_lock + @with_cursor + def get(self, sid): + self._cr.execute("SELECT payload FROM http_sessions WHERE sid=%s", (sid,)) + try: + data = json.loads(self._cr.fetchone()[0]) + except Exception: + return self.new() + + return self.session_class(data, sid, False) + + # This method is not part of the Session interface but is called nevertheless, + # so let's get it from FilesystemSessionStore. + rotate = http.FilesystemSessionStore.rotate + + @with_lock + @with_cursor + def vacuum(self, max_lifetime=http.SESSION_LIFETIME): + self._cr.execute( + "DELETE FROM http_sessions " + "WHERE now() at time zone 'UTC' - write_date > %s", + (f"{max_lifetime} seconds",), + ) + + +_original_session_store = http.root.__class__.session_store + + +@lazy_property +def session_store(self): + session_db_uri = os.environ.get("SESSION_DB_URI") + if session_db_uri: + _logger.debug("HTTP sessions stored in: db") + return PGSessionStore(session_db_uri, session_class=http.Session) + return _original_session_store.__get__(self, self.__class__) + + +# Monkey patch of standard methods +_logger.debug("Monkey patching session store") +http.root.__class__.session_store = session_store +# Reset the lazy property cache +vars(http.root).pop("session_store", None) diff --git a/session_db/pyproject.toml b/session_db/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/session_db/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/session_db/readme/DESCRIPTION.md b/session_db/readme/DESCRIPTION.md new file mode 100644 index 00000000000..079bebe862c --- /dev/null +++ b/session_db/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +Store sessions in a database instead of the filesystem. This simplifies +the configuration of horizontally scalable deployments, by avoiding the +need for a distributed filesystem to store the Odoo sessions. diff --git a/session_db/readme/USAGE.md b/session_db/readme/USAGE.md new file mode 100644 index 00000000000..8b1213ee3f9 --- /dev/null +++ b/session_db/readme/USAGE.md @@ -0,0 +1,7 @@ +Set this module in the server wide modules. + +Set a `SESSION_DB_URI` environment variable as a full postgresql +connection string, like `postgres://user:passwd@server/db` or `db`. + +It is recommended to use a dedicated database for this module, and +possibly a dedicated postgres user for additional security. diff --git a/session_db/static/description/icon.png b/session_db/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/session_db/static/description/icon.png differ diff --git a/session_db/static/description/index.html b/session_db/static/description/index.html new file mode 100644 index 00000000000..b643081a1a4 --- /dev/null +++ b/session_db/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Store sessions in DB + + + +
+

Store sessions in DB

+ + +

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

+

Store sessions in a database instead of the filesystem. This simplifies +the configuration of horizontally scalable deployments, by avoiding the +need for a distributed filesystem to store the Odoo sessions.

+

Table of contents

+ +
+

Usage

+

Set this module in the server wide modules.

+

Set a SESSION_DB_URI environment variable as a full postgresql +connection string, like postgres://user:passwd@server/db or db.

+

It is recommended to use a dedicated database for this module, and +possibly a dedicated postgres user for additional security.

+
+
+

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

+
    +
  • Odoo SA
  • +
  • ACSONE SA/NV
  • +
+
+
+

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.

+

Current maintainer:

+

sbidoul

+

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/session_db/tests/__init__.py b/session_db/tests/__init__.py new file mode 100644 index 00000000000..22a56c88981 --- /dev/null +++ b/session_db/tests/__init__.py @@ -0,0 +1 @@ +from . import test_pg_session_store diff --git a/session_db/tests/test_pg_session_store.py b/session_db/tests/test_pg_session_store.py new file mode 100644 index 00000000000..61e7500ec31 --- /dev/null +++ b/session_db/tests/test_pg_session_store.py @@ -0,0 +1,82 @@ +from unittest import mock + +import psycopg2 + +from odoo import http +from odoo.sql_db import connection_info_for +from odoo.tests.common import TransactionCase +from odoo.tools import config + +from odoo.addons.session_db.pg_session_store import PGSessionStore + + +def _make_postgres_uri( + login=None, password=None, host=None, port=None, database=None, **kwargs +): + uri = ["postgres://"] + if login: + uri.append(login) + if password: + uri.append(f":{password}") + uri.append("@") + if host: + uri.append(host) + if port: + uri.append(f":{port}") + uri.append("/") + if database: + uri.append(database) + return "".join(uri) + + +class TestPGSessionStore(TransactionCase): + def setUp(self): + super().setUp() + _, connection_info = connection_info_for(config["db_name"]) + self.session_store = PGSessionStore( + _make_postgres_uri(**connection_info), session_class=http.Session + ) + + def test_session_crud(self): + session = self.session_store.new() + session["test"] = "test" + self.session_store.save(session) + assert session.sid is not None + assert self.session_store.get(session.sid)["test"] == "test" + self.session_store.delete(session) + assert self.session_store.get(session.sid).get("test") is None + + def test_retry(self): + """Test that session operations are retried before failing""" + with mock.patch("odoo.sql_db.Cursor.execute") as mock_execute: + mock_execute.side_effect = psycopg2.OperationalError() + try: + self.session_store.get("abc") + except psycopg2.OperationalError: # pylint: disable=except-pass + pass + else: + # We don't use self.assertRaises because Odoo is overriding + # in a way that interferes with the Cursor.execute mock + raise AssertionError("expected psycopg2.OperationalError") + assert mock_execute.call_count == 5 + # when the error is resolved, it works again + self.session_store.get("abc") + + def test_retry_connect_fail(self): + with mock.patch("odoo.sql_db.Cursor.execute") as mock_execute, mock.patch( + "odoo.sql_db.db_connect" + ) as mock_db_connect: + mock_execute.side_effect = psycopg2.OperationalError() + mock_db_connect.side_effect = RuntimeError("connection failed") + # get fails, and a RuntimeError is raised when trying to reconnect + try: + self.session_store.get("abc") + except RuntimeError: # pylint: disable=except-pass + pass + else: + # We don't use self.assertRaises because Odoo is overriding + # in a way that interferes with the Cursor.execute mock + raise AssertionError("expected RuntimeError") + assert mock_execute.call_count == 1 + # when the error is resolved, it works again + self.session_store.get("abc") diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 5c3ab51f663..3fbb29efcab 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.20240915.0" +version = "17.0.20240917.0" dependencies = [ "odoo-addon-auditlog>=17.0dev,<17.1dev", "odoo-addon-base_cron_exclusion>=17.0dev,<17.1dev", @@ -19,6 +19,7 @@ dependencies = [ "odoo-addon-scheduler_error_mailer>=17.0dev,<17.1dev", "odoo-addon-sentry>=17.0dev,<17.1dev", "odoo-addon-server_action_logging>=17.0dev,<17.1dev", + "odoo-addon-session_db>=17.0dev,<17.1dev", "odoo-addon-tracking_manager>=17.0dev,<17.1dev", "odoo-addon-upgrade_analysis>=17.0dev,<17.1dev", ] diff --git a/upgrade_analysis/i18n/es_AR.po b/upgrade_analysis/i18n/es_AR.po index 464123bb0be..98ff27aa39f 100644 --- a/upgrade_analysis/i18n/es_AR.po +++ b/upgrade_analysis/i18n/es_AR.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 15.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2022-09-04 06:07+0000\n" +"PO-Revision-Date: 2024-09-17 02:06+0000\n" "Last-Translator: Ignacio Buioli \n" "Language-Team: none\n" "Language: es_AR\n" @@ -14,7 +14,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 4.3.2\n" +"X-Generator: Weblate 5.6.2\n" #. module: upgrade_analysis #: model_terms:ir.ui.view,arch_db:upgrade_analysis.view_upgrade_install_wizard_form @@ -292,7 +292,7 @@ msgstr "Módulo" #. module: upgrade_analysis #: model:ir.model.fields,field_description:upgrade_analysis.field_upgrade_install_wizard__module_ids msgid "Modules" -msgstr "" +msgstr "Módulos" #. module: upgrade_analysis #: model:ir.model.fields,field_description:upgrade_analysis.field_upgrade_install_wizard__module_qty