[ADD] sql_db: pre/post-commit/rollback hooks
This commit is contained in:
@@ -119,8 +119,8 @@ class ir_cron(models.Model):
|
||||
:param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
|
||||
must not be committed/rolled back!
|
||||
"""
|
||||
try:
|
||||
with api.Environment.manage():
|
||||
with api.Environment.manage():
|
||||
try:
|
||||
cron = api.Environment(job_cr, job['user_id'], {
|
||||
'lastcall': fields.Datetime.from_string(job['lastcall'])
|
||||
})[cls._name]
|
||||
@@ -153,9 +153,9 @@ class ir_cron(models.Model):
|
||||
cron.flush()
|
||||
cron.invalidate_cache()
|
||||
|
||||
finally:
|
||||
job_cr.commit()
|
||||
cron_cr.commit()
|
||||
finally:
|
||||
job_cr.commit()
|
||||
cron_cr.commit()
|
||||
|
||||
@classmethod
|
||||
def _process_jobs(cls, db_name):
|
||||
|
||||
@@ -270,3 +270,35 @@ class TestFormatLangDate(TransactionCase):
|
||||
# Check given `lang_code` overwites context lang
|
||||
self.assertEqual(misc.format_time(lang.with_context(lang='fr_FR').env, time_part, time_format='short', lang_code='zh_CN'), '\u4e0b\u53484:30')
|
||||
self.assertEqual(misc.format_time(lang.with_context(lang='zh_CN').env, time_part, time_format='medium', lang_code='fr_FR'), '16:30:22')
|
||||
|
||||
|
||||
class TestGroupCalls(BaseCase):
|
||||
def test_callbacks(self):
|
||||
log = []
|
||||
|
||||
def foo():
|
||||
log.append("foo")
|
||||
|
||||
def bar(items):
|
||||
log.extend(items)
|
||||
callbacks.add(baz)
|
||||
|
||||
def baz():
|
||||
log.append("baz")
|
||||
|
||||
callbacks = misc.GroupCalls()
|
||||
callbacks.add(foo)
|
||||
callbacks.add(bar, list)[0].append(1)
|
||||
callbacks.add(bar, list)[0].append(2)
|
||||
self.assertEqual(log, [])
|
||||
|
||||
callbacks()
|
||||
self.assertEqual(log, ["foo", 1, 2, "baz"])
|
||||
|
||||
callbacks()
|
||||
self.assertEqual(log, ["foo", 1, 2, "baz"])
|
||||
|
||||
callbacks.add(bar, list)[0].append(3)
|
||||
callbacks.clear()
|
||||
callbacks()
|
||||
self.assertEqual(log, ["foo", 1, 2, "baz"])
|
||||
|
||||
+98
-76
@@ -15,6 +15,7 @@ import logging
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from decorator import decorator
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
import psycopg2.extensions
|
||||
@@ -80,7 +81,72 @@ re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$')
|
||||
|
||||
sql_counter = 0
|
||||
|
||||
class Cursor(object):
|
||||
|
||||
@decorator
|
||||
def check(f, self, *args, **kwargs):
|
||||
""" Wrap a cursor method that cannot be called when the cursor is closed. """
|
||||
if self._closed:
|
||||
msg = 'Unable to use a closed cursor.'
|
||||
if self.__closer:
|
||||
msg += ' It was closed at %s, line %s' % self.__closer
|
||||
raise psycopg2.OperationalError(msg)
|
||||
return f(self, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseCursor:
|
||||
""" Base class for cursors that manages pre/post commit/rollback hooks. """
|
||||
|
||||
def __init__(self):
|
||||
self.precommit = tools.GroupCalls()
|
||||
self.postcommit = tools.GroupCalls()
|
||||
self.prerollback = tools.GroupCalls()
|
||||
self.postrollback = tools.GroupCalls()
|
||||
|
||||
@contextmanager
|
||||
@check
|
||||
def savepoint(self, flush=True):
|
||||
"""context manager entering in a new savepoint"""
|
||||
name = uuid.uuid1().hex
|
||||
if flush:
|
||||
flush_env(self)
|
||||
self.precommit()
|
||||
self.prerollback.clear()
|
||||
self.execute('SAVEPOINT "%s"' % name)
|
||||
try:
|
||||
yield
|
||||
if flush:
|
||||
flush_env(self)
|
||||
self.precommit()
|
||||
self.prerollback.clear()
|
||||
except Exception:
|
||||
if flush:
|
||||
clear_env(self)
|
||||
self.precommit.clear()
|
||||
self.prerollback()
|
||||
self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
|
||||
raise
|
||||
else:
|
||||
self.execute('RELEASE SAVEPOINT "%s"' % name)
|
||||
|
||||
def __enter__(self):
|
||||
""" Using the cursor as a contextmanager automatically commits and
|
||||
closes it::
|
||||
|
||||
with cr:
|
||||
cr.execute(...)
|
||||
|
||||
# cr is committed if no failure occurred
|
||||
# cr is closed in any case
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is None:
|
||||
self.commit()
|
||||
self.close()
|
||||
|
||||
|
||||
class Cursor(BaseCursor):
|
||||
"""Represents an open transaction to the PostgreSQL DB backend,
|
||||
acting as a lightweight wrapper around psycopg2's
|
||||
``cursor`` objects.
|
||||
@@ -152,18 +218,9 @@ class Cursor(object):
|
||||
"""
|
||||
IN_MAX = 1000 # decent limit on size of IN queries - guideline = Oracle limit
|
||||
|
||||
def check(f):
|
||||
@wraps(f)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self._closed:
|
||||
msg = 'Unable to use a closed cursor.'
|
||||
if self.__closer:
|
||||
msg += ' It was closed at %s, line %s' % self.__closer
|
||||
raise psycopg2.OperationalError(msg)
|
||||
return f(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def __init__(self, pool, dbname, dsn, serialized=True):
|
||||
super().__init__()
|
||||
|
||||
self.sql_from_log = {}
|
||||
self.sql_into_log = {}
|
||||
|
||||
@@ -197,9 +254,6 @@ class Cursor(object):
|
||||
|
||||
self.cache = {}
|
||||
|
||||
# event handlers, see method after() below
|
||||
self._event_handlers = {'commit': [], 'rollback': []}
|
||||
|
||||
def __build_dict(self, row):
|
||||
return {d.name: row[i] for i, d in enumerate(self._obj.description)}
|
||||
def dictfetchone(self):
|
||||
@@ -373,71 +427,33 @@ class Cursor(object):
|
||||
back or committed independently. You may consider the use of a
|
||||
dedicated temporary cursor to do some database operation.
|
||||
"""
|
||||
self._event_handlers[event].append(func)
|
||||
|
||||
def _pop_event_handlers(self):
|
||||
# return the current handlers, and reset them on self
|
||||
result = self._event_handlers
|
||||
self._event_handlers = {'commit': [], 'rollback': []}
|
||||
return result
|
||||
if event == 'commit':
|
||||
self.postcommit.add(func)
|
||||
elif event == 'rollback':
|
||||
self.postrollback.add(func)
|
||||
|
||||
@check
|
||||
def commit(self):
|
||||
""" Perform an SQL `COMMIT`
|
||||
"""
|
||||
""" Perform an SQL `COMMIT` """
|
||||
flush_env(self)
|
||||
self.precommit()
|
||||
result = self._cnx.commit()
|
||||
for func in self._pop_event_handlers()['commit']:
|
||||
func()
|
||||
self.prerollback.clear()
|
||||
self.postrollback.clear()
|
||||
self.postcommit()
|
||||
return result
|
||||
|
||||
@check
|
||||
def rollback(self):
|
||||
""" Perform an SQL `ROLLBACK`
|
||||
"""
|
||||
""" Perform an SQL `ROLLBACK` """
|
||||
clear_env(self)
|
||||
self.precommit.clear()
|
||||
self.postcommit.clear()
|
||||
self.prerollback()
|
||||
result = self._cnx.rollback()
|
||||
for func in self._pop_event_handlers()['rollback']:
|
||||
func()
|
||||
self.postrollback()
|
||||
return result
|
||||
|
||||
def __enter__(self):
|
||||
""" Using the cursor as a contextmanager automatically commits and
|
||||
closes it::
|
||||
|
||||
with cr:
|
||||
cr.execute(...)
|
||||
|
||||
# cr is committed if no failure occurred
|
||||
# cr is closed in any case
|
||||
"""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is None:
|
||||
self.commit()
|
||||
self.close()
|
||||
|
||||
@contextmanager
|
||||
@check
|
||||
def savepoint(self, flush=True):
|
||||
"""context manager entering in a new savepoint"""
|
||||
name = uuid.uuid1().hex
|
||||
if flush:
|
||||
flush_env(self)
|
||||
self.execute('SAVEPOINT "%s"' % name)
|
||||
try:
|
||||
yield
|
||||
if flush:
|
||||
flush_env(self)
|
||||
except Exception:
|
||||
if flush:
|
||||
clear_env(self)
|
||||
self.execute('ROLLBACK TO SAVEPOINT "%s"' % name)
|
||||
raise
|
||||
else:
|
||||
self.execute('RELEASE SAVEPOINT "%s"' % name)
|
||||
|
||||
@check
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._obj, name)
|
||||
@@ -447,7 +463,7 @@ class Cursor(object):
|
||||
return self._closed
|
||||
|
||||
|
||||
class TestCursor(object):
|
||||
class TestCursor(BaseCursor):
|
||||
""" A pseudo-cursor to be used for tests, on top of a real cursor. It keeps
|
||||
the transaction open across requests, and simulates committing, rolling
|
||||
back, and closing:
|
||||
@@ -488,21 +504,27 @@ class TestCursor(object):
|
||||
def autocommit(self, on):
|
||||
_logger.debug("TestCursor.autocommit(%r) does nothing", on)
|
||||
|
||||
@check
|
||||
def commit(self):
|
||||
""" Perform an SQL `COMMIT` """
|
||||
flush_env(self)
|
||||
self.precommit()
|
||||
self._cursor.execute('SAVEPOINT "%s"' % self._savepoint)
|
||||
self.prerollback.clear()
|
||||
# ignore post-commit/rollback hooks
|
||||
self.postcommit.clear()
|
||||
self.postrollback.clear()
|
||||
|
||||
@check
|
||||
def rollback(self):
|
||||
""" Perform an SQL `ROLLBACK` """
|
||||
clear_env(self)
|
||||
self.precommit.clear()
|
||||
self.prerollback()
|
||||
self._cursor.execute('ROLLBACK TO SAVEPOINT "%s"' % self._savepoint)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
if exc_type is None:
|
||||
self.commit()
|
||||
self.close()
|
||||
# ignore post-commit/rollback hooks
|
||||
self.postcommit.clear()
|
||||
self.postrollback.clear()
|
||||
|
||||
def __getattr__(self, name):
|
||||
value = getattr(self._cursor, name)
|
||||
|
||||
@@ -382,10 +382,12 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
|
||||
expected = counters.get(login, default)
|
||||
if flush:
|
||||
self.env.user.flush()
|
||||
self.env.cr.precommit()
|
||||
count0 = self.cr.sql_log_count
|
||||
yield
|
||||
if flush:
|
||||
self.env.user.flush()
|
||||
self.env.cr.precommit()
|
||||
count = self.cr.sql_log_count - count0
|
||||
if count != expected:
|
||||
# add some info on caller to allow semi-automatic update of query count
|
||||
@@ -404,9 +406,11 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
|
||||
# same operations, otherwise the caches might not be ready!
|
||||
if flush:
|
||||
self.env.user.flush()
|
||||
self.env.cr.precommit()
|
||||
yield
|
||||
if flush:
|
||||
self.env.user.flush()
|
||||
self.env.cr.precommit()
|
||||
|
||||
def assertRecordValues(self, records, expected_values):
|
||||
''' Compare a recordset with a list of dictionaries representing the expected results.
|
||||
|
||||
@@ -1088,6 +1088,56 @@ class LastOrderedSet(OrderedSet):
|
||||
OrderedSet.add(self, elem)
|
||||
|
||||
|
||||
class GroupCalls:
|
||||
""" A collection of callbacks with support for aggregated arguments. Upon
|
||||
call, every registered function is called once with positional arguments.
|
||||
When registering a function, a tuple of positional arguments is returned, so
|
||||
that the caller can modify the arguments in place. This allows to
|
||||
accumulate some data to process once::
|
||||
|
||||
callbacks = GroupCalls()
|
||||
|
||||
# register print (by default with a list)
|
||||
[args] = callbacks.register(print, list)
|
||||
args.append(42)
|
||||
|
||||
# add an element to the list to print
|
||||
[args] = callbacks.register(print, list)
|
||||
args.append(43)
|
||||
|
||||
# print "[42, 43]"
|
||||
callbacks()
|
||||
"""
|
||||
def __init__(self):
|
||||
self._func_args = {} # {func: args}
|
||||
|
||||
def __call__(self):
|
||||
""" Call all the registered functions (in first addition order) with
|
||||
their respective arguments. Only recurrent functions remain registered
|
||||
after the call.
|
||||
"""
|
||||
func_args = self._func_args
|
||||
while func_args:
|
||||
func = next(iter(func_args))
|
||||
args = func_args.pop(func)
|
||||
func(*args)
|
||||
|
||||
def add(self, func, *types):
|
||||
""" Register the given function, and return the tuple of positional
|
||||
arguments to call the function with. If the function is not registered
|
||||
yet, the list of arguments is made up by invoking the given types.
|
||||
"""
|
||||
try:
|
||||
return self._func_args[func]
|
||||
except KeyError:
|
||||
args = self._func_args[func] = [type_() for type_ in types]
|
||||
return args
|
||||
|
||||
def clear(self):
|
||||
""" Remove all callbacks from self. """
|
||||
self._func_args.clear()
|
||||
|
||||
|
||||
class IterableGenerator:
|
||||
""" An iterable object based on a generator function, which is called each
|
||||
time the object is iterated over.
|
||||
|
||||
Reference in New Issue
Block a user