boost-ext / sml

C++14 State Machine library
https://boost-ext.github.io/sml
Boost Software License 1.0
1.16k stars 178 forks source link

Submachine terminate event should be actioned in outer state #121

Closed madmongo1 closed 5 years ago

madmongo1 commented 7 years ago

In the UML standard, when an inner state terminates, the event that caused it is processed in the containing state.

This allows the containing state to transition correctly to different states depending on the exit event of the inner state.

This allows inner states to be used as "subroutines" that produce an outcome.

At the moment, in SML this does not happen. The makes it impractical to write a state machine that uses inner states that control their own lifetime.

This requires that exit events are propagated outward in the state machine and will require that the execution context for an inner state transition also includes all outer contexts as far as the state machine itself.

This has the added benefit that external systems (such as I/O) can process events on an inner state (for which they might have been defined) and have the result correctly affect the outer states if this causes an exit of the I/O state.

madmongo1 commented 7 years ago

happy to help with the coding. I think this is a great project. MSM is outdated and difficult to integrate with asio etc.

DenizThatMenace commented 7 years ago

Not that I doubt what you are saying but can you point me to the text in the UML 2.5 specification where this is defined?

I assume it must be somewhere around paragraph 14.2.3.4.6 and/or 14.2.3.7 but I cannot find this stated (explicitly).

As a side-node, I think a terminate state (X) in Boost.SML actually relates to an exitPoint in the UML 2.5 specification (and not to a terminate state).

madmongo1 commented 7 years ago

Thanks for replying. I want to say, I really like the way you have structured the SML - stateless states and injectable dependencies are certainly the way to go IMHO.

On 28 July 2017 at 12:59, Deniz Bahadir notifications@github.com wrote:

Not that I doubt what you are saying but can you point me to the text in the UML 2.5 specification http://www.omg.org/spec/UML/2.5/ where this is defined?

Might be quicker and easier to refer you to the note about this in the MSM docs:

Important note 3: UML states that for the exit point, the same event must be used in both transitions. MSM relaxes this rule and only wants the event on the inside transition to be convertible to the one of the outside transition. In our case, event6 is convertible from event5.

I assume it must be somewhere around paragraph 14.2.3.4.6 and/or 14.2.3.7 but I cannot find this stated (explicitly).

As a side-node, I think a terminate state (X) in Boost.SML actually relates to an exitPoint in the UML 2.5 specification (and not to a terminate state).

That makes sense. You want the X of a substate to be an exitPoint. You also want the event that cause the transition to the exitPoint to happen in the outer state, so that the outer state can transition as a result (and know the reason why it's transitioning!)

Further,

I use state machines with ASIO. Often I want a transition action to start an asynchronous process in asio and then at some later date I want the completion handler from the async operation to cause the processing of an event. The current interface for this in SML is difficult and I had to hack it (see my post on the chat in the SML website). I'll repeat the github project that demonstrates it here:

https://github.com/madmongo1/sml-goblinz

My position on this is:

A transition action should have access to the back end of the state that caused it, as well as the dependencies, so that it can schedule async events into the state at a later date, and...

... therefore reaching the exitPoint of that state must push the exit event to the outer state so that the state machine can react as a result of this event exiting the current state. (see above).

Again, I love this project. If you need more QI, I'm happy to help. I'm glad you started this project - you saved me having to figure it out for myself, since MSM is an unmaintainable nightmare for any real application.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/boost-experimental/sml/issues/121#issuecomment-318624009, or mute the thread https://github.com/notifications/unsubscribe-auth/AE7syXSnOJv25-dVnEEZvmdA-s5DsHldks5sSb8kgaJpZM4OmTH5 .

madmongo1 commented 7 years ago

for completeness, here was my hack:

/* Get our your sword and kill stinking humans! */ auto start_killin = [](auto &&event, auto &&sm, auto &&deps, auto &&subs) { auto &io = sml::aux::get<goblin_io &>(deps); io.kill_timer.expires_from_now(boost::posix_time::milliseconds(200)); io.kill_timer.async_wait(io.strand.wrap([&](auto err) { if (not err) { sm.process_event(kill_a_pleb(), deps, subs); sm.process_event(try_to_kill_again(), deps, subs); } })); };

On 28 July 2017 at 14:51, Richard Hodges hodges.r@gmail.com wrote:

Thanks for replying. I want to say, I really like the way you have structured the SML - stateless states and injectable dependencies are certainly the way to go IMHO.

On 28 July 2017 at 12:59, Deniz Bahadir notifications@github.com wrote:

Not that I doubt what you are saying but can you point me to the text in the UML 2.5 specification http://www.omg.org/spec/UML/2.5/ where this is defined?

Might be quicker and easier to refer you to the note about this in the MSM docs:

Important note 3: UML states that for the exit point, the same event must be used in both transitions. MSM relaxes this rule and only wants the event on the inside transition to be convertible to the one of the outside transition. In our case, event6 is convertible from event5.

I assume it must be somewhere around paragraph 14.2.3.4.6 and/or 14.2.3.7 but I cannot find this stated (explicitly).

As a side-node, I think a terminate state (X) in Boost.SML actually relates to an exitPoint in the UML 2.5 specification (and not to a terminate state).

That makes sense. You want the X of a substate to be an exitPoint. You also want the event that cause the transition to the exitPoint to happen in the outer state, so that the outer state can transition as a result (and know the reason why it's transitioning!)

Further,

I use state machines with ASIO. Often I want a transition action to start an asynchronous process in asio and then at some later date I want the completion handler from the async operation to cause the processing of an event. The current interface for this in SML is difficult and I had to hack it (see my post on the chat in the SML website). I'll repeat the github project that demonstrates it here:

https://github.com/madmongo1/sml-goblinz

My position on this is:

A transition action should have access to the back end of the state that caused it, as well as the dependencies, so that it can schedule async events into the state at a later date, and...

... therefore reaching the exitPoint of that state must push the exit event to the outer state so that the state machine can react as a result of this event exiting the current state. (see above).

Again, I love this project. If you need more QI, I'm happy to help. I'm glad you started this project - you saved me having to figure it out for myself, since MSM is an unmaintainable nightmare for any real application.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/boost-experimental/sml/issues/121#issuecomment-318624009, or mute the thread https://github.com/notifications/unsubscribe-auth/AE7syXSnOJv25-dVnEEZvmdA-s5DsHldks5sSb8kgaJpZM4OmTH5 .

DenizThatMenace commented 7 years ago

Thanks for replying. I want to say, I really like the way you have structured the SML - stateless states and injectable dependencies are certainly the way to go IMHO. ... Again, I love this project. If you need more QI, I'm happy to help. I'm glad you started this project - you saved me having to figure it out for myself, since MSM is an unmaintainable nightmare for any real application.

I am glad you like the project, I do, too. But I am not the one to thank. @krzysztof-jusiak is the author and the kudos belong to him. I am just a user of Boost.SML as you are, who is trying to use it in his own code.

The current interface for this in SML is difficult and I had to hack it (see my post on the chat in the SML website). I'll repeat the github project that demonstrates it here:

https://github.com/madmongo1/sml-goblinz

I am not sure if the chat is of much help. I have not seen Kris react there lately. (Then again he is also slow reacting here on Github. :-( )

for completeness, here was my hack:

/* Get our your sword and kill stinking humans! */ auto start_killin = [](auto &&event, auto &&sm, auto &&deps, auto &&subs) { auto &io = sml::aux::get<goblin_io &>(deps); io.kill_timer.expires_from_now(boost::posix_time::milliseconds(200)); io.kill_timer.async_wait(io.strand.wrap([&](auto err) { if (not err) { sm.process_event(kill_a_pleb(), deps, subs); sm.process_event(try_to_kill_again(), deps, subs); } })); };

I think, the change introduced by Kris with commit 2095f08 might simplify this for you. Kris wrote something about it in ticket #94.

DenizThatMenace commented 7 years ago

On 28 July 2017 at 12:59, Deniz Bahadir @.***> wrote: Not that I doubt what you are saying but can you point me to the text in the UML 2.5 specification http://www.omg.org/spec/UML/2.5/ where this is defined?

Might be quicker and easier to refer you to the note about this in the MSM docs:

Important note 3: UML states that for the exit point, the same event must be used in both transitions. MSM relaxes this rule and only wants the event on the inside transition to be convertible to the one of the outside transition. In our case, event6 is convertible from event5.

Now the question is, to what version of the UML specification does Christophe Henry (the author of Boost.MSM) refer here?

But by reading that section I think I understand what Christophe means and it probably refers to section 14.2.3.7 in the UML 2.5. specification. And that makes me think that Kris seems not to have properly implemented pseudo-states (and thereby no exitPoints). (See #29.) At least for substate-machines the X is more a terminate-state than an exitPoint. Although, if I recall correctly from the UML specification, a transition to a terminate-state should not have any exit/entry-actions called and should terminate (the entire?) state-machine.

jovere commented 6 years ago

This state machine system seems very compact and efficient compared to many other implementations I've seen. However, being able to have a substate control its lifetime is quite important.

@madmongo1, have you been able to add exit states? As you stated, composite states aren't very useful if there isn't an entry state/exit state possible in the composite state. I feel an implementation of the entry/exit pseudo state would be awesome. https://www.uml-diagrams.org/state-machine-diagrams.html#entry-point-pseudostate

madmongo1 commented 6 years ago

HI Jeremy,

It's been a while since I looked at SML and it fell off my radar. But thanks to your mention I'll have another look.

On Tue, 2 Oct 2018 at 14:46, Jeremy Overesch notifications@github.com wrote:

This state machine system seems very compact and efficient compared to many other implementations I've seen. However, being able to have a substate control its lifetime is quite important.

@madmongo1 https://github.com/madmongo1, have you been able to add exit states? As you stated, composite states aren't very useful if there isn't an entry state/exit state possible in the composite state. I feel an implementation of the entry/exit pseudo state would be awesome. https://www.uml-diagrams.org/state-machine-diagrams.html#entry-point-pseudostate

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/boost-experimental/sml/issues/121#issuecomment-426259838, or mute the thread https://github.com/notifications/unsubscribe-auth/AE7sybvp8YLlr4W6GtbFr62HcFUiy2FOks5ug2AogaJpZM4OmTH5 .

wangii commented 5 years ago

any news on this? or any workaround? it's the single issue draws me back to statechart/msm :(

wangii commented 5 years ago

for anyone who is interested:

GuiCodron commented 5 years ago

Hi @wangii, I saw your post and I feel your pain with being forced to work with MSM.

It's true that not being able to address outer state machine from sub state machine is a huge limitation of the framework but it can be quite easily bypassed by adding the outer state machine inside the dependencies of the state machine. See the example below.

#include <cassert>
#include <queue>
#include <memory>

#include <iostream>
#include <thread>
#include <boost/sml.hpp>

namespace sml = boost::sml;

namespace {

struct main_state;

struct Dependencies {
    std::shared_ptr<sml::sm<main_state, sml::defer_queue<std::queue>, sml::process_queue<std::queue>>> sm;
    std::string msg;

    template<typename E>
    void process_event(E e) {
        ([] (auto& sm, const auto& e) {
            sm->process_event(e);
        })(this->sm, e);
    }
};

typedef void (*action)(void);

//
struct ge1 {};
struct ge2 {};
struct ge3 {};

struct o1e1{};
struct o1e2{};
struct o2e1{};
struct o2e2{};

struct o1;
struct o2;

struct o1 {

    struct s1{};
auto operator()() const noexcept {
    using namespace sml;
    return make_transition_table(
        (*state<s1>) + event<o1e2> / [] (const auto& e, Dependencies& deps) { deps.msg+="o1e2s2|";} = "s2"_s
        , state<s1>  + on_entry<_>  / [this](Dependencies& deps){ deps.msg+="o1i|"; }
        , state<s1>  + event<ge1> / [] (const auto& e, Dependencies& deps) { deps.msg+="o1s1ge1|";}
        , state<s1>  + event<ge2> / [] (const auto& e, Dependencies& deps) { deps.msg+="o1s1ge2|";}
        , "s2"_s  + event<o1e2> / [] (const auto& e, Dependencies& deps) { deps.msg+="o1s2e2|";} = X
    );
}
};

struct o2 {

auto operator()() const noexcept {
    using namespace sml;
    return make_transition_table(
        (*"s1"_s) + event<o2e2>  / [] (const auto& e, Dependencies& deps) { deps.msg+="o2e2s2|";} = "s2"_s
        , "s1"_s  + on_entry<_>  / [this](Dependencies& deps){ deps.msg+="o2i|"; }
        , "s1"_s  + event<ge1> / [] (const auto& e, Dependencies& deps) { deps.msg+="o2s1ge1|";}
        , "s1"_s  + event<ge2> / [] (const auto& e, Dependencies& deps) { deps.msg+="o2s1ge2|";}
        , "s2"_s  + event<o2e2> / [] (const auto& e, Dependencies& deps) { 
               deps.msg+="o2s2e2|"; 
               deps.process_event(o1e2{});
        } = X
    );
}
};

struct main_state {

    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            (*state<o1>) + on_entry<sml::initial> / [this](Dependencies& deps){ deps.msg+="mo1i|"; }
            , state<o1> + event<ge3> / [this](Dependencies& deps){ deps.msg+="mo1_ge3|"; }
            ,(*state<o2>) + on_entry<sml::initial> / [this](Dependencies& deps){ deps.msg+="mo2i|"; }
        );
    }
};

}

int main(int argv, char** argc) {

    Dependencies deps;
    auto sm = std::make_shared<sml::sm<main_state, sml::defer_queue<std::queue>, sml::process_queue<std::queue>>>(deps);
    deps.sm = sm;
    using namespace sml;
    sm->process_event(ge1{});
    sm->process_event(ge2{});
    sm->process_event(ge3{});
    sm->process_event(o2e2{});
    sm->process_event(o1e2{});
    sm->process_event(o2e2{});

    std::cout << deps.msg << "\n";

}

This example is a bit big but you can see all the interesting part inside struct Dependencies and the transition o2::s2 on event o2e2. The small trick inside processEvent is needed to force lazy type evaluation of sm (since it is only fully specified once main_state is described). This example show that this method allow orthogonal state machines to interact with each other via event process on whole sm.

For your specific need of event propagation on state machine termination, you can just trigger an event on entry of X state that can be caught by upper level state machine.

It may be useful to start SM in an idle state and trigger a "start" that go to working state once the sm pointer is set inside dependencies to avoid segfault if anonymous transitions lead to an early call to processEvent.

Hopefully the small workaround I use to propagate event from sub state to outer state can help you.

wangii commented 5 years ago

hi @GuiCodron

thank you very much for the hint! It's almost what I was experimenting except I used std::function to capture the outermost SM. I was digging into the codes to check if any unintended side effects and your hint is a great relief!

the down side of the approach, after preliminary test, is the binary size, about 20% more without O, or 25% more with O3, using clang.

I also tried throwing exceptions, didn't work.

GuiCodron commented 5 years ago

Is it because of the dependency? Maybe with plain pointer the cost is smaller. Some test with a real state machine (with real objects and function execution) might have more signification.

It's really interesting, if you have any more result on this I would be interested.

On this subject it could be interesting to add an helper class or at least some documentation on this workaround since it is a real requirement of any real world state machine.

erikzenkerLogmein commented 5 years ago

@GuiCodron I found a little mistake in your example code. The defer policy should be: defer_queue<std::deque>

robwiss commented 5 years ago

@GuiCodron Are you certain that approach is safe? I had thought of doing something similar but hesitated because by calling process_event on the outer state machine while in the event handler for an inner state machine the program doesn't get to unwind the stack from the initial process_event call. I haven't examined sml to see if it's doing anything important during that stack unwinding that needs to happen before the next call to process_event but if it does then that's a problem.

A safer approach that lets us stay ignorant of sml would be to wrap sml with another type that couples the sm object with a process queue, then empties the process queue before proceeding and stick THAT in the dependency object. Maybe I'll take a crack at it.

madmongo1 commented 5 years ago

On Tue, 9 Apr 2019 at 09:25, robwiss notifications@github.com wrote:

@GuiCodron https://github.com/GuiCodron Are you certain that approach is safe? I had thought of doing something similar but hesitated because by calling process_event on the outer state machine while in the event handler for an inner state machine the program doesn't get to unwind the stack from the initial process_event call. I haven't examined sml to see if it's doing anything important during that stack unwinding that needs to happen before the next call to process_event but if it does then that's a problem.

A safer approach that lets us stay ignorant of sml would be to wrap sml with another type that couples the sm object with a process queue, then empties the process queue before proceeding and stick THAT in the dependency object. Maybe I'll take a crack at it.

A good model here (IMHO) is boost msm. If inner state exit points can be observed as transitions in an outer state, there is no need to call process_event in an event handler. The exit event should only be propagated up once the inner state's transition is complete.

In general, if a state machine needs to call process_event in an event handler, it's either poorly written or written on an incomplete framework.

https://www.boost.org/doc/libs/1_69_0/libs/msm/doc/HTML/ch02s02.html#d0e151

See diagram after text: "An exit point pseudo state exits a composite state or a submachine and forces termination of execution in all contained regions."

Notice how the outer state transitions automatically as the inner state exits. In strict UML, the event that caused the inner termination is propagated to the outer state. In MSM, the designer is allowed (as an extension) to modify the event type as it is propagated (via overloaded constructor). This extra feature is un-necessary.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/boost-experimental/sml/issues/121#issuecomment-481076554, or mute the thread https://github.com/notifications/unsubscribe-auth/AE7syT9hM0M2tRw42IThLLtWWQ6Hp0lnks5ve_n_gaJpZM4OmTH5 .

-- Richard Hodges hodges.r@gmail.com office: +442032898513 home: +376841522 mobile: +376380212

robwiss commented 5 years ago

I poked around some more and it looks like a test was added for sending events directly to the outer sm when passed in as a dependency as part of #76, so this behavior is explicitly supported and even has a test.

aliasdevelopment commented 5 years ago

Hi robwiss

Have you managed to forward sub state events to the parent state?

The reason I ask is that the test/ft/actions_process.cpp has been changed. The process_event_from_substate test does not support forward of sub state events to the parent state. The test only performs sub state to sub state process event.

kris-jusiak commented 5 years ago

Fixed by https://github.com/boost-experimental/sml/pull/302