From ad190943a917a7290c5d29bdbf0c12b49b1fef09 Mon Sep 17 00:00:00 2001 From: Matthieu Dietrich Date: Mon, 13 Apr 2015 14:53:44 +0200 Subject: [PATCH 1/5] Rename module as base_concurrency --- base_concurrency/__init__.py | 22 +++ base_concurrency/__openerp__.py | 44 ++++++ base_concurrency/cron.xml | 24 +++ base_concurrency/res_users.py | 137 ++++++++++++++++++ base_concurrency/security/ir.model.access.csv | 2 + 5 files changed, 229 insertions(+) create mode 100644 base_concurrency/__init__.py create mode 100644 base_concurrency/__openerp__.py create mode 100644 base_concurrency/cron.xml create mode 100644 base_concurrency/res_users.py create mode 100644 base_concurrency/security/ir.model.access.csv diff --git a/base_concurrency/__init__.py b/base_concurrency/__init__.py new file mode 100644 index 000000000..364b1d0b3 --- /dev/null +++ b/base_concurrency/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Matthieu Dietrich +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import res_users diff --git a/base_concurrency/__openerp__.py b/base_concurrency/__openerp__.py new file mode 100644 index 000000000..90f08f946 --- /dev/null +++ b/base_concurrency/__openerp__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Matthieu Dietrich +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{"name": "Base Concurrency", + "version": "1.0", + "author": "Camptocamp,Odoo Community Association (OCA)", + "category": "Specific Module", + "description": """ +Module to regroup all workarounds/fixes to avoid concurrency issues in SQL. + +* res.users login_date: +the login date is now separated from res.users; on long transactions, +"re-logging" by opening a new tab changes the current res.user row, +which creates concurrency issues with PostgreSQL in the first transaction. + +This creates a new table and a function field to avoid this. In order to +avoid breaking modules which access via SQL the login_date column, a cron +(inactive by default) can be used to sync data. +""", + "website": "http://camptocamp.com", + "depends": ['base'], + "data": ['security/ir.model.access.csv', + 'cron.xml'], + "auto_install": False, + "installable": True + } diff --git a/base_concurrency/cron.xml b/base_concurrency/cron.xml new file mode 100644 index 000000000..a134f7387 --- /dev/null +++ b/base_concurrency/cron.xml @@ -0,0 +1,24 @@ + + + + + + Synchronize login dates in res.users + 1 + + + 1 + days + -1 + + + + + + + diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py new file mode 100644 index 000000000..603ea9c6e --- /dev/null +++ b/base_concurrency/res_users.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Matthieu Dietrich +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import logging +import psycopg2 +import openerp.exceptions +from openerp import pooler, SUPERUSER_ID +from openerp.osv import orm, fields + +_logger = logging.getLogger(__name__) + + +# New class to store the login date +class ResUsersLogin(orm.Model): + + _name = 'res.users.login' + _columns = { + 'user_id': fields.many2one('res.users', 'User', required=True), + 'login_dt': fields.date('Latest connection'), + } + + _sql_constraints = [ + ('user_id_unique', + 'unique(user_id)', + 'The user can only have one login line!') + ] + + # Cron method + def cron_sync_login_date(self, cr, uid, context=None): + # Simple SQL query to update the original login_date column. + try: + cr.execute("UPDATE res_users SET login_date = " + "(SELECT login_dt FROM res_users_login " + "WHERE res_users_login.user_id = res_users.id)") + cr.commit() + except Exception as e: + cr.rollback() + _logger.exception('Could not synchronize login dates: %s', e) + + return True + + +class ResUsers(orm.Model): + + _inherit = 'res.users' + + # Function to retrieve the login date from the res.users object + # (used in some functions, and the user state) + def _get_login_date(self, cr, uid, ids, name, args, context=None): + res = {} + user_login_obj = self.pool['res.users.login'] + for user_id in ids: + login_ids = user_login_obj.search( + cr, uid, [('user_id', '=', user_id)], limit=1, + context=context) + if len(login_ids) == 0: + res[user_id] = False + else: + login = user_login_obj.browse(cr, uid, login_ids[0], + context=context) + res[user_id] = login.login_dt + return res + + _columns = { + 'login_date': fields.function(_get_login_date, + string='Latest connection', + type='date', select=1, + readonly=True, store=False, + nodrop=True), + } + + # Re-defining the login function in order to use the new table + def login(self, db, login, password): + if not password: + return False + user_id = False + cr = pooler.get_db(db).cursor() + try: + # check if user exists + res = self.search(cr, SUPERUSER_ID, [('login', '=', login)]) + if res: + user_id = res[0] + try: + # check credentials + self.check_credentials(cr, user_id, password) + except openerp.exceptions.AccessDenied: + _logger.info("Login failed for db:%s login:%s", db, login) + user_id = False + + if user_id: + try: + update_clause = ('NO KEY UPDATE' + if cr._cnx.server_version >= 90300 + else 'UPDATE') + cr.execute("SELECT login_dt " + "FROM res_users_login " + "WHERE user_id=%%s " + "FOR %s NOWAIT" % update_clause, + (user_id,), log_exceptions=False) + # create login line if not existing + result = cr.fetchone() + if not result: + cr.execute("INSERT INTO res_users_login " + "(user_id) VALUES (%s)", (user_id,)) + cr.execute("UPDATE res_users_login " + "SET login_dt = now() AT TIME ZONE 'UTC' " + "WHERE user_id=%s", (user_id,)) + cr.commit() + except psycopg2.OperationalError: + _logger.warning("Failed to update last_login " + "for db:%s login:%s", + db, login, exc_info=True) + cr.rollback() + except Exception as e: + _logger.exception('Login exception: %s', e) + user_id = False + finally: + cr.close() + + return user_id diff --git a/base_concurrency/security/ir.model.access.csv b/base_concurrency/security/ir.model.access.csv new file mode 100644 index 000000000..922391f56 --- /dev/null +++ b/base_concurrency/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +"access_res_users_login_all","res_users_login all","model_res_users_login",,1,0,0,0 From bec246bf05df0938c6a9bcfafe3a9c4aa4f5b8a1 Mon Sep 17 00:00:00 2001 From: Matthieu Dietrich Date: Mon, 13 Apr 2015 14:54:07 +0200 Subject: [PATCH 2/5] Improve the UPSERT mechanism --- base_concurrency/res_users.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py index 603ea9c6e..36ffbcfe6 100644 --- a/base_concurrency/res_users.py +++ b/base_concurrency/res_users.py @@ -106,22 +106,22 @@ class ResUsers(orm.Model): if user_id: try: - update_clause = ('NO KEY UPDATE' - if cr._cnx.server_version >= 90300 - else 'UPDATE') cr.execute("SELECT login_dt " "FROM res_users_login " "WHERE user_id=%%s " - "FOR %s NOWAIT" % update_clause, - (user_id,), log_exceptions=False) + "FOR UPDATE NOWAIT", (user_id,), + log_exceptions=False) # create login line if not existing result = cr.fetchone() - if not result: + if result: + cr.execute("UPDATE res_users_login " + "SET login_dt = now() " + "AT TIME ZONE 'UTC' " + "WHERE user_id=%s", (user_id,)) + else: cr.execute("INSERT INTO res_users_login " - "(user_id) VALUES (%s)", (user_id,)) - cr.execute("UPDATE res_users_login " - "SET login_dt = now() AT TIME ZONE 'UTC' " - "WHERE user_id=%s", (user_id,)) + "(user_id, login_dt) " + "VALUES (%s, now())", (user_id,)) cr.commit() except psycopg2.OperationalError: _logger.warning("Failed to update last_login " From 9c3a6647eeb4d5cb43d0d59f29d41534920054b4 Mon Sep 17 00:00:00 2001 From: Matthieu Dietrich Date: Mon, 13 Apr 2015 16:28:59 +0200 Subject: [PATCH 3/5] Remove extra %s --- base_concurrency/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py index 36ffbcfe6..905af8566 100644 --- a/base_concurrency/res_users.py +++ b/base_concurrency/res_users.py @@ -108,7 +108,7 @@ class ResUsers(orm.Model): try: cr.execute("SELECT login_dt " "FROM res_users_login " - "WHERE user_id=%%s " + "WHERE user_id=%s " "FOR UPDATE NOWAIT", (user_id,), log_exceptions=False) # create login line if not existing From 86e3f414ccd6ee4896e8a8ca1b58b098d3c7ef79 Mon Sep 17 00:00:00 2001 From: Charbel Jacquin Date: Fri, 24 Jul 2015 12:00:07 +0200 Subject: [PATCH 4/5] login() renamed to _login() in 8.0 --- base_concurrency/res_users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py index 905af8566..9e910fc1c 100644 --- a/base_concurrency/res_users.py +++ b/base_concurrency/res_users.py @@ -87,7 +87,7 @@ class ResUsers(orm.Model): } # Re-defining the login function in order to use the new table - def login(self, db, login, password): + def _login(self, db, login, password): if not password: return False user_id = False From f9736b54371328e9aac21f906c2cf33cec3730ea Mon Sep 17 00:00:00 2001 From: Charbel Jacquin Date: Fri, 24 Jul 2015 15:18:17 +0200 Subject: [PATCH 5/5] fix cursor obtention --- base_concurrency/res_users.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base_concurrency/res_users.py b/base_concurrency/res_users.py index 9e910fc1c..4ceeaeff5 100644 --- a/base_concurrency/res_users.py +++ b/base_concurrency/res_users.py @@ -21,7 +21,7 @@ import logging import psycopg2 import openerp.exceptions -from openerp import pooler, SUPERUSER_ID +from openerp import SUPERUSER_ID from openerp.osv import orm, fields _logger = logging.getLogger(__name__) @@ -91,7 +91,7 @@ class ResUsers(orm.Model): if not password: return False user_id = False - cr = pooler.get_db(db).cursor() + cr = self.pool.cursor() try: # check if user exists res = self.search(cr, SUPERUSER_ID, [('login', '=', login)])