+4
-2
@@ -721,9 +721,11 @@
|
||||
if (!(selection instanceof Array) &&
|
||||
!(key in this._values2selection)) {
|
||||
if (!jQuery.isEmptyObject(this.attributes.selection_change_with)) {
|
||||
prm = this.model.execute(selection, [value]);
|
||||
prm = this.model.execute(
|
||||
selection, [value], {}, true, false);
|
||||
} else {
|
||||
prm = this.model.execute(selection, []);
|
||||
prm = this.model.execute(
|
||||
selection, [], {}, true, false);
|
||||
}
|
||||
prm = prm.then(selection => {
|
||||
this._values2selection[key] = selection;
|
||||
|
||||
+1
-1
@@ -1226,7 +1226,7 @@
|
||||
try {
|
||||
result = this.model.execute(
|
||||
'autocomplete_' + fieldname, [values], this.get_context(),
|
||||
false);
|
||||
false, false);
|
||||
} catch (e) {
|
||||
result = [];
|
||||
}
|
||||
|
||||
@@ -113,6 +113,11 @@
|
||||
};
|
||||
|
||||
var ajax_error = function(query, status_, error) {
|
||||
if (!process_exception) {
|
||||
console.debug(`RPC error calling ${args}: ${status_}: ${error}.`);
|
||||
dfd.reject();
|
||||
return;
|
||||
}
|
||||
if (query.status == 401) {
|
||||
//Try to relog
|
||||
Sao.Session.renew(session).then(function() {
|
||||
@@ -123,6 +128,11 @@
|
||||
dfd.resolve();
|
||||
}
|
||||
}, dfd.reject);
|
||||
} else if (query.status == 408) {
|
||||
Sao.common.message.run(
|
||||
Sao.i18n.gettext('Request times out. Try again later.'),
|
||||
'tryton-error')
|
||||
.always(dfd.reject);
|
||||
} else if (query.status == 429) {
|
||||
Sao.common.message.run(
|
||||
Sao.i18n.gettext('Too many requests. Try again later.'),
|
||||
|
||||
+1
-1
@@ -1021,7 +1021,7 @@ var Sao = {
|
||||
}
|
||||
return ir_model.execute('global_search',
|
||||
[text, Sao.config.limit, Sao.main_menu_screen.model_name],
|
||||
Sao.main_menu_screen.context)
|
||||
Sao.main_menu_screen.context, true, false)
|
||||
.then(s_results => {
|
||||
var results = [];
|
||||
for (const result of s_results) {
|
||||
|
||||
+4
-3
@@ -1068,7 +1068,8 @@
|
||||
(ids.length == this.limit)) {
|
||||
count_prm = this.model.execute(
|
||||
'search_count',
|
||||
[domain, 0, this.count_limit], context)
|
||||
[domain, 0, this.count_limit], context,
|
||||
true, false)
|
||||
.then(count => {
|
||||
this.search_count = count;
|
||||
return this.search_count;
|
||||
@@ -1807,10 +1808,10 @@
|
||||
values[p] = null;
|
||||
}
|
||||
selection = this.model.execute(props.selection,
|
||||
[values], undefined, false);
|
||||
[values], undefined, false, true);
|
||||
} else {
|
||||
selection = this.model.execute(props.selection,
|
||||
[], undefined, false);
|
||||
[], undefined, false, true);
|
||||
}
|
||||
return selection.sort(function(a, b) {
|
||||
return a[1].localeCompare(b[1]);
|
||||
|
||||
@@ -1076,11 +1076,12 @@ function eval_pyson(value){
|
||||
'method': (
|
||||
'model.' + action.res_model + '.search_count'),
|
||||
'params': [
|
||||
['AND', domain, tab_domain], 0, 100, context],
|
||||
}, Sao.Session.current_session).then(value => {
|
||||
this._set_count(
|
||||
value, i, current, counter,
|
||||
action.name, tab_domains);
|
||||
['AND', domain, 0, tab_domain], 100, context],
|
||||
}, Sao.Session.current_session, true, false).then(
|
||||
value => {
|
||||
this._set_count(
|
||||
value, i, current, counter,
|
||||
action.name, tab_domains);
|
||||
});
|
||||
promesses.push(prm);
|
||||
}, this);
|
||||
@@ -1089,7 +1090,8 @@ function eval_pyson(value){
|
||||
'method': (
|
||||
'model.' + action.res_model + '.search_count'),
|
||||
'params': [domain, 0, 100, context],
|
||||
}, Sao.Session.current_session).then(value => {
|
||||
}, Sao.Session.current_session, true, false
|
||||
).then(value => {
|
||||
this._set_count(
|
||||
value, 0, current, counter,
|
||||
action.name, tab_domains);
|
||||
|
||||
@@ -1014,6 +1014,10 @@ def process_exception(exception, *args, **kwargs):
|
||||
PLOCK.release()
|
||||
if args:
|
||||
return rpc_execute(*args)
|
||||
elif exception.faultCode == str(int(HTTPStatus.REQUEST_TIMEOUT)):
|
||||
message(
|
||||
_("Request times out. Try again later."),
|
||||
msg_type=Gtk.MessageType.ERROR)
|
||||
elif exception.faultCode == str(int(HTTPStatus.TOO_MANY_REQUESTS)):
|
||||
message(
|
||||
_('Too many requests. Try again later.'),
|
||||
|
||||
@@ -30,10 +30,13 @@ class SelectionMixin(object):
|
||||
and key not in self._values2selection):
|
||||
try:
|
||||
if self.attrs.get('selection_change_with'):
|
||||
selection = RPCExecute('model', self.model_name, selection,
|
||||
value)
|
||||
selection = RPCExecute(
|
||||
'model', self.model_name, selection, value,
|
||||
process_exception=False)
|
||||
else:
|
||||
selection = RPCExecute('model', self.model_name, selection)
|
||||
selection = RPCExecute(
|
||||
'model', self.model_name, selection,
|
||||
process_exception=False)
|
||||
except RPCException:
|
||||
selection = []
|
||||
self._values2selection[key] = selection
|
||||
|
||||
@@ -23,7 +23,8 @@ from tryton.common import RPCContextReload, RPCException, RPCExecute
|
||||
from tryton.common.cellrendererclickablepixbuf import (
|
||||
CellRendererClickablePixbuf)
|
||||
from tryton.config import CONFIG, TRYTON_ICON, get_config_dir
|
||||
from tryton.exceptions import TrytonError, TrytonServerUnavailable
|
||||
from tryton.exceptions import (
|
||||
TrytonError, TrytonServerError, TrytonServerUnavailable)
|
||||
from tryton.gui.window import Window
|
||||
from tryton.jsonrpc import object_hook
|
||||
from tryton.pyson import PYSONDecoder
|
||||
@@ -375,7 +376,7 @@ class Main(Gtk.Application):
|
||||
def set_result(result):
|
||||
try:
|
||||
result = result()
|
||||
except RPCException:
|
||||
except (RPCException, TrytonServerError):
|
||||
result = []
|
||||
if search_text != widget.get_text():
|
||||
if callback:
|
||||
@@ -401,7 +402,8 @@ class Main(Gtk.Application):
|
||||
|
||||
RPCExecute('model', 'ir.model', 'global_search', search_text,
|
||||
CONFIG['client.limit'], self.menu_screen.model_name,
|
||||
context=self.menu_screen.context, callback=set_result)
|
||||
context=self.menu_screen.context, callback=set_result,
|
||||
process_exception=False)
|
||||
return False
|
||||
|
||||
def changed(widget):
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import tryton.common as common
|
||||
from tryton.common import RPCException, RPCExecute
|
||||
from tryton.config import CONFIG
|
||||
from tryton.exceptions import TrytonServerError
|
||||
from tryton.pyson import PYSONDecoder
|
||||
|
||||
from . import field as fields
|
||||
@@ -677,8 +678,9 @@ class Record:
|
||||
try:
|
||||
res = RPCExecute(
|
||||
'model', self.model_name,
|
||||
'autocomplete_' + fieldname, args, context=self.get_context())
|
||||
except RPCException:
|
||||
'autocomplete_' + fieldname, args, context=self.get_context(),
|
||||
process_exception=False)
|
||||
except (RPCException, TrytonServerError):
|
||||
# ensure res is a list
|
||||
res = []
|
||||
self.autocompletion[fieldname] = res
|
||||
|
||||
@@ -21,6 +21,7 @@ from tryton.common import (
|
||||
from tryton.common.domain_inversion import canonicalize
|
||||
from tryton.common.domain_parser import DomainParser
|
||||
from tryton.config import CONFIG
|
||||
from tryton.exceptions import TrytonServerError
|
||||
from tryton.gui.window.infobar import InfoBar
|
||||
from tryton.gui.window.view_form.model.group import Group
|
||||
from tryton.gui.window.view_form.view import View
|
||||
@@ -308,8 +309,9 @@ class Screen:
|
||||
try:
|
||||
self.search_count = RPCExecute(
|
||||
'model', self.model_name, 'search_count',
|
||||
domain, 0, self.count_limit, context=context)
|
||||
except RPCException:
|
||||
domain, 0, self.count_limit, context=context,
|
||||
process_exception=False)
|
||||
except (RPCException, TrytonServerError):
|
||||
self.search_count = 0
|
||||
else:
|
||||
self.search_count = len(ids)
|
||||
|
||||
@@ -7,6 +7,7 @@ from gi.repository import Gtk
|
||||
import tryton.common as common
|
||||
from tryton.action import Action
|
||||
from tryton.config import CONFIG
|
||||
from tryton.exceptions import TrytonServerError
|
||||
from tryton.pyson import PYSONDecoder
|
||||
|
||||
|
||||
@@ -189,14 +190,16 @@ class Link(StateMixin, Gtk.Button):
|
||||
['AND', domain, tab_domain], 0, 100, context=context,
|
||||
callback=functools.partial(
|
||||
self._set_count, idx=i, current=self._current,
|
||||
counter=counter, label=label))
|
||||
counter=counter, label=label),
|
||||
process_exception=False)
|
||||
else:
|
||||
common.RPCExecute(
|
||||
'model', action['res_model'], 'search_count',
|
||||
domain, 0, 100, context=context,
|
||||
callback=functools.partial(
|
||||
self._set_count, current=self._current,
|
||||
counter=counter, label=label))
|
||||
counter=counter, label=label),
|
||||
process_exception=False)
|
||||
|
||||
def _set_count(self, value, idx=0, current=None, counter=None, label=''):
|
||||
if current != self._current or not self.get_parent():
|
||||
@@ -206,7 +209,7 @@ class Link(StateMixin, Gtk.Button):
|
||||
if count > 99:
|
||||
count = '99+'
|
||||
counter[idx] = count
|
||||
except common.RPCException:
|
||||
except (common.RPCException, TrytonServerError):
|
||||
pass
|
||||
self._set_label_counter(label, counter)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
* Add timeout parameter on RPC definitions
|
||||
|
||||
Version 7.0.0 - 2023-10-30
|
||||
--------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
RPC
|
||||
===
|
||||
|
||||
.. class:: RPC([readonly[, instantiate[, [decorator, result[, check_access[, unique[, fresh_session[, cache]]]]]]]])
|
||||
.. class:: RPC([readonly[, instantiate[, [decorator, result[, check_access[, unique[, fresh_session[, cache[, timeout]]]]]]]]])
|
||||
|
||||
Define the behavior of Remote Procedure Call.
|
||||
|
||||
@@ -46,6 +46,12 @@ Instance attributes are:
|
||||
|
||||
A :class:`RPCCache` instance to compute the cache duration for the answer.
|
||||
|
||||
.. attribute:: RPC.timeout
|
||||
|
||||
The timeout in second for the call.
|
||||
``0`` or ``None`` means no timeout is applied.
|
||||
Default is ``None``.
|
||||
|
||||
|
||||
RPCCache
|
||||
--------
|
||||
|
||||
@@ -271,6 +271,13 @@ The maximum size in bytes of an authenticated request (zero means no limit).
|
||||
|
||||
Default: 2GB
|
||||
|
||||
timeout
|
||||
~~~~~~~
|
||||
|
||||
The timeout in seconds before aborting requests that have their execution time
|
||||
depending on the parameters.
|
||||
|
||||
Default: ``60``
|
||||
|
||||
cache
|
||||
-----
|
||||
|
||||
@@ -12,7 +12,8 @@ from trytond.config import config
|
||||
|
||||
__all__ = [
|
||||
'name', 'Database', 'TableHandler',
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError']
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError',
|
||||
'DatabaseTimeoutError']
|
||||
|
||||
|
||||
name = urllib.parse.urlparse(config.get('database', 'uri', default='')).scheme
|
||||
@@ -34,4 +35,5 @@ Database = _module.Database
|
||||
DatabaseIntegrityError = _module.DatabaseIntegrityError
|
||||
DatabaseDataError = _module.DatabaseDataError
|
||||
DatabaseOperationalError = _module.DatabaseOperationalError
|
||||
DatabaseTimeoutError = _module.DatabaseTimeoutError
|
||||
TableHandler = _module.TableHandler
|
||||
|
||||
@@ -6,6 +6,7 @@ from sql import For
|
||||
|
||||
DatabaseIntegrityError = None
|
||||
DatabaseOperationalError = None
|
||||
DatabaseTimeoutError = None
|
||||
|
||||
SQLType = namedtuple('SQLType', 'base type')
|
||||
|
||||
@@ -26,7 +27,8 @@ class DatabaseInterface(object):
|
||||
def connect(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_connection(self, autocommit=False, readonly=False):
|
||||
def get_connection(
|
||||
self, autocommit=False, readonly=False, statement_timeout=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def put_connection(self, connection, close=False):
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
from .database import (
|
||||
Database, DatabaseDataError, DatabaseIntegrityError,
|
||||
DatabaseOperationalError)
|
||||
DatabaseOperationalError, DatabaseTimeoutError)
|
||||
from .table import TableHandler
|
||||
|
||||
__all__ = [
|
||||
Database, TableHandler,
|
||||
DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError]
|
||||
DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError,
|
||||
DatabaseTimeoutError]
|
||||
|
||||
@@ -26,6 +26,7 @@ from psycopg2 import DataError as DatabaseDataError
|
||||
from psycopg2 import IntegrityError as DatabaseIntegrityError
|
||||
from psycopg2 import OperationalError as DatabaseOperationalError
|
||||
from psycopg2 import ProgrammingError
|
||||
from psycopg2.errors import QueryCanceled as DatabaseTimeoutError
|
||||
from psycopg2.extras import register_default_json, register_default_jsonb
|
||||
from sql import Cast, Flavor, For, Table
|
||||
from sql.conditionals import Coalesce
|
||||
@@ -38,7 +39,8 @@ from trytond.tools.gevent import is_gevent_monkey_patched
|
||||
|
||||
__all__ = [
|
||||
'Database',
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError']
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError',
|
||||
'DatabaseTimeoutError']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -254,7 +256,8 @@ class Database(DatabaseInterface):
|
||||
def connect(self):
|
||||
return self
|
||||
|
||||
def get_connection(self, autocommit=False, readonly=False):
|
||||
def get_connection(
|
||||
self, autocommit=False, readonly=False, statement_timeout=None):
|
||||
retry = max(config.getint('database', 'retry'), _maxconn)
|
||||
for count in range(retry, -1, -1):
|
||||
try:
|
||||
@@ -274,8 +277,13 @@ class Database(DatabaseInterface):
|
||||
isolation_level=ISOLATION_LEVEL_REPEATABLE_READ,
|
||||
readonly=readonly,
|
||||
autocommit=autocommit)
|
||||
# Detect disconnection
|
||||
conn.cursor().execute('SELECT 1')
|
||||
with conn.cursor() as cur:
|
||||
if statement_timeout:
|
||||
cur.execute('SET statement_timeout=%s' %
|
||||
(statement_timeout * 1000))
|
||||
else:
|
||||
# Detect disconnection
|
||||
cur.execute('SELECT 1')
|
||||
except DatabaseOperationalError:
|
||||
self._connpool.putconn(conn, close=True)
|
||||
continue
|
||||
@@ -283,6 +291,7 @@ class Database(DatabaseInterface):
|
||||
return conn
|
||||
|
||||
def put_connection(self, connection, close=False):
|
||||
connection.reset()
|
||||
self._connpool.putconn(connection, close=close)
|
||||
|
||||
def close(self):
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
from .database import (
|
||||
Database, DatabaseDataError, DatabaseIntegrityError,
|
||||
DatabaseOperationalError)
|
||||
DatabaseOperationalError, DatabaseTimeoutError)
|
||||
from .table import TableHandler
|
||||
|
||||
__all__ = [
|
||||
Database, TableHandler,
|
||||
DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError]
|
||||
DatabaseIntegrityError, DatabaseDataError, DatabaseOperationalError,
|
||||
DatabaseTimeoutError]
|
||||
|
||||
@@ -29,7 +29,8 @@ from trytond.transaction import Transaction
|
||||
|
||||
__all__ = [
|
||||
'Database',
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError']
|
||||
'DatabaseIntegrityError', 'DatabaseDataError', 'DatabaseOperationalError',
|
||||
'DatabaseTimeoutError']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_default_name = config.get('database', 'default_name', default=':memory:')
|
||||
@@ -39,6 +40,10 @@ class DatabaseDataError(DatabaseError):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseTimeoutError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SQLiteExtract(Function):
|
||||
__slots__ = ()
|
||||
_function = 'EXTRACT'
|
||||
@@ -445,7 +450,8 @@ class Database(DatabaseInterface):
|
||||
db_uri = urllib.parse.urlunparse(db_uri)
|
||||
return db_uri.replace('sqlite', 'file', 1)
|
||||
|
||||
def get_connection(self, autocommit=False, readonly=False):
|
||||
def get_connection(
|
||||
self, autocommit=False, readonly=False, statement_timeout=None):
|
||||
if self._conn is None:
|
||||
self.connect()
|
||||
if autocommit:
|
||||
|
||||
@@ -70,6 +70,7 @@ class TrytonConfigParser(configparser.ConfigParser):
|
||||
self.set('request', 'max_size', str(2 * 1024 * 1024))
|
||||
self.set('request', 'max_size_authenticated',
|
||||
str(2 * 1024 * 1024 * 1024))
|
||||
self.set('request', 'timeout', str(60))
|
||||
self.add_section('cache')
|
||||
self.set('cache', 'transaction', '10')
|
||||
self.set('cache', 'model', '200')
|
||||
|
||||
@@ -13,6 +13,7 @@ from sql.conditionals import Case
|
||||
from sql.operators import Equal
|
||||
|
||||
from trytond.cache import Cache
|
||||
from trytond.config import config
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import (
|
||||
DeactivableMixin, EvalEnvironment, Exclude, Index, ModelSingleton,
|
||||
@@ -31,6 +32,7 @@ from trytond.wizard import Button, StateAction, StateView, Wizard
|
||||
from .resource import ResourceAccessMixin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_request_timeout = config.getint('request', 'timeout', default=0)
|
||||
|
||||
|
||||
class ConditionError(ValidationError):
|
||||
@@ -77,7 +79,7 @@ class Model(ModelSQL, ModelView):
|
||||
'list_history': RPC(),
|
||||
'get_notification': RPC(),
|
||||
'get_names': RPC(),
|
||||
'global_search': RPC(),
|
||||
'global_search': RPC(timeout=_request_timeout),
|
||||
})
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -39,6 +39,7 @@ _cache_count_timeout = config.getint(
|
||||
'cache', 'count_timeout', default=60 * 60 * 24)
|
||||
_cache_count_clear = config.getint(
|
||||
'cache', 'count_clear', default=1000)
|
||||
_request_timeout = config.getint('request', 'timeout', default=0)
|
||||
|
||||
|
||||
def local_cache(Model, transaction=None):
|
||||
@@ -136,11 +137,15 @@ class ModelStorage(Model):
|
||||
'delete': RPC(readonly=False, instantiate=0),
|
||||
'copy': RPC(readonly=False, instantiate=0, unique=False,
|
||||
result=lambda r: list(map(int, r))),
|
||||
'search': RPC(result=lambda r: list(map(int, r))),
|
||||
'search_count': RPC(),
|
||||
'search_read': RPC(),
|
||||
'resources': RPC(instantiate=0, unique=False),
|
||||
'export_data_domain': RPC(),
|
||||
'search': RPC(
|
||||
result=lambda r: list(map(int, r)),
|
||||
timeout=_request_timeout),
|
||||
'search_count': RPC(timeout=_request_timeout),
|
||||
'search_read': RPC(timeout=_request_timeout),
|
||||
'resources': RPC(
|
||||
instantiate=0, unique=False,
|
||||
timeout=_request_timeout),
|
||||
'export_data_domain': RPC(timeout=_request_timeout),
|
||||
'export_data': RPC(instantiate=0, unique=False),
|
||||
'import_data': RPC(readonly=False),
|
||||
})
|
||||
|
||||
@@ -184,7 +184,8 @@ def _dispatch(request, pool, *args, **kwargs):
|
||||
if count:
|
||||
time.sleep(0.02 * (retry - count))
|
||||
with Transaction().start(
|
||||
pool.database_name, user, readonly=rpc.readonly,
|
||||
pool.database_name, user,
|
||||
readonly=rpc.readonly, timeout=rpc.timeout,
|
||||
**transaction_extras) as transaction:
|
||||
try:
|
||||
c_args, c_kwargs, transaction.context, transaction.timestamp \
|
||||
@@ -207,6 +208,9 @@ def _dispatch(request, pool, *args, **kwargs):
|
||||
transaction.tasks.clear()
|
||||
e.fix(transaction_extras)
|
||||
continue
|
||||
except backend.DatabaseTimeoutError:
|
||||
logger.warning(log_message, *log_args, exc_info=True)
|
||||
abort(HTTPStatus.REQUEST_TIMEOUT)
|
||||
except backend.DatabaseOperationalError:
|
||||
if count < retry and not rpc.readonly:
|
||||
transaction.rollback()
|
||||
|
||||
@@ -21,11 +21,12 @@ class RPC(object):
|
||||
|
||||
__slots__ = (
|
||||
'readonly', 'instantiate', 'decorator', 'result',
|
||||
'check_access', 'fresh_session', 'unique', 'cache')
|
||||
'check_access', 'fresh_session', 'unique', 'cache', 'timeout')
|
||||
|
||||
def __init__(
|
||||
self, readonly=True, instantiate=None, decorator=None, result=None,
|
||||
check_access=True, fresh_session=False, unique=True, cache=None):
|
||||
check_access=True, fresh_session=False, unique=True, cache=None,
|
||||
timeout=None):
|
||||
self.readonly = readonly
|
||||
self.instantiate = instantiate
|
||||
self.decorator = decorator
|
||||
@@ -40,6 +41,7 @@ class RPC(object):
|
||||
if not isinstance(cache, RPCCache):
|
||||
cache = RPCCache(**cache)
|
||||
self.cache = cache
|
||||
self.timeout = timeout
|
||||
|
||||
def convert(self, obj, *args, **kwargs):
|
||||
args = list(args)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from trytond import backend
|
||||
from trytond.tests.test_tryton import CONTEXT, DB_NAME, USER, activate_module
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
@@ -121,3 +122,26 @@ class TransactionTestCase(unittest.TestCase):
|
||||
dm.tpc_vote.assert_not_called()
|
||||
dm.tpc_abort.assert_called_once_with(transaction)
|
||||
dm.tpc_finish.assert_not_called()
|
||||
|
||||
@unittest.skipUnless(backend.name == 'postgresql', "Test pg_settings")
|
||||
def test_postgresl_statement_timeout(self):
|
||||
get_timeout = (
|
||||
"SELECT setting FROM pg_settings "
|
||||
"WHERE name='statement_timeout'")
|
||||
|
||||
with Transaction().start(DB_NAME, USER) as transaction:
|
||||
cursor = transaction.connection.cursor()
|
||||
cursor.execute(get_timeout)
|
||||
self.assertEqual('0', cursor.fetchone()[0])
|
||||
|
||||
with Transaction().start(DB_NAME, USER, timeout=1) as transaction:
|
||||
cursor = transaction.connection.cursor()
|
||||
cursor.execute(get_timeout)
|
||||
self.assertEqual('1000', cursor.fetchone()[0])
|
||||
|
||||
@unittest.skipUnless(backend.name == 'postgresql', "Use pg_sleep")
|
||||
def test_postgresql_statement_timeout_exception(self):
|
||||
with self.assertRaises(backend.DatabaseTimeoutError):
|
||||
with Transaction().start(DB_NAME, USER, timeout=1) as transaction:
|
||||
cursor = transaction.connection.cursor()
|
||||
cursor.execute("SELECT pg_sleep(2)")
|
||||
|
||||
@@ -159,7 +159,7 @@ class Transaction(object):
|
||||
return cache
|
||||
|
||||
def start(self, database_name, user, readonly=False, context=None,
|
||||
close=False, autocommit=False, **extras):
|
||||
close=False, autocommit=False, timeout=None, **extras):
|
||||
'''
|
||||
Start transaction
|
||||
'''
|
||||
@@ -197,7 +197,7 @@ class Transaction(object):
|
||||
if count:
|
||||
time.sleep(0.002 * (_retry - count))
|
||||
self.connection = database.get_connection(readonly=readonly,
|
||||
autocommit=autocommit)
|
||||
autocommit=autocommit, statement_timeout=timeout)
|
||||
try:
|
||||
lock_tables = extras.get('_lock_tables', [])
|
||||
for table in lock_tables:
|
||||
|
||||
Reference in New Issue
Block a user