Inspecting State and Error Recovery

Each time you create a trigger manager instance, you also assign a storage backend. The storage backend is responsible for maintaining session state, but it also provides a number of methods and attributes that your application can inspect.

What’s In Session State?

Inside of a session’s state are 3 objects:

  • tasks contains the configured trigger tasks.
  • instances contains instances of each task.
  • metadata contains internal metadata.

In general, you won’t need to interact with these objects directly, but they can be useful for inspecting and troubleshooting sessions.

Inspecting Session State

To inspect a session’s state, your application will interact with the trigger manager’s storage backend.

Tip

If you only want to inspect a session’s state (i.e., you don’t need to fire triggers, change task instance status, etc.), you do not need to create a trigger manager instance; you only need an instance of the storage backend.

Inspecting Task Configuration

To inspect a trigger task’s configuration, load it from tasks:

task = trigger_manager.storage.tasks['t_importSubject']

In the above example, task is an instance of triggers.types.TaskConfig.

Inspecting Instance Configuration

To inspect a trigger instance configuration, load it from instances:

instance = trigger_manager.storage.instances['t_importSubject#0']

In the above example, instance is an instance of triggers.types.TaskInstance.

Note

To get the instance, you must provide the name of the instance, not the name of the task:

# Using instance name:
>>> trigger_manager.storage.instances['t_importSubject#0']
TaskInstance(...)

# Using task name:
>>> trigger_manager.storage.instances['t_importSubject']
KeyError: 't_importSubject'

Finding Instances By Trigger Task

If you want to find all the instances for a particular task, use the instances_of_task method:

instances =\
  trigger_manager.storage.instances_of_task['t_importSubject']

In the above example, instances is a list of TaskInstance objects.

Finding Unresolved Tasks and Instances

When inspecting the state of a session, one of the most critical pieces of information that applications need is the list of tasks that haven’t been finished yet.

The storage backend provides two methods to facilitate this:

get_unresolved_tasks()
Returns a list of all tasks that haven’t run yet, or have one or more unresolved instances.
get_unresolved_instances()
Returns a list of all unresolved instances.

The difference between these methods is subtle but important.

It is best explained using an example:

>>> from uuid import uuid4
>>> from triggers import TriggerManager
>>> from triggers.storages.cache import CacheStorageBackend

>>> trigger_manager =\
...   TriggerManager(CacheStorageBackend(uuid4().hex))
...

>>> trigger_manager.update_configuration({
...   't_importSubject': {
...     'after': ['firstPageReceived', 'questionnaireComplete'],
...     'run':   '...',
...   },
... })
...

# ``t_importSubject`` hasn't run yet, so it is unresolved.
>>> trigger_manager.storage.get_unresolved_tasks()
[<TaskConfig 't_importSubject'>]

# None of the triggers in ``t_importSubject.after`` have fired
# yet, so no task instance has been created yet.
>>> trigger_manager.storage.get_unresolved_instances()
[]

>>> trigger_manager.fire('firstPageReceived')

# After the trigger fires, the trigger manager creates an
# instance for ``t_importSubject``, but it can't run yet, because
# it's still waiting for the other trigger.
>>> [<TaskInstance 't_importSubject#0'>]

Getting the Full Picture

If you want to get a snapshot of the state of every task and instance, invoke the debug_repr method:

from pprint import pprint
pprint(trigger_manager.storage.debug_repr())

Tip

As the name implies, this is intended to be used only for debugging purposes.

If you find yourself wanting to use it as part of normal operations, this likely indicates a deficiency in the Trigger Manager’s feature set; please post a feature request on the Triggers Framework Bug Tracker so that we can take a look!

Error Recovery

On occasion, a trigger task instance may fail (e.g., due to an uncaught exception).

When this happens, you can recover by replaying or skipping the failed instance(s).

Tip

If the instance fails due to an uncaught exception, the exception and traceback will be stored in the failed instance’s metadata so that you can inspect them.

To access these values, find the TaskInstance and inspect its metadata value:

failed_instance =\
  trigger_manager.storage.instances['t_importSubject#0']

pprint(failed_instance.metadata)

Replaying Failed Task Instances

To replay a failed task invoke the trigger manager’s replay_failed_instance() method, e.g.:

trigger_manager.replay_failed_instance('t_importSubject#0')

Note that you must provide the name of the instance that failed, not the task.

The trigger manager will clone the failed instance and schedule it for execution immediately.

The failed instance’s status will be changed to “replayed” (see Task Instance Status), but otherwise it remains unchanged. This allows you to trace the history of a failed task, retain the original exception details, etc.

If necessary/desired, you may replay the instance with different trigger kwargs:

trigger_manager.replay_failed_instance(
  failed_instance = 't_importSubject#0',

  replacement_kwargs = {
    'firstPageReceived':      {'responses': {...}},
    'questionnaireComplete':  {},
  },
)

Important

The replacement kwargs will be used instead of the trigger kwargs provided to the failed instance. If you only want to change some of the trigger kwargs for the replayed instance, you will need to merge them manually.

Example:

failed_instance =\
  trigger_manager.storage.instances['t_importSubject#0']

# Change the ``firstPageReceived`` trigger kwargs
# for the replay, but keep the rest the same.
replacement_kwargs = failed_instance.kwargs
replacement_kwargs['firstPageReceived'] = {'responses': {...}}

trigger_manager.replay_failed_instance(
  failed_instance,
  replacement_kwargs,
)

Skipping Failed Task Instances

Sometimes there is just no way to recover a failed task instance, but you still want to mark it as resolved, or to simulate a successful result so that other tasks can still run (i.e., simulate a cascade).

To accomplish this, invoke the skip_failed_instance() method:

trigger_manager.skip_failed_instance('t_importSubject#0')

Note that you must provide the name of the instance that failed, not the task.

The trigger manager will change the status of the instance from “failed” to “skipped” (see Task Instance Status).

By default, marking a failed instance as skipped will not cause a cascade, so any tasks that depend on the failed one won’t be able to run.

In many cases, this is actually the desired behavior, but if you would like to force a cascade anyway, you can simulate a successful result:

trigger_manager.skip_failed_instance(
  failed_instance = 't_importSubject#0',

  # Trigger a cascade.
  cascade = True,

  # Simulate the result from ``t_importSubject#0``.
  result = {'subjectId': 42},
)

The above code has basically the same effect as if the t_importSubject#0 instance finished successfully and caused a cascade:

trigger_manager.fire(
  trigger_name    = 't_importSubject',
  trigger_kwargs  = {'subjectId': 42},
)