Skip to content

Commit

Permalink
allow the @task_with_status_report decorator to accept a name to be u…
Browse files Browse the repository at this point in the history
…sed in monitoring (#193)

* allow the latest_task_run_monitor to accept a name

* changelog entry

* Grammar fix

Co-authored-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com>
  • Loading branch information
nhoening and Flix6x committed Sep 27, 2021
1 parent 2d22083 commit cb3de7c
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 10 deletions.
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Expand Up @@ -11,7 +11,6 @@ New features
-----------
* Set a logo for the top left corner with the new FLEXMEASURES_MENU_LOGO_PATH setting [see `PR #184 <http://www.github.com/SeitaBV/flexmeasures/pull/184>`_]
* Add an extra style-sheet which applies to all pages with the new FLEXMEASURES_EXTRA_CSS_PATH setting [see `PR #185 <http://www.github.com/SeitaBV/flexmeasures/pull/185>`_]


Bugfixes
-----------
Expand All @@ -21,6 +20,7 @@ Infrastructure / Support
----------------------
* FlexMeasures plugins can be Python packages now. We provide `a cookie-cutter template <https://github.com/SeitaBV/flexmeasures-plugin-template>`_ for this approach. [see `PR #182 <http://www.github.com/SeitaBV/flexmeasures/pull/182>`_]
* Set default timezone for new users using the FLEXMEASURES_TIMEZONE config setting [see `PR #190 <http://www.github.com/SeitaBV/flexmeasures/pull/190>`_]
* Monitored CLI tasks can get better names for identification [see `PR #193 <http://www.github.com/SeitaBV/flexmeasures/pull/193>`_]


v0.6.1 | September XX, 2021
Expand Down
15 changes: 14 additions & 1 deletion documentation/dev/error-monitoring.rst
Expand Up @@ -21,5 +21,18 @@ This task status monitoring is enabled by decorating the functions behind these
.. code-block:: python
@task_with_status_report
def my_function():
...
Then, FlexMeasures will log if this task ran, and if it succeeded or failed. The result is in the table ``latest_task_runs``, and that's where the ``flexmeasures monitor tasks`` will look.

.. note:: The decorator should be placed right before the function (after all other decorators).

Per default the function name is used as task name. If the number of tasks accumulate (e.g. by using multiple plugins that each define a task or two), it is useful to come up with more dedicated names. You can add a custom name as argument to the decorator:

.. code-block:: python
@task_with_status_report("pluginA_myFunction")
def my_function():
...
Then, FlexMeasures will log if this task ran, and if it succeeded or failed.
24 changes: 16 additions & 8 deletions flexmeasures/data/transactional.py
Expand Up @@ -4,6 +4,7 @@
"""
import sys
from datetime import datetime
from typing import Optional

import pytz
import click
Expand All @@ -12,6 +13,7 @@

from flexmeasures.data.config import db
from flexmeasures.utils.error_utils import get_err_source_info
from flexmeasures.utils.coding_utils import optional_arg_decorator
from flexmeasures.data.models.task_runs import LatestTaskRun


Expand Down Expand Up @@ -75,7 +77,8 @@ class PartialTaskCompletionException(Exception):
pass


def task_with_status_report(task_function):
@optional_arg_decorator
def task_with_status_report(task_function, task_name: Optional[str] = None):
"""Decorator for tasks which should report their runtime and status in the db (as LatestTaskRun entries).
Tasks decorated with this endpoint should also leave committing or rolling back the session to this
decorator (for the reasons that it is nice to centralise that but also practically, this decorator
Expand All @@ -84,18 +87,24 @@ def task_with_status_report(task_function):
it can raise a PartialTaskCompletionException and we recommend to use save-points (db.session.being_nested) to
do partial rollbacks (see https://docs.sqlalchemy.org/en/latest/orm/session_transaction.html#using-savepoint)."""

task_name_to_report = (
task_name # store this closure var somewhere else before we might assign to it
)
if task_name_to_report is None:
task_name_to_report = task_function.__name__

def wrap(*args, **kwargs):
status: bool = True
partial: bool = False
try:
task_function(*args, **kwargs)
click.echo("[FLEXMEASURES] Task %s ran fine." % task_function.__name__)
click.echo("[FLEXMEASURES] Task %s ran fine." % task_name_to_report)
except Exception as e:
exc_info = sys.exc_info()
last_traceback = exc_info[2]
click.echo(
'[FLEXMEASURES] Task %s encountered a problem: "%s". More details: %s'
% (task_function.__name__, str(e), get_err_source_info(last_traceback))
% (task_name_to_report, str(e), get_err_source_info(last_traceback))
)
status = False
if e.__class__ == PartialTaskCompletionException:
Expand All @@ -108,24 +117,23 @@ def wrap(*args, **kwargs):
# now save the status of the task
db.session.begin_nested() # any failure here does not invalidate any task results we might commit
try:
task_name = task_function.__name__
task_run = LatestTaskRun.query.filter(
LatestTaskRun.name == task_name
LatestTaskRun.name == task_name_to_report
).one_or_none()
if task_run is None:
task_run = LatestTaskRun(name=task_name)
task_run = LatestTaskRun(name=task_name_to_report)
db.session.add(task_run)
task_run.datetime = datetime.utcnow().replace(tzinfo=pytz.utc)
task_run.status = status
click.echo(
"[FLEXMEASURES] Reported task %s status as %s"
% (task_function.__name__, status)
% (task_name_to_report, status)
)
db.session.commit()
except Exception as e:
click.echo(
"[FLEXMEASURES] Could not report the running of task %s. Encountered the following problem: [%s]."
" The task might have run fine." % (task_function.__name__, str(e))
" The task might have run fine." % (task_name_to_report, str(e))
)
db.session.rollback()

Expand Down
37 changes: 37 additions & 0 deletions flexmeasures/utils/coding_utils.py
Expand Up @@ -74,6 +74,43 @@ def _getattr(obj, attr):
return functools.reduce(_getattr, [obj] + attr.split("."))


def optional_arg_decorator(fn):
"""
A decorator which _optionally_ accepts arguments.
So a decorator like this:
@optional_arg_decorator
def register_something(fn, optional_arg = 'Default Value'):
...
return fn
will work in both of these usage scenarios:
@register_something('Custom Name')
def custom_name():
pass
@register_something
def default_name():
pass
Thanks to https://stackoverflow.com/questions/3888158/making-decorators-with-optional-arguments#comment65959042_24617244
"""

def wrapped_decorator(*args):
if len(args) == 1 and callable(args[0]):
return fn(args[0])
else:

def real_decorator(decoratee):
return fn(decoratee, *args)

return real_decorator

return wrapped_decorator


def sort_dict(unsorted_dict: dict) -> dict:
sorted_dict = dict(sorted(unsorted_dict.items(), key=lambda item: item[0]))
return sorted_dict

0 comments on commit cb3de7c

Please sign in to comment.