Skip to content

Commit 7e3cbd3

Browse files
committed
Merge PR #3413 into 19.0
Signed-off-by sbidoul
2 parents 4b06851 + b53d0a0 commit 7e3cbd3

13 files changed

Lines changed: 915 additions & 0 deletions

File tree

session_db/README.rst

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
.. image:: https://odoo-community.org/readme-banner-image
2+
:target: https://odoo-community.org/get-involved?utm_source=readme
3+
:alt: Odoo Community Association
4+
5+
====================
6+
Store sessions in DB
7+
====================
8+
9+
..
10+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
11+
!! This file is generated by oca-gen-addon-readme !!
12+
!! changes will be overwritten. !!
13+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14+
!! source digest: sha256:1f019db79d78ab20a204e51d1750ed8b2e7c22dd3d585569f97579c339bc34c7
15+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16+
17+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
18+
:target: https://odoo-community.org/page/development-status
19+
:alt: Beta
20+
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
21+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
22+
:alt: License: LGPL-3
23+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
24+
:target: https://github.com/OCA/server-tools/tree/19.0/session_db
25+
:alt: OCA/server-tools
26+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
27+
:target: https://translation.odoo-community.org/projects/server-tools-19-0/server-tools-19-0-session_db
28+
:alt: Translate me on Weblate
29+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
30+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=19.0
31+
:alt: Try me on Runboat
32+
33+
|badge1| |badge2| |badge3| |badge4| |badge5|
34+
35+
Store sessions in a database instead of the filesystem. This simplifies
36+
the configuration of horizontally scalable deployments, by avoiding the
37+
need for a distributed filesystem to store the Odoo sessions.
38+
39+
**Table of contents**
40+
41+
.. contents::
42+
:local:
43+
44+
Usage
45+
=====
46+
47+
Set this module in the server wide modules.
48+
49+
Set a ``SESSION_DB_URI`` environment variable as a full postgresql
50+
connection string, like ``postgres://user:passwd@server/db`` or ``db``.
51+
52+
It is recommended to use a dedicated database for this module, and
53+
possibly a dedicated postgres user for additional security.
54+
55+
Bug Tracker
56+
===========
57+
58+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
59+
In case of trouble, please check there if your issue has already been reported.
60+
If you spotted it first, help us to smash it by providing a detailed and welcomed
61+
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20session_db%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
62+
63+
Do not contact contributors directly about support or help with technical issues.
64+
65+
Credits
66+
=======
67+
68+
Authors
69+
-------
70+
71+
* Odoo SA
72+
* ACSONE SA/NV
73+
74+
Maintainers
75+
-----------
76+
77+
This module is maintained by the OCA.
78+
79+
.. image:: https://odoo-community.org/logo.png
80+
:alt: Odoo Community Association
81+
:target: https://odoo-community.org
82+
83+
OCA, or the Odoo Community Association, is a nonprofit organization whose
84+
mission is to support the collaborative development of Odoo features and
85+
promote its widespread use.
86+
87+
.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px
88+
:target: https://github.com/sbidoul
89+
:alt: sbidoul
90+
91+
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
92+
93+
|maintainer-sbidoul|
94+
95+
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/19.0/session_db>`_ project on GitHub.
96+
97+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

session_db/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import pg_session_store

session_db/__manifest__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "Store sessions in DB",
3+
"version": "19.0.1.0.0",
4+
"author": "Odoo SA,ACSONE SA/NV,Odoo Community Association (OCA)",
5+
"license": "LGPL-3",
6+
"website": "https://github.com/OCA/server-tools",
7+
"maintainers": ["sbidoul"],
8+
}

session_db/i18n/it.po

Whitespace-only changes.

session_db/i18n/session_db.pot

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
#
4+
msgid ""
5+
msgstr ""
6+
"Project-Id-Version: Odoo Server 18.0\n"
7+
"Report-Msgid-Bugs-To: \n"
8+
"Last-Translator: \n"
9+
"Language-Team: \n"
10+
"MIME-Version: 1.0\n"
11+
"Content-Type: text/plain; charset=UTF-8\n"
12+
"Content-Transfer-Encoding: \n"
13+
"Plural-Forms: \n"

session_db/pg_session_store.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
# Copyright (c) Odoo SA 2017
2+
# @author Nicolas Seinlet
3+
# Copyright (c) ACSONE SA 2022
4+
# @author Stéphane Bidoul
5+
import functools
6+
import json
7+
import logging
8+
import os
9+
10+
import psycopg2
11+
12+
import odoo
13+
from odoo import http
14+
from odoo.tools._vendor import sessions
15+
16+
_logger = logging.getLogger(__name__)
17+
18+
lock = None
19+
if odoo.evented:
20+
import gevent.lock
21+
22+
lock = gevent.lock.RLock()
23+
elif odoo.tools.config["workers"] == 0:
24+
import threading
25+
26+
lock = threading.RLock()
27+
28+
29+
def with_lock(func):
30+
def wrapper(*args, **kwargs):
31+
try:
32+
if lock is not None:
33+
lock.acquire()
34+
return func(*args, **kwargs)
35+
finally:
36+
if lock is not None:
37+
lock.release()
38+
39+
return wrapper
40+
41+
42+
def with_cursor(func):
43+
def wrapper(self, *args, **kwargs):
44+
tries = 0
45+
while True:
46+
tries += 1
47+
try:
48+
self._ensure_connection()
49+
return func(self, *args, **kwargs)
50+
except (psycopg2.InterfaceError, psycopg2.OperationalError):
51+
self._close_connection()
52+
if tries > 4:
53+
_logger.warning(
54+
"session_db operation try %s/5 failed, aborting", tries
55+
)
56+
raise
57+
_logger.info("session_db operation try %s/5 failed, retrying", tries)
58+
59+
return wrapper
60+
61+
62+
class PGSessionStore(sessions.SessionStore):
63+
def __init__(self, uri, session_class=None):
64+
super().__init__(session_class)
65+
self._uri = uri
66+
self._cr = None
67+
self._open_connection()
68+
self._setup_db()
69+
70+
def __del__(self):
71+
self._close_connection()
72+
73+
@with_lock
74+
def _ensure_connection(self):
75+
if self._cr is None:
76+
self._open_connection()
77+
78+
@with_lock
79+
def _open_connection(self):
80+
self._close_connection()
81+
cnx = odoo.sql_db.db_connect(self._uri, allow_uri=True)
82+
self._cr = cnx.cursor()
83+
self._cr._cnx.autocommit = True
84+
85+
@with_lock
86+
def _close_connection(self):
87+
"""Return cursor to the pool."""
88+
if self._cr is not None:
89+
try:
90+
self._cr.close()
91+
except Exception: # pylint: disable=except-pass
92+
pass
93+
self._cr = None
94+
95+
@with_lock
96+
@with_cursor
97+
def _setup_db(self):
98+
self._cr.execute(
99+
"""
100+
CREATE TABLE IF NOT EXISTS http_sessions (
101+
sid varchar PRIMARY KEY,
102+
write_date timestamp without time zone NOT NULL,
103+
payload text NOT NULL
104+
)
105+
"""
106+
)
107+
108+
@with_lock
109+
@with_cursor
110+
def save(self, session):
111+
payload = json.dumps(dict(session))
112+
self._cr.execute(
113+
"""
114+
INSERT INTO http_sessions(sid, write_date, payload)
115+
VALUES (%(sid)s, now() at time zone 'UTC', %(payload)s)
116+
ON CONFLICT (sid)
117+
DO UPDATE SET payload = %(payload)s,
118+
write_date = now() at time zone 'UTC'
119+
""",
120+
dict(sid=session.sid, payload=payload),
121+
)
122+
123+
@with_lock
124+
@with_cursor
125+
def delete(self, session):
126+
self._cr.execute("DELETE FROM http_sessions WHERE sid=%s", (session.sid,))
127+
128+
@with_lock
129+
@with_cursor
130+
def get(self, sid):
131+
self._cr.execute("SELECT payload FROM http_sessions WHERE sid=%s", (sid,))
132+
try:
133+
data = json.loads(self._cr.fetchone()[0])
134+
except Exception:
135+
return self.new()
136+
137+
return self.session_class(data, sid, False)
138+
139+
# Odoo's FileSystemSessionStore has a few additional methods that are independent
140+
# of the actual storage backend. We reuse them here.
141+
rotate = http.FilesystemSessionStore.rotate
142+
generate_key = http.FilesystemSessionStore.generate_key
143+
is_valid_key = http.FilesystemSessionStore.is_valid_key
144+
delete_old_sessions = http.FilesystemSessionStore.delete_old_sessions
145+
146+
@with_lock
147+
@with_cursor
148+
def vacuum(self, max_lifetime=http.SESSION_LIFETIME):
149+
self._cr.execute(
150+
"DELETE FROM http_sessions "
151+
"WHERE now() at time zone 'UTC' - write_date > %s",
152+
(f"{max_lifetime} seconds",),
153+
)
154+
155+
@with_lock
156+
@with_cursor
157+
def get_missing_session_identifiers(self, identifiers: list[str]) -> set[str]:
158+
"""
159+
:param identifiers: session identifiers whose file existence must be checked
160+
identifiers are a part session sid (first 42 chars)
161+
:type identifiers: iterable
162+
:return: the identifiers which are not present on the filesystem
163+
:rtype: set
164+
165+
Note 1:
166+
Working with identifiers 42 characters long means that
167+
we don't have to work with the entire sid session,
168+
while maintaining sufficient entropy to avoid collisions.
169+
See details in ``generate_key``.
170+
171+
Note 2:
172+
Scans the session store for inactive (GC'd) sessions.
173+
Performance is acceptable for an infrequent background job.
174+
"""
175+
missing_identifiers = set()
176+
for identifier in identifiers:
177+
self._cr.execute(
178+
"SELECT sid FROM http_sessions WHERE sid LIKE %s||'%%' LIMIT 1",
179+
(identifier,),
180+
)
181+
if self._cr.rowcount == 0:
182+
missing_identifiers.add(identifier)
183+
return missing_identifiers
184+
185+
@with_lock
186+
@with_cursor
187+
def delete_from_identifiers(self, identifiers: list[str]) -> None:
188+
for identifier in identifiers:
189+
if not http._session_identifier_re.match(identifier):
190+
raise ValueError(
191+
"Identifier format incorrect, "
192+
"did you pass in a string instead of a list?"
193+
)
194+
self._cr.execute(
195+
"DELETE FROM http_sessions WHERE sid LIKE %s||'%%'", (identifier,)
196+
)
197+
198+
199+
_original_session_store = http.root.__class__.session_store
200+
201+
202+
@functools.cached_property
203+
def session_store(self):
204+
session_db_uri = os.environ.get("SESSION_DB_URI")
205+
if session_db_uri:
206+
_logger.debug("HTTP sessions stored in: db")
207+
return PGSessionStore(session_db_uri, session_class=http.Session)
208+
return _original_session_store.__get__(self, self.__class__)
209+
210+
211+
# Monkey patch of standard methods
212+
_logger.debug("Monkey patching session store")
213+
http.root.__class__.session_store = session_store
214+
http.root.__class__.session_store.__set_name__(http.root.__class__, "session_store")
215+
# Reset the lazy property cache
216+
vars(http.root).pop("session_store", None)

session_db/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"

session_db/readme/DESCRIPTION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Store sessions in a database instead of the filesystem. This simplifies
2+
the configuration of horizontally scalable deployments, by avoiding the
3+
need for a distributed filesystem to store the Odoo sessions.

session_db/readme/USAGE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Set this module in the server wide modules.
2+
3+
Set a `SESSION_DB_URI` environment variable as a full postgresql
4+
connection string, like `postgres://user:passwd@server/db` or `db`.
5+
6+
It is recommended to use a dedicated database for this module, and
7+
possibly a dedicated postgres user for additional security.
9.23 KB
Loading

0 commit comments

Comments
 (0)