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..905af8566 --- /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: + cr.execute("SELECT login_dt " + "FROM res_users_login " + "WHERE user_id=%s " + "FOR UPDATE NOWAIT", (user_id,), + log_exceptions=False) + # create login line if not existing + result = cr.fetchone() + 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, login_dt) " + "VALUES (%s, now())", (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