Manage errors during non-interactive operations
issue10667 review346781002
This commit is contained in:
1 parent
216a7a9f50
commit
b843e4e5bf
12 files changed
+333
-1
No files matched your search
@@ -1,3 +1,4 @@
|
||||
* Manage errors during non-interactive operations
|
||||
* Add estimation count to ModelStorage
|
||||
* Support empty digits in format_number of Report
|
||||
* Add digits mixin
|
||||
|
||||
@@ -284,6 +284,16 @@ The number of days after which processed tasks are removed.
|
||||
|
||||
Default: ``30``
|
||||
|
||||
error
|
||||
-----
|
||||
|
||||
clean_days
|
||||
~~~~~~~~~~
|
||||
|
||||
The number of days after which reported errors are removed.
|
||||
|
||||
Default: ``90``
|
||||
|
||||
table
|
||||
-----
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import calendar_
|
||||
from . import message
|
||||
from . import email_
|
||||
from . import routes
|
||||
from . import error
|
||||
|
||||
__all__ = ['register', 'routes']
|
||||
|
||||
@@ -102,6 +103,7 @@ def register():
|
||||
email_.EmailAddress,
|
||||
email_.EmailTemplate,
|
||||
email_.EmailTemplate_Report,
|
||||
error.Error,
|
||||
module='ir', type_='model')
|
||||
Pool.register(
|
||||
translation.TranslationSet,
|
||||
|
||||
+9
-1
@@ -7,6 +7,7 @@ import logging
|
||||
|
||||
from trytond import backend
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import UserError, UserWarning
|
||||
from trytond.model import (
|
||||
ModelView, ModelSQL, DeactivableMixin, fields, dualmethod)
|
||||
from trytond.pool import Pool
|
||||
@@ -69,6 +70,7 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
method = fields.Selection([
|
||||
('ir.trigger|trigger_time', "Run On Time Triggers"),
|
||||
('ir.queue|clean', "Clean Task Queue"),
|
||||
('ir.error|clean', "Clean Errors"),
|
||||
], "Method", required=True)
|
||||
|
||||
@classmethod
|
||||
@@ -147,6 +149,8 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
now = datetime.datetime.now()
|
||||
retry = config.getint('database', 'retry')
|
||||
with transaction.start(db_name, 0, context={'_skip_warnings': True}):
|
||||
pool = Pool()
|
||||
Error = pool.get('ir.error')
|
||||
transaction.database.lock(transaction.connection, cls._table)
|
||||
crons = cls.search(['OR',
|
||||
('next_call', '<=', now),
|
||||
@@ -168,7 +172,11 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
if (isinstance(e, backend.DatabaseOperationalError)
|
||||
and count):
|
||||
continue
|
||||
logger.error('%s failed', name, exc_info=True)
|
||||
if isinstance(e, (UserError, UserWarning)):
|
||||
Error.log(cron, e)
|
||||
logger.info('%s failed', name)
|
||||
else:
|
||||
logger.critical('%s failed', name, exc_info=True)
|
||||
cron.next_call = cron.compute_next_call(now)
|
||||
cron.save()
|
||||
transaction.commit()
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
import datetime as dt
|
||||
import functools
|
||||
import logging
|
||||
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import UserError, UserWarning
|
||||
from trytond.model import ModelSQL, ModelView, Workflow, fields
|
||||
from trytond.tools import firstline
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
clean_days = config.getint('error', 'clean_days', default=90)
|
||||
|
||||
|
||||
def set_user(field):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(cls, records, *args, **kwargs):
|
||||
result = func(cls, records, *args, **kwargs)
|
||||
cls.write(
|
||||
[r for r in records
|
||||
if not getattr(r, field)], {
|
||||
field: Transaction().user,
|
||||
})
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def reset_user(*fields):
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(cls, records, *args, **kwargs):
|
||||
result = func(cls, records, *args, **kwargs)
|
||||
cls.write(records, {f: None for f in fields})
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class Error(Workflow, ModelView, ModelSQL):
|
||||
"Error"
|
||||
__name__ = 'ir.error'
|
||||
|
||||
origin = fields.Reference("Origin", [
|
||||
('ir.cron', "Action"),
|
||||
('ir.queue', "Task"),
|
||||
], readonly=True)
|
||||
origin_string = origin.translated('origin')
|
||||
message = fields.Text("Message", readonly=True)
|
||||
description = fields.Text("Description", readonly=True)
|
||||
summary = fields.Function(fields.Char("Summary"), 'on_change_with_summary')
|
||||
|
||||
processed_by = fields.Many2One(
|
||||
'res.user', "Processed by",
|
||||
states={
|
||||
'readonly': Eval('state').in_(['processing', 'solved']),
|
||||
},
|
||||
depends=['state'])
|
||||
solved_by = fields.Many2One(
|
||||
'res.user', "Solved by",
|
||||
states={
|
||||
'readonly': Eval('state').in_(['solved']),
|
||||
},
|
||||
depends=['state'])
|
||||
|
||||
state = fields.Selection([
|
||||
('open', "Open"),
|
||||
('processing', "Processing"),
|
||||
('solved', "Solved"),
|
||||
], "State", readonly=True, select=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__setup__()
|
||||
cls._transitions |= {
|
||||
('open', 'processing'),
|
||||
('processing', 'solved'),
|
||||
('processing', 'open'),
|
||||
}
|
||||
cls._buttons.update({
|
||||
'open': {
|
||||
'invisible': Eval('state') != 'processing',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'process': {
|
||||
'invisible': Eval('state') != 'open',
|
||||
'depends': ['state'],
|
||||
},
|
||||
'solve': {
|
||||
'invisible': Eval('state') != 'processing',
|
||||
'depends': ['state'],
|
||||
},
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def default_state(cls):
|
||||
return 'open'
|
||||
|
||||
@fields.depends('message')
|
||||
def on_change_with_summary(self, name=None):
|
||||
return firstline(self.message or '')
|
||||
|
||||
def get_rec_name(self, name):
|
||||
if self.origin:
|
||||
return "%s - %s" % (self.origin_string, self.origin.rec_name)
|
||||
return super().get_rec_name(name)
|
||||
|
||||
@classmethod
|
||||
def log(cls, origin, exception):
|
||||
try:
|
||||
assert isinstance(exception, (UserError, UserWarning))
|
||||
with Transaction().new_transaction(autocommit=True):
|
||||
if not cls.search([
|
||||
('origin', '=', str(origin)),
|
||||
('message', '=', exception.message),
|
||||
('description', '=', exception.description),
|
||||
('state', '!=', 'solved'),
|
||||
]):
|
||||
cls.create([{
|
||||
'origin': str(origin),
|
||||
'message': exception.message,
|
||||
'description': exception.description,
|
||||
}])
|
||||
except Exception:
|
||||
logger.critical(
|
||||
"failed to store exception %s of %s", exception, origin,
|
||||
exc_info=True)
|
||||
|
||||
@classmethod
|
||||
def clean(cls, date=None):
|
||||
if date is None:
|
||||
date = (
|
||||
dt.datetime.now() - dt.timedelta(days=clean_days))
|
||||
errors = cls.search([('create_date', '<', date)])
|
||||
cls.delete(errors)
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('open')
|
||||
@reset_user('processed_by')
|
||||
def open(cls, errors):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('processing')
|
||||
@set_user('processed_by')
|
||||
def process(cls, errors):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@ModelView.button
|
||||
@Workflow.transition('solved')
|
||||
@set_user('solved_by')
|
||||
def solve(cls, errors):
|
||||
pool = Pool()
|
||||
Cron = pool.get('ir.cron')
|
||||
Queue = pool.get('ir.queue')
|
||||
for error in errors:
|
||||
if isinstance(error.origin, Cron):
|
||||
Cron.__queue__.run_once([error.origin])
|
||||
elif isinstance(error.origin, Queue):
|
||||
task = error.origin
|
||||
Queue.push(task.name, task.data)
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tryton>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="error_view_list">
|
||||
<field name="model">ir.error</field>
|
||||
<field name="type">tree</field>
|
||||
<field name="name">error_list</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="error_view_form">
|
||||
<field name="model">ir.error</field>
|
||||
<field name="type">form</field>
|
||||
<field name="name">error_form</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.action.act_window" id="act_error_form">
|
||||
<field name="name">Errors</field>
|
||||
<field name="res_model">ir.error</field>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_error_form_view1">
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="view" ref="error_view_list"/>
|
||||
<field name="act_window" ref="act_error_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.view" id="act_error_form_view2">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view" ref="error_view_form"/>
|
||||
<field name="act_window" ref="act_error_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_error_form_domain_open">
|
||||
<field name="name">Open</field>
|
||||
<field name="sequence" eval="10"/>
|
||||
<field name="domain" eval="[('state', '=', 'open')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_error_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_error_form_domain_processing">
|
||||
<field name="name">Processing</field>
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="domain" eval="[('state', '=', 'processing')]" pyson="1"/>
|
||||
<field name="count" eval="True"/>
|
||||
<field name="act_window" ref="act_error_form"/>
|
||||
</record>
|
||||
<record model="ir.action.act_window.domain" id="act_error_form_domain_all">
|
||||
<field name="name">All</field>
|
||||
<field name="sequence" eval="9999"/>
|
||||
<field name="domain"></field>
|
||||
<field name="act_window" ref="act_error_form"/>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
parent="ir.menu_scheduler"
|
||||
action="act_error_form"
|
||||
sequence="50"
|
||||
id="menu_error_form"/>
|
||||
|
||||
<record model="ir.model.button" id="error_open_button">
|
||||
<field name="name">open</field>
|
||||
<field name="string">Open</field>
|
||||
<field name="model" search="[('model', '=', 'ir.error')]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="error_process_button">
|
||||
<field name="name">process</field>
|
||||
<field name="string">Process</field>
|
||||
<field name="model" search="[('model', '=', 'ir.error')]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.button" id="error_solve_button">
|
||||
<field name="name">solve</field>
|
||||
<field name="string">Solve</field>
|
||||
<field name="model" search="[('model', '=', 'ir.error')]"/>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<record model="ir.cron" id="cron_error_clean">
|
||||
<field name="method">ir.error|clean</field>
|
||||
<field name="interval_number" eval="1"/>
|
||||
<field name="interval_type">months</field>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -222,6 +222,7 @@ class _Method(object):
|
||||
scheduled_at = now + scheduled_at
|
||||
expected_at = context.pop('queue_expected_at', None)
|
||||
context.pop('_check_access', None)
|
||||
context.pop('language', None)
|
||||
if expected_at is not None:
|
||||
expected_at = now + expected_at
|
||||
try:
|
||||
|
||||
@@ -21,3 +21,4 @@ xml:
|
||||
message.xml
|
||||
queue.xml
|
||||
email.xml
|
||||
error.xml
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<form>
|
||||
<label name="origin"/>
|
||||
<field name="rec_name" colspan="3"/>
|
||||
|
||||
<separator name="message" colspan="4"/>
|
||||
<field name="message" colspan="4"/>
|
||||
|
||||
<separator name="description" colspan="4"/>
|
||||
<field name="description" colspan="4"/>
|
||||
|
||||
<label name="state"/>
|
||||
<field name="state"/>
|
||||
<group col="-1" colspan="2" id="buttons">
|
||||
<button name="open" icon="tryton-back"/>
|
||||
<button name="process" icon="tryton-forward"/>
|
||||
<button name="solve" icon="tryton-close"/>
|
||||
</group>
|
||||
</form>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0"?>
|
||||
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
this repository contains the full copyright notices and license terms. -->
|
||||
<tree>
|
||||
<field name="rec_name" string="Origin" expand="1"/>
|
||||
<field name="summary" expand="2"/>
|
||||
<field name="state"/>
|
||||
<button name="open"/>
|
||||
<button name="process"/>
|
||||
<button name="solve"/>
|
||||
</tree>
|
||||
@@ -1067,5 +1067,21 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.model.access" id="access_ir_error">
|
||||
<field name="model" search="[('model', '=', 'ir.error')]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="False"/>
|
||||
</record>
|
||||
<record model="ir.model.access" id="access_ir_error_admin">
|
||||
<field name="model" search="[('model', '=', 'ir.error')]"/>
|
||||
<field name="group" ref="group_admin"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_delete" eval="True"/>
|
||||
</record>
|
||||
</data>
|
||||
</tryton>
|
||||
@@ -12,6 +12,7 @@ from sql import Flavor
|
||||
|
||||
from trytond import backend
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import UserError, UserWarning
|
||||
from trytond.pool import Pool
|
||||
from trytond.status import processing
|
||||
from trytond.transaction import Transaction
|
||||
@@ -99,6 +100,7 @@ def run_task(pool, task_id):
|
||||
if not isinstance(pool, Pool):
|
||||
pool = Pool(pool)
|
||||
Queue = pool.get('ir.queue')
|
||||
Error = pool.get('ir.error')
|
||||
name = '<Task %s@%s>' % (task_id, pool.database_name)
|
||||
logger.info('%s started', name)
|
||||
retry = config.getint('database', 'retry')
|
||||
@@ -121,6 +123,9 @@ def run_task(pool, task_id):
|
||||
transaction.rollback()
|
||||
continue
|
||||
raise
|
||||
except (UserError, UserWarning) as e:
|
||||
Error.log(task, e)
|
||||
raise
|
||||
logger.info('%s done', name)
|
||||
except backend.DatabaseOperationalError:
|
||||
logger.info('%s failed, retrying', name, exc_info=True)
|
||||
@@ -139,5 +144,7 @@ def run_task(pool, task_id):
|
||||
except Exception:
|
||||
logger.critical(
|
||||
'rescheduling %s failed', name, exc_info=True)
|
||||
except (UserError, UserWarning):
|
||||
logger.info('%s failed', name)
|
||||
except Exception:
|
||||
logger.critical('%s failed', name, exc_info=True)
|
||||
Reference in new issue
Block a user