pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.49k stars 524 forks source link

Add on_exception callbacks #485

Closed thedrow closed 3 years ago

thedrow commented 3 years ago

It'd be useful to be able to handle exceptions raised inside callbacks using on_exception callbacks.

In my case, I want to transition to the crashed state whenever an unhandled exception is raised during transitions.

aleneum commented 3 years ago

Hi @thedrow,

the current way to do this is using finalize_event callbacks to check for errors. Alternatively, you could extend your own Event for some convenience:

from transitions import Machine, Event

class ErrorEvent(Event):
    def _process(self, event_data):
        try:
            return super()._process(event_data)
        except Exception:
            event_data.args = [event_data.error]
            event_data.machine.callbacks(["on_exception"], event_data)

class ErrorMachine(Machine):

    event_cls = ErrorEvent

    def on_exception(self, error):
        print("Help:", error)

    def raise_exception(self):
        raise Exception("Oh noes!")

m = ErrorMachine(after_state_change='raise_exception')
m.to_initial()

Having on_exception in core would be useful but I fear that this could become quite complex to handle (should ALL exceptions be caught? should exceptions be raised? only when there is no on_exception attached to a model?). Let's see if there is some feedback considering this.

OneHoopyFrood commented 3 years ago

I personally think there's room in the core for this sort of functionality.

should ALL exceptions be caught?

Need to think this through, but my initial thought is yes. I'd leave the handling up to the user.

should exceptions be raised? only when there is no on_exception attached to a model?

Yes, that seems reasonable. Raise normally unless the callback is registered.

jekel commented 3 years ago

I need same functionality now(but for AsyncMachine), to not wrap code inside each transition into try except block it will be very useful to have on_exception callback where I can catch all errors inside async callbacks

aleneum commented 3 years ago

I opted for a simpler less invasive version for on_exception (than ignore_invalid_callbacks) and pushed it to master

aleneum commented 3 years ago

closing this since there has been no report of issues or improvement suggestions. If you experience issues with on_exception, please open a bug report.

e0lithic commented 1 year ago

Can you please showcase an example of using the on_exception callback to change the state of the model to a desired fail state? On doing a transition like self.to_failure() in the exception callback , it essentially results in a infinite loop.

aleneum commented 1 year ago

On doing a transition like self.to_failure() in the exception callback , it essentially results in a infinite loop.

Hello @e0lithic, please open a separate issue or a discussion instead of commenting on a two years old closed issue. There is no specific magic involved in handling exceptions:

from transitions import Machine

class Model:

    def handle_error(self):
        self.to_failed()

    def on_enter_failing(self):
        raise RuntimeError("Failing!")

model = Model()
machine = Machine(model=model, states=['initial', 'failing', 'failed'], initial='initial',
                  on_exception='handle_error')

assert model.is_initial()
model.to_failing()
assert model.is_failed()

You should make sure that a failure recovering procedure does not raise exceptions itself. If you end up in an infinite loop, it is very likely that your to_failure trigger raises an exception though.