pytransitions / transitions

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

may_ function in parallel state does not report correct value #647

Closed spearsear closed 3 months ago

spearsear commented 7 months ago

Thank you for taking the time to report a bug! Your support is essential for the maintenance of this project. Please fill out the following fields to ease bug hunting and resolving this issue as soon as possible:

Describe the bug In parallel state of a hierarchical machine, the may_ function for a trigger returns False, yet it should return True, since the trigger indeed return True

I have a simple statemachine which runs a task for each month in quarter 1 parallelly, then run each month in quarter 2 parallelly. When it reach the parallell state of run_qtr1, may function of any trigger in quater1 should return True, yet it returns False. And I indeed can run the trigger function

from transitions.extensions import HierarchicalMachine
        from transitions.extensions.nesting import NestedState
        class Model:
            def __init__(self):
                pass

            def qtr1_completed(self, event):
                # check embedding_[1-3]_succeeded state has been reached
                return True

            def qtr2_completed(self, event):
                # check embedding_[1-3]_succeeded state has been reached
                return True

        model = Model()
        states = [
            'workflow_started',
            {
                'name': 'running_qtr_1',
                'parallel': [
                    {
                        'name': 'running_jan',
                        'children': [
                            'jan_started', 'jan_succeeded', 'jan_failed'
                        ],
                        'initial': 'jan_started',
                        'transitions': [
                            ['end_success_jan', 'jan_started', 'jan_succeeded'],
                            ['end_failure_jan', 'jan_started', 'jan_failed']
                        ]
                    },
                    {
                        'name': 'running_feb',
                        'children': [
                            'feb_started', 'feb_succeeded', 'feb_failed'
                        ],
                        'initial': 'feb_started',
                        'transitions': [
                            ['end_success_feb', 'feb_started', 'feb_succeeded'],
                            ['end_failure_feb', 'feb_started', 'feb_failed']
                        ]
                    },
                    {
                        'name': 'running_mar',
                        'children': [
                            'mar_started', 'mar_succeeded', 'mar_failed'
                        ],
                        'initial': 'mar_started',
                        'transitions': [
                            ['end_success_mar', 'mar_started', 'mar_succeeded'],
                            ['end_failure_mar', 'mar_started', 'mar_failed']
                        ]
                    },
                ]
            },
            'completed_qtr_1',
            {
                'name': 'running_qtr_2',
                'parallel': [
                    {
                        'name': 'running_apr',
                        'children': [
                            'apr_started', 'apr_succeeded', 'apr_failed'
                        ],
                        'initial': 'apr_started',
                        'transitions': [
                            ['end_success_apr', 'apr_started', 'apr_succeeded'],
                            ['end_failure_apr', 'apr_started', 'apr_failed']
                        ]
                    },
                    {
                        'name': 'running_may',
                        'children': [
                            'may_started', 'may_succeeded', 'may_failed'
                        ],
                        'initial': 'may_started',
                        'transitions': [
                            ['end_success_may', 'may_started', 'may_succeeded'],
                            ['end_failure_may', 'may_started', 'may_failed']
                        ]
                    },
                    {
                        'name': 'running_jun',
                        'children': [
                            'jun_started', 'jun_succeeded', 'jun_failed'
                        ],
                        'initial': 'jun_started',
                        'transitions': [
                            ['end_success_jun', 'jun_started', 'jun_succeeded'],
                            ['end_failure_jun', 'jun_started', 'jun_failed']
                        ]
                    },
                ],
            },
            'completed_qtr_2',
            'workflow_succeeded', 'workflow_failed'
        ]
        transitions = [
                {'trigger': 'next', 'source': 'workflow_started', 'dest': 'running_qtr_1'},
                {'trigger': 'next', 'source': 'running_qtr_1~running_jan~jan_succeeded', 'dest': 'completed_qtr_1',
                 'conditions': 'qtr1_completed'},
                {'trigger': 'next', 'source': 'running_qtr_1~running_feb~feb_succeeded', 'dest': 'completed_qtr_1',
                 'conditions': 'qtr1_completed'},
                {'trigger': 'next', 'source': 'running_qtr_1~running_mar~mar_succeeded', 'dest': 'completed_qtr_1',
                 'conditions': 'qtr1_completed'},
                {'trigger': 'next', 'source': 'completed_qtr_1', 'dest': 'running_qtr_2'},
                {'trigger': 'next', 'source': 'running_qtr_2~running_apr~apr_succeeded', 'dest': 'completed_qtr_2',
                 'conditions': 'qtr2_completed'},
                {'trigger': 'next', 'source': 'running_qtr_2~running_may~may_succeeded', 'dest': 'completed_qtr_2',
                 'conditions': 'qtr2_completed'},
                {'trigger': 'next', 'source': 'running_qtr_2~running_jun~jun_succeeded', 'dest': 'completed_qtr_2',
                 'conditions': 'qtr2_completed'},
                {'trigger': 'next', 'source': 'completed_qtr_2', 'dest': 'workflow_succeeded'},
                {'trigger': 'next',
                 'source': [
                     'running_qtr_1~running_jan~jan_failed',
                     'running_qtr_1~running_feb~feb_failed',
                     'running_qtr_1~running_mar~mar_failed',
                     'running_qtr_2~running_apr~apr_failed',
                     'running_qtr_2~running_may~may_failed',
                     'running_qtr_2~running_jun~jun_failed'
                 ],
                 'dest': 'workflow_failed'
                 },
            ]
        initial_state = 'workflow_started'
        machine = HierarchicalMachine(
            model=model,
            states=states,
            transitions=transitions,
            initial=initial_state
        )
        print(f"current_state is: {machine.model.state}")
        machine.model.next()
        print(f"current_state is: {machine.model.state}")
        # BUG HERE??? may_ function returns False
        print(f"may_end_success_feb: {machine.model.may_end_success_feb()} <<--- BUG HERE???, Should be True")
        # but indeed I can run the trigger
        machine.model.end_success_feb()
        # you see the parallel state has transitioned to "running_qtr_1~running_feb~feb_succeeded" as expected
        print(f"current_state is: {machine.model.state}")

