From 4a8dea0cf0efdbdc8abd9b5bd89d7d7117e4fb13 Mon Sep 17 00:00:00 2001 From: Nicolas Seinlet Date: Tue, 3 Oct 2017 14:17:19 +0200 Subject: [PATCH 01/27] session_db : store sessions in a database rather than in filestore --- session_db/__init__.py | 1 + session_db/__manifest__.py | 24 +++++++ session_db/models/__init__.py | 1 + session_db/models/session.py | 125 ++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 session_db/__init__.py create mode 100644 session_db/__manifest__.py create mode 100644 session_db/models/__init__.py create mode 100644 session_db/models/session.py diff --git a/session_db/__init__.py b/session_db/__init__.py new file mode 100644 index 00000000000..bff786c0885 --- /dev/null +++ b/session_db/__init__.py @@ -0,0 +1 @@ +import models diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py new file mode 100644 index 00000000000..d805980ffd3 --- /dev/null +++ b/session_db/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Store sessions in DB", + 'description': """ + Storing sessions in DB +- workls only with workers > 0 +- set the session_db parameter in the odoo config file +- session_db parameter value is a full postgresql connection string, like user:passwd@server/db +- choose another DB than the odoo db itself, for security purpose +- it also possible to use another PostgreSQL user for the same security reasons + +Set this module in the server wide modules + """, + 'category': '', + 'version': '1.0', + + 'depends': [ + ], + + 'data': [ + ], + 'demo': [ + ], +} diff --git a/session_db/models/__init__.py b/session_db/models/__init__.py new file mode 100644 index 00000000000..ca2be93e797 --- /dev/null +++ b/session_db/models/__init__.py @@ -0,0 +1 @@ +import session diff --git a/session_db/models/session.py b/session_db/models/session.py new file mode 100644 index 00000000000..3bd96d352ec --- /dev/null +++ b/session_db/models/session.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +import psycopg2 +import json +import logging +import random +import werkzeug.contrib.sessions +import time + +import odoo +from odoo import http +from odoo.tools.func import lazy_property + +_logger = logging.getLogger(__name__) + +def with_cursor(func): + def wrapper(self, *args, **kwargs): + tries = 0 + while True: + tries += 1 + try: + return func(self, *args, **kwargs) + except psycopg2.InterfaceError as e: + _logger.info("Session in DB connection Retry %s/5" % tries) + if tries>4: + raise e + self._open_connection() + return wrapper + +class PGSessionStore(werkzeug.contrib.sessions.SessionStore): + # FIXME This class is NOT thread-safe. Only use in worker mode + def __init__(self, uri, session_class=None): + super(PGSessionStore, self).__init__(session_class) + self._uri = uri + self._open_connection() + self._setup_db() + + def __del__(self): + self._cr.close() + + def _open_connection(self): + cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) + self._cr = cnx.cursor() + self._cr.autocommit(True) + + @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_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_cursor + def delete(self, session): + self._cr.execute("DELETE FROM http_sessions WHERE sid=%s", [session.sid]) + + @with_cursor + def get(self, sid): + self._cr.execute("UPDATE http_sessions SET write_date = now() at time zone 'UTC' WHERE sid=%s", [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) + + @with_cursor + def gc(self): + self._cr.execute( + "DELETE FROM http_sessions WHERE now() at time zone 'UTC' - write_date > '7 days'" + ) + + +def session_gc(session_store): + """ + Global cleaning of sessions using either the standard way (delete session files), + Or the DB way. + """ + if random.random() < 0.001: + # we keep session one week + if hasattr(session_store, 'gc'): + session_store.gc() + return + last_week = time.time() - 60*60*24*7 + for fname in os.listdir(session_store.path): + path = os.path.join(session_store.path, fname) + try: + if os.path.getmtime(path) < last_week: + os.unlink(path) + except OSError: + pass + +class Root(http.Root): + @lazy_property + def session_store(self): + """ + Store sessions in DB rather than on FS if parameter permit so + """ + # Setup http sessions + session_db = odoo.tools.config.get('session_db') + if session_db: + _logger.debug("Sessions in db %s" % session_db) + return PGSessionStore(session_db, session_class=http.OpenERPSession) + path = odoo.tools.config.session_dir + _logger.debug('HTTP sessions stored in: %s', path) + return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=http.OpenERPSession) + +# #Monkey patch of standard methods +_logger.debug("Monkey patching sessions") +http.session_gc = session_gc +http.root = Root() From c53859a3c33102a4cd3c1dbd024c7b711c1026c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 29 Oct 2022 14:31:47 +0200 Subject: [PATCH 02/27] [MIG] session_db to 16.0 --- session_db/README.rst | 85 ++++++++++++++++++++ session_db/__init__.py | 2 +- session_db/__manifest__.py | 27 ++----- session_db/models/__init__.py | 1 - session_db/models/session.py | 125 ----------------------------- session_db/pg_session_store.py | 129 ++++++++++++++++++++++++++++++ session_db/readme/DESCRIPTION.rst | 1 + session_db/readme/ROADMAP.rst | 1 + session_db/readme/USAGE.rst | 7 ++ 9 files changed, 229 insertions(+), 149 deletions(-) create mode 100644 session_db/README.rst delete mode 100644 session_db/models/__init__.py delete mode 100644 session_db/models/session.py create mode 100644 session_db/pg_session_store.py create mode 100644 session_db/readme/DESCRIPTION.rst create mode 100644 session_db/readme/ROADMAP.rst create mode 100644 session_db/readme/USAGE.rst diff --git a/session_db/README.rst b/session_db/README.rst new file mode 100644 index 00000000000..413eea27281 --- /dev/null +++ b/session_db/README.rst @@ -0,0 +1,85 @@ +==================== +Store sessions in DB +==================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/16.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-16-0/server-tools-16-0-session_db + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Store sessions in a database instead of the filesystem. + +**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. + +Known issues / Roadmap +====================== + +This module does not work with multi-threaded workers, so it requires workers > 0. + +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 smashing 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. + +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 index bff786c0885..d051c561ca8 100644 --- a/session_db/__init__.py +++ b/session_db/__init__.py @@ -1 +1 @@ -import models +from . import pg_session_store diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index d805980ffd3..c644d2679b3 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,24 +1,7 @@ -# -*- coding: utf-8 -*- { - 'name': "Store sessions in DB", - 'description': """ - Storing sessions in DB -- workls only with workers > 0 -- set the session_db parameter in the odoo config file -- session_db parameter value is a full postgresql connection string, like user:passwd@server/db -- choose another DB than the odoo db itself, for security purpose -- it also possible to use another PostgreSQL user for the same security reasons - -Set this module in the server wide modules - """, - 'category': '', - 'version': '1.0', - - 'depends': [ - ], - - 'data': [ - ], - 'demo': [ - ], + "name": "Store sessions in DB", + "version": "16.0.1.0.0", + "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", + "license": "LGPL-3", + "website": "https://github.com/OCA/server-tools", } diff --git a/session_db/models/__init__.py b/session_db/models/__init__.py deleted file mode 100644 index ca2be93e797..00000000000 --- a/session_db/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -import session diff --git a/session_db/models/session.py b/session_db/models/session.py deleted file mode 100644 index 3bd96d352ec..00000000000 --- a/session_db/models/session.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -import psycopg2 -import json -import logging -import random -import werkzeug.contrib.sessions -import time - -import odoo -from odoo import http -from odoo.tools.func import lazy_property - -_logger = logging.getLogger(__name__) - -def with_cursor(func): - def wrapper(self, *args, **kwargs): - tries = 0 - while True: - tries += 1 - try: - return func(self, *args, **kwargs) - except psycopg2.InterfaceError as e: - _logger.info("Session in DB connection Retry %s/5" % tries) - if tries>4: - raise e - self._open_connection() - return wrapper - -class PGSessionStore(werkzeug.contrib.sessions.SessionStore): - # FIXME This class is NOT thread-safe. Only use in worker mode - def __init__(self, uri, session_class=None): - super(PGSessionStore, self).__init__(session_class) - self._uri = uri - self._open_connection() - self._setup_db() - - def __del__(self): - self._cr.close() - - def _open_connection(self): - cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) - self._cr = cnx.cursor() - self._cr.autocommit(True) - - @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_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_cursor - def delete(self, session): - self._cr.execute("DELETE FROM http_sessions WHERE sid=%s", [session.sid]) - - @with_cursor - def get(self, sid): - self._cr.execute("UPDATE http_sessions SET write_date = now() at time zone 'UTC' WHERE sid=%s", [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) - - @with_cursor - def gc(self): - self._cr.execute( - "DELETE FROM http_sessions WHERE now() at time zone 'UTC' - write_date > '7 days'" - ) - - -def session_gc(session_store): - """ - Global cleaning of sessions using either the standard way (delete session files), - Or the DB way. - """ - if random.random() < 0.001: - # we keep session one week - if hasattr(session_store, 'gc'): - session_store.gc() - return - last_week = time.time() - 60*60*24*7 - for fname in os.listdir(session_store.path): - path = os.path.join(session_store.path, fname) - try: - if os.path.getmtime(path) < last_week: - os.unlink(path) - except OSError: - pass - -class Root(http.Root): - @lazy_property - def session_store(self): - """ - Store sessions in DB rather than on FS if parameter permit so - """ - # Setup http sessions - session_db = odoo.tools.config.get('session_db') - if session_db: - _logger.debug("Sessions in db %s" % session_db) - return PGSessionStore(session_db, session_class=http.OpenERPSession) - path = odoo.tools.config.session_dir - _logger.debug('HTTP sessions stored in: %s', path) - return werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=http.OpenERPSession) - -# #Monkey patch of standard methods -_logger.debug("Monkey patching sessions") -http.session_gc = session_gc -http.root = Root() diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py new file mode 100644 index 00000000000..05a46514be0 --- /dev/null +++ b/session_db/pg_session_store.py @@ -0,0 +1,129 @@ +# 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__) + + +def with_cursor(func): + def wrapper(self, *args, **kwargs): + tries = 0 + while True: + tries += 1 + try: + return func(self, *args, **kwargs) + except psycopg2.InterfaceError as e: + _logger.info("Session in DB connection Retry %s/5" % tries) + if tries > 4: + raise e + self._open_connection() + + return wrapper + + +class PGSessionStore(sessions.SessionStore): + def __init__(self, uri, session_class=None): + super().__init__(session_class) + self._uri = uri + self._cr = None + # FIXME This class is NOT thread-safe. Only use in worker mode + if odoo.tools.config["workers"] == 0: + raise ValueError("session_db requires multiple workers") + self._open_connection() + self._setup_db() + + def __del__(self): + if self._cr is not None: + self._cr.close() + + def _open_connection(self): + cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) + self._cr = cnx.cursor() + self._cr._cnx.autocommit = True + + @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_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_cursor + def delete(self, session): + self._cr.execute("DELETE FROM http_sessions WHERE sid=%s", (session.sid,)) + + @with_cursor + def get(self, sid): + self._cr.execute( + "UPDATE http_sessions " + "SET write_date = now() at time zone 'UTC' " + "WHERE sid=%s", + [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_cursor + def vacuum(self): + self._cr.execute( + "DELETE FROM http_sessions " + "WHERE now() at time zone 'UTC' - write_date > '7 days'" + ) + + +_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/readme/DESCRIPTION.rst b/session_db/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..2b129a05366 --- /dev/null +++ b/session_db/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Store sessions in a database instead of the filesystem. diff --git a/session_db/readme/ROADMAP.rst b/session_db/readme/ROADMAP.rst new file mode 100644 index 00000000000..c4cedb7baee --- /dev/null +++ b/session_db/readme/ROADMAP.rst @@ -0,0 +1 @@ +This module does not work with multi-threaded workers, so it requires workers > 0. diff --git a/session_db/readme/USAGE.rst b/session_db/readme/USAGE.rst new file mode 100644 index 00000000000..8ea69803a6a --- /dev/null +++ b/session_db/readme/USAGE.rst @@ -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. From 87be2eca29480975e7693870414227fcd2eb99b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 29 Oct 2022 15:36:13 +0200 Subject: [PATCH 03/27] session_db: use SESSION_LIFETIME constant --- session_db/pg_session_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 05a46514be0..951b67a229a 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -106,7 +106,8 @@ def get(self, sid): def vacuum(self): self._cr.execute( "DELETE FROM http_sessions " - "WHERE now() at time zone 'UTC' - write_date > '7 days'" + "WHERE now() at time zone 'UTC' - write_date > %s", + (f"{http.SESSION_LIFETIME} seconds",), ) From 5109801f800ca8b751c56a1a6e13dd6915180324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 29 Oct 2022 15:38:03 +0200 Subject: [PATCH 04/27] session_db: do not update write_date on get The upstream FilesystemSessionStore does not do that. --- session_db/pg_session_store.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 951b67a229a..3feae5f2b5c 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -84,12 +84,6 @@ def delete(self, session): @with_cursor def get(self, sid): - self._cr.execute( - "UPDATE http_sessions " - "SET write_date = now() at time zone 'UTC' " - "WHERE sid=%s", - [sid], - ) self._cr.execute("SELECT payload FROM http_sessions WHERE sid=%s", (sid,)) try: data = json.loads(self._cr.fetchone()[0]) From 249f4888a55475a4c1a6bddb73930efb7b87463e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 29 Oct 2022 17:39:22 +0200 Subject: [PATCH 05/27] session_db: declare maintainer --- session_db/__manifest__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index c644d2679b3..e969d96f240 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -4,4 +4,5 @@ "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", + "maintainers": ["sbidoul"], } From 5486d86dc6dbba1d4c747a4939ff481ce14109d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 17 Jan 2023 16:50:59 +0100 Subject: [PATCH 06/27] session_db: explain why such as module is useful --- session_db/readme/DESCRIPTION.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/session_db/readme/DESCRIPTION.rst b/session_db/readme/DESCRIPTION.rst index 2b129a05366..ee705201aab 100644 --- a/session_db/readme/DESCRIPTION.rst +++ b/session_db/readme/DESCRIPTION.rst @@ -1 +1,3 @@ -Store sessions in a database instead of the filesystem. +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. From 3a754051f7861017bc9ce88bd5033b9c36a75116 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Thu, 19 Jan 2023 16:29:23 +0000 Subject: [PATCH 07/27] [UPD] Update session_db.pot --- session_db/i18n/session_db.pot | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 session_db/i18n/session_db.pot diff --git a/session_db/i18n/session_db.pot b/session_db/i18n/session_db.pot new file mode 100644 index 00000000000..78d58d53fe0 --- /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 16.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" From 74fba614bcf14114ed102e2d603c9bd166a716ed Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 19 Jan 2023 16:32:06 +0000 Subject: [PATCH 08/27] [UPD] README.rst --- session_db/README.rst | 12 +- session_db/static/description/index.html | 431 +++++++++++++++++++++++ 2 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 session_db/static/description/index.html diff --git a/session_db/README.rst b/session_db/README.rst index 413eea27281..525e7939248 100644 --- a/session_db/README.rst +++ b/session_db/README.rst @@ -25,7 +25,9 @@ Store sessions in DB |badge1| |badge2| |badge3| |badge4| |badge5| -Store sessions in a database instead of the filesystem. +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** @@ -80,6 +82,14 @@ 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/static/description/index.html b/session_db/static/description/index.html new file mode 100644 index 00000000000..0abed81ab5f --- /dev/null +++ b/session_db/static/description/index.html @@ -0,0 +1,431 @@ + + + + + + +Store sessions in DB + + + +
+

Store sessions in DB

+ + +

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

+

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.

+
+
+

Known issues / Roadmap

+

This module does not work with multi-threaded workers, so it requires workers > 0.

+
+
+

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 smashing 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.

+
+
+
+ + From c8985682191ec389d07a46eef8e8cb407dc122ad Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 19 Jan 2023 16:32:06 +0000 Subject: [PATCH 09/27] [ADD] icon.png --- session_db/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 session_db/static/description/icon.png diff --git a/session_db/static/description/icon.png b/session_db/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 35b8dbe8c3fd15c1e54d1549a05e436ee8d966fd Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 19 Jan 2023 16:32:06 +0000 Subject: [PATCH 10/27] session_db 16.0.1.0.1 --- session_db/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index e969d96f240..dd64986e261 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Store sessions in DB", - "version": "16.0.1.0.0", + "version": "16.0.1.0.1", "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", From d49c69102f6358c791a7c700bef44b0c472ead60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sat, 21 Jan 2023 13:27:21 +0100 Subject: [PATCH 11/27] session_db: improve resiliency to database errors Retry on OperationalError exception, which we receive on database restart. Return cursor to pool when reconnecting. --- session_db/pg_session_store.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 3feae5f2b5c..e5974232de5 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -23,7 +23,7 @@ def wrapper(self, *args, **kwargs): tries += 1 try: return func(self, *args, **kwargs) - except psycopg2.InterfaceError as e: + except (psycopg2.InterfaceError, psycopg2.OperationalError) as e: _logger.info("Session in DB connection Retry %s/5" % tries) if tries > 4: raise e @@ -49,6 +49,13 @@ def __del__(self): def _open_connection(self): cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) + try: + # return cursor to the pool + if self._cr is not None: + self._cr.close() + self._cr = None + except Exception: # pylint: disable=except-pass + pass self._cr = cnx.cursor() self._cr._cnx.autocommit = True From 894613b6129a991607d57ac5c3ad4ec2d37f6ba3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 23 Jan 2023 13:36:12 +0000 Subject: [PATCH 12/27] session_db 16.0.1.0.2 --- session_db/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index dd64986e261..cc09e0f8927 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Store sessions in DB", - "version": "16.0.1.0.1", + "version": "16.0.1.0.2", "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", From 182d44f26c8daff41f40a3cac718660b9a3bfb6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 10 Feb 2023 19:15:26 +0100 Subject: [PATCH 13/27] session_db: gevent and thread support There were concurrency issues in evented mode. So while I was at it, I added support for threaded mode too. --- session_db/pg_session_store.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index e5974232de5..189be9658f9 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -15,6 +15,29 @@ _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): @@ -37,9 +60,6 @@ def __init__(self, uri, session_class=None): super().__init__(session_class) self._uri = uri self._cr = None - # FIXME This class is NOT thread-safe. Only use in worker mode - if odoo.tools.config["workers"] == 0: - raise ValueError("session_db requires multiple workers") self._open_connection() self._setup_db() @@ -47,6 +67,7 @@ def __del__(self): if self._cr is not None: self._cr.close() + @with_lock def _open_connection(self): cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) try: @@ -59,6 +80,7 @@ def _open_connection(self): self._cr = cnx.cursor() self._cr._cnx.autocommit = True + @with_lock @with_cursor def _setup_db(self): self._cr.execute( @@ -71,6 +93,7 @@ def _setup_db(self): """ ) + @with_lock @with_cursor def save(self, session): payload = json.dumps(dict(session)) @@ -85,10 +108,12 @@ def save(self, session): 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,)) @@ -103,6 +128,7 @@ def get(self, sid): # so let's get it from FilesystemSessionStore. rotate = http.FilesystemSessionStore.rotate + @with_lock @with_cursor def vacuum(self): self._cr.execute( From 71352e0496a93b73b9f5e67a0289511da8d08cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 10 Feb 2023 19:21:10 +0100 Subject: [PATCH 14/27] session_db: cosmetics --- session_db/pg_session_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 189be9658f9..1a6e3a2d4f9 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -69,7 +69,6 @@ def __del__(self): @with_lock def _open_connection(self): - cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) try: # return cursor to the pool if self._cr is not None: @@ -77,6 +76,7 @@ def _open_connection(self): self._cr = None except Exception: # pylint: disable=except-pass pass + cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) self._cr = cnx.cursor() self._cr._cnx.autocommit = True From 69868a84eb2bac60c11efc34346441523ab7a241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 10 Feb 2023 19:57:41 +0100 Subject: [PATCH 15/27] session_db: improve cursor release --- session_db/pg_session_store.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 1a6e3a2d4f9..849e431de00 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -69,13 +69,13 @@ def __del__(self): @with_lock def _open_connection(self): - try: - # return cursor to the pool - if self._cr is not None: + # return cursor to the pool + if self._cr is not None: + try: self._cr.close() - self._cr = None - except Exception: # pylint: disable=except-pass - pass + except Exception: # pylint: disable=except-pass + pass + self._cr = None cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) self._cr = cnx.cursor() self._cr._cnx.autocommit = True From c7af2bd6df636a09ecd8dc534258d7eb3b07cb87 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 21 Feb 2023 09:49:13 +0000 Subject: [PATCH 16/27] session_db 16.0.1.0.3 --- session_db/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index cc09e0f8927..8cbab9cf26e 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Store sessions in DB", - "version": "16.0.1.0.2", + "version": "16.0.1.0.3", "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", From 6c241ead8ba3117d6ec962ba30957b77979975da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 23 Mar 2023 17:36:07 +0100 Subject: [PATCH 17/27] session_db: reconnect if needed If the connection to the database fails when retrying a session operation, we end up with no cursore, which makes subsequent session operations fail. We fix this by ensuring we have cursor before any operations. --- session_db/pg_session_store.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 849e431de00..5dc02fe0d94 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -45,6 +45,7 @@ def wrapper(self, *args, **kwargs): while True: tries += 1 try: + self._ensure_connection() return func(self, *args, **kwargs) except (psycopg2.InterfaceError, psycopg2.OperationalError) as e: _logger.info("Session in DB connection Retry %s/5" % tries) @@ -67,6 +68,11 @@ def __del__(self): if self._cr is not None: self._cr.close() + @with_lock + def _ensure_connection(self): + if self._cr is None: + self._open_connection() + @with_lock def _open_connection(self): # return cursor to the pool From 87ba744c9f8325c93395add748fb3d9f11aa10e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 23 Mar 2023 20:09:59 +0100 Subject: [PATCH 18/27] session_db: add a few tests --- session_db/tests/__init__.py | 1 + session_db/tests/test_pg_session_store.py | 70 +++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 session_db/tests/__init__.py create mode 100644 session_db/tests/test_pg_session_store.py 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..8e57ce53bcd --- /dev/null +++ b/session_db/tests/test_pg_session_store.py @@ -0,0 +1,70 @@ +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() + with self.assertRaises(psycopg2.OperationalError): + self.session_store.get("abc") + 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 + with self.assertRaises(RuntimeError): + self.session_store.get("abc") + assert mock_execute.call_count == 1 + # when the error is resolved, it works again + self.session_store.get("abc") From 41264b9b6f77267ba54cc8da6baed5040a2830c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 24 Mar 2023 09:38:44 +0100 Subject: [PATCH 19/27] session_db: refactor retry handling --- session_db/pg_session_store.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index 5dc02fe0d94..cf2dd607d07 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -47,11 +47,14 @@ def wrapper(self, *args, **kwargs): try: self._ensure_connection() return func(self, *args, **kwargs) - except (psycopg2.InterfaceError, psycopg2.OperationalError) as e: - _logger.info("Session in DB connection Retry %s/5" % tries) + except (psycopg2.InterfaceError, psycopg2.OperationalError): + self._close_connection() if tries > 4: - raise e - self._open_connection() + _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 @@ -65,8 +68,7 @@ def __init__(self, uri, session_class=None): self._setup_db() def __del__(self): - if self._cr is not None: - self._cr.close() + self._close_connection() @with_lock def _ensure_connection(self): @@ -75,16 +77,20 @@ def _ensure_connection(self): @with_lock def _open_connection(self): - # return cursor to the pool + 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 - cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True) - self._cr = cnx.cursor() - self._cr._cnx.autocommit = True @with_lock @with_cursor From 034903899886e392a4e0ee2478dd8a9ea00730fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 27 Mar 2023 19:01:20 +0200 Subject: [PATCH 20/27] session_db: fix tests for v16 compatibility --- session_db/tests/test_pg_session_store.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/session_db/tests/test_pg_session_store.py b/session_db/tests/test_pg_session_store.py index 8e57ce53bcd..61e7500ec31 100644 --- a/session_db/tests/test_pg_session_store.py +++ b/session_db/tests/test_pg_session_store.py @@ -50,8 +50,14 @@ 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() - with self.assertRaises(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") @@ -63,8 +69,14 @@ def test_retry_connect_fail(self): 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 - with self.assertRaises(RuntimeError): + 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") From 52b397383cbbee63754842aae46d11efc729b4a3 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Tue, 11 Apr 2023 14:47:46 +0000 Subject: [PATCH 21/27] session_db 16.0.1.0.4 --- session_db/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index 8cbab9cf26e..6891ada7ad0 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Store sessions in DB", - "version": "16.0.1.0.3", + "version": "16.0.1.0.4", "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", From bc2a3830e29c8bee4784d6f1db0edf5b675ad2d4 Mon Sep 17 00:00:00 2001 From: Christoph Fiehe Date: Wed, 16 Aug 2023 11:37:03 +0200 Subject: [PATCH 22/27] Fixes the issue "PGSessionStore.vacuum() got an unexpected keyword argument 'max_lifetime'" when autovacuum gets executed. Signed-off-by: Christoph Fiehe --- session_db/pg_session_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/session_db/pg_session_store.py b/session_db/pg_session_store.py index cf2dd607d07..ad47eb4fec0 100644 --- a/session_db/pg_session_store.py +++ b/session_db/pg_session_store.py @@ -142,11 +142,11 @@ def get(self, sid): @with_lock @with_cursor - def vacuum(self): + 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"{http.SESSION_LIFETIME} seconds",), + (f"{max_lifetime} seconds",), ) From 8e08a55c3fcbb52a19d09e5ba05cb11ddad952c8 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 1 Sep 2023 08:03:35 +0000 Subject: [PATCH 23/27] session_db 16.0.1.0.5 --- session_db/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session_db/__manifest__.py b/session_db/__manifest__.py index 6891ada7ad0..d02e1c60a8f 100644 --- a/session_db/__manifest__.py +++ b/session_db/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Store sessions in DB", - "version": "16.0.1.0.4", + "version": "16.0.1.0.5", "author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)", "license": "LGPL-3", "website": "https://github.com/OCA/server-tools", From c9a8c6941748df411c59fc31ed013b0ac66056f5 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sun, 3 Sep 2023 16:49:46 +0000 Subject: [PATCH 24/27] [UPD] README.rst --- session_db/README.rst | 15 +++++---- session_db/static/description/index.html | 40 +++++++++++++----------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/session_db/README.rst b/session_db/README.rst index 525e7939248..fff8708275d 100644 --- a/session_db/README.rst +++ b/session_db/README.rst @@ -2,10 +2,13 @@ Store sessions in DB ==================== -.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0e92dd612d4d3860e95f5c06e6b9ea02c10fc402034c328effb4a9ac44b9cb73 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status @@ -19,11 +22,11 @@ Store sessions in DB .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-session_db :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/16.0 - :alt: Try me on Runbot +.. |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=16.0 + :alt: Try me on Runboat -|badge1| |badge2| |badge3| |badge4| |badge5| +|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 @@ -55,7 +58,7 @@ 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 smashing it by providing a detailed and welcomed +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. diff --git a/session_db/static/description/index.html b/session_db/static/description/index.html index 0abed81ab5f..9637f1a7f6a 100644 --- a/session_db/static/description/index.html +++ b/session_db/static/description/index.html @@ -1,20 +1,20 @@ - + - + Store sessions in DB