From 9759c2b51a1250aa345c21b7cc6b793f4965ac2d Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 28 May 2018 19:51:58 +0300 Subject: Add: BuildStateMachine class --- ick2/buildsm.py | 223 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 ick2/buildsm.py (limited to 'ick2/buildsm.py') diff --git a/ick2/buildsm.py b/ick2/buildsm.py new file mode 100644 index 0000000..2e6d079 --- /dev/null +++ b/ick2/buildsm.py @@ -0,0 +1,223 @@ +# Copyright (C) 2018 Lars Wirzenius +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +import ick2 + + +BUILD_TRIGGERED = 'triggered' +BUILD_BUILDING = 'building' +BUILD_NOTIFYING = 'notifying' +BUILD_DONE = 'done' +BUILD_FAILED = 'failed' + + +class StateMachine: + + def __init__(self, get_state, set_state): + self.transitions = {} + self.get_state = get_state + self.set_state = set_state + + def add_transition(self, state, event_class, handler): + if state not in self.transitions: + self.transitions[state] = {} + assert event_class not in self.transitions[state] + self.transitions[state][event_class] = handler + + def handle_event(self, event): + state = self.get_state() + assert state in self.transitions + assert event.__class__ in self.transitions[state] + func = self.transitions[state][event.__class__] + new_state, resp = func(event) + self.set_state(new_state) + return resp + + +class BuildStateMachine: + + def __init__(self, build): + self.build = build + self.sm = self.init_sm() + + def init_sm(self): + transitions = { + BUILD_TRIGGERED: { + BuildStartsEvent: self.set_state_to_building, + }, + BUILD_BUILDING: { + NeedWorkEvent: self.pick_action, + PartialActionOutputEvent: self.set_state_to_building, + ActionFinishedEvent: self.mark_action_done, + ActionFailedEvent: self.mark_build_failed, + }, + BUILD_NOTIFYING: { + NeedWorkEvent: self.pick_notification_action, + PartialActionOutputEvent: self.set_state_to_notifying, + ActionFinishedEvent: self.mark_notification_done, + ActionFailedEvent: self.mark_build_failed, + }, + } + + sm = StateMachine(self.get_state, self.set_state) + for state in transitions: + for event_class in transitions[state]: + func = transitions[state][event_class] + sm.add_transition(state, event_class, func) + return sm + + def get_state(self): + return self.build.resource['status'] + + def set_state(self, state): + self.build.resource['status'] = state + + def handle_event(self, event): + old_state = self.get_state() + resp = self.sm.handle_event(event) + new_state = self.get_state() + return resp + + def set_state_to_building(self, event): + return BUILD_BUILDING, None + + def set_state_to_notifying(self, event): # pragma: no cover + return BUILD_NOTIFYING, None + + def pick_action(self, event): + graph = self.build.get_graph() + action_ids = graph.find_actions(ick2.ACTION_READY) + if not action_ids: # pragma: no cover + self.build.resource['exit_code'] = 0 + return BUILD_DONE, None + + action_id = action_ids[0] + graph.set_action_status(action_id, ick2.ACTION_BUILDING) + action = graph.get_action(action_id) + return BUILD_BUILDING, (action_id, action) + + def pick_notification_action(self, event): + graph = self.build.get_graph() + action_ids = graph.find_actions(ick2.ACTION_READY) + if not action_ids: # pragma: no cover + self.build.resource['exit_code'] = 0 + return BUILD_DONE, None + + action_id = action_ids[0] + graph.set_action_status(action_id, ick2.ACTION_BUILDING) + action = graph.get_action(action_id) + return BUILD_NOTIFYING, (action_id, action) + + def mark_action_done(self, event): + graph = self.build.get_graph() + graph.set_action_status(event.action_id, ick2.ACTION_DONE) + graph.unblock() + if graph.has_more_to_do(): + return BUILD_BUILDING, None + + self.add_notification_action() + return BUILD_NOTIFYING, None + + def add_notification_action(self): + action = { + 'action': 'notify', + } + graph = self.build.get_graph() + graph.append_action(action, ick2.ACTION_READY, depends=[]) + + def mark_notification_done(self, event): + graph = self.build.get_graph() + graph.set_action_status(event.action_id, ick2.ACTION_DONE) + graph.unblock() + if graph.has_more_to_do(): # pragma: no cover + return BUILD_NOTIFYING, None + + if self.build.resource.get('exit_code') is None: + self.build.resource['exit_code'] = 0 + return BUILD_DONE, None + + return BUILD_FAILED, None + + def mark_build_failed(self, event): + graph = self.build.get_graph() + graph.set_action_status(event.action_id, ick2.BUILD_FAILED) + self.build.resource['exit_code'] = event.exit_code + self.add_notification_action() + return BUILD_NOTIFYING, None + + +# Thing should be something we can create a BuildEvent from. +def create_build_event(thing): + if thing == BuildStartsEvent: + return BuildStartsEvent() + + if thing == NeedWorkEvent: + return NeedWorkEvent() + + if isinstance(thing, dict): + exit_code = thing.get('exit_code') + action_id = thing.get('action_id') + if exit_code is None: + return PartialActionOutputEvent() + if exit_code == 0: + return ActionFinishedEvent(action_id) + return ActionFailedEvent(action_id, exit_code) + + +class BuildEvent: # pragma: no cover + + event_type = 'BuildEvent' + + def __str__(self): + return self.event_type + + +class BuildStartsEvent(BuildEvent): + + event_type = 'build-starts' + + +class NeedWorkEvent(BuildEvent): + + event_type = 'need-work' + + +class PartialActionOutputEvent(BuildEvent): + + event_type = 'partial-output' + + +class ActionFinishedEvent(BuildEvent): + + event_type = 'action-finished' + + def __init__(self, action_id): + self.action_id = action_id + + +class ActionFailedEvent(BuildEvent): + + event_type = 'action-failed' + + def __init__(self, action_id, exit_code): + self.action_id = action_id + self.exit_code = exit_code + + +class UnexpectedEvent(Exception): # pragma: no cover + + def __init__(self, event, state): + super().__init__('Did not expect %s in %s' % (event, state)) -- cgit v1.2.1