Expected behavior may_ function for the triggers in parallel states should return True

Additional context Output of above code:

current_state is: workflow_started
current_state is: ['running_qtr_1~running_jan~jan_started', 'running_qtr_1~running_feb~feb_started', 'running_qtr_1~running_mar~mar_started']
may_end_success_feb: False  <<--- BUG HERE??? Should be True
current_state is ['running_qtr_1~running_jan~jan_started', 'running_qtr_1~running_feb~feb_succeeded', 'running_qtr_1~running_mar~mar_started']
aleneum commented 4 months ago

Hi @spearsear,

I could reproduce the error. I agree that may_ and the actual transition should come to the same conclusion. This is an MRE based on your provided example:

from transitions.extensions import HierarchicalMachine
from transitions.extensions.nesting import NestedState

NestedState.separator = "~"

states = [
    'workflow_started',
    {
        'name': 'running_qtr_1',
        'parallel': [
            {
                'name': 'running_jan',
                'children': [
                    'jan_started', 'jan_succeeded'
                ],
                'initial': 'jan_started'
            },
            {
                'name': 'running_feb',
                'children': [
                    'feb_started', 'feb_succeeded'
                ],
                'initial': 'feb_started',
                'transitions': [
                    ['end_success_feb', 'feb_started', 'feb_succeeded'],
                ]
            },
        ]
    },
]
transitions = [
        {'trigger': 'next', 'source': 'workflow_started', 'dest': 'running_qtr_1'}
]
initial_state = 'workflow_started'
machine = HierarchicalMachine(
    states=states,
    transitions=transitions,
    initial=initial_state
)
machine.next()
print(f"current_state is: {machine.state}")
may_res = machine.may_end_success_feb()
res = machine.end_success_feb()
assert may_res == res
print(f"current_state is: {machine.state}")