def _transition() -> bool: with app.session.begin_nested(): lock_label(app, label) current_state = get_current_state(app, label, state_machine) if isinstance(current_state, Action): return process_action( app=app, state=current_state, state_machine=state_machine, label=label, ) elif isinstance(current_state, Gate): # pragma: no branch if not current_state.trigger_on_entry: return False return process_gate( app=app, state=current_state, state_machine=state_machine, label=label, ) else: raise RuntimeError( # pragma: no cover "Unsupported state type {0}".format(current_state), )
def process_cron( process: LabelStateProcessor, get_labels: Callable[[StateMachine, State], Iterable[str]], app: App, state_machine: StateMachine, state: State, ): """ Cron event entrypoint. """ with app.new_session(): relevant_labels = get_labels(state_machine, state) for label_name in relevant_labels: with suppress_exceptions(app.logger): label = LabelRef(name=label_name, state_machine=state_machine.name) could_progress = False with app.new_session(): lock_label(app, label) current_state = get_current_state(app, label, state_machine) if current_state != state: continue could_progress = process( app=app, state=state, state_machine=state_machine, label=label, ) with app.new_session(): if could_progress: process_transitions(app, label)
def delete_label(app: App, label: LabelRef) -> None: """ Deletes the metadata for a label and marks the label as deleted. The history for the label is not changed (in order to allow post-hoc analysis of the path the label took through the state machine). """ state_machine = get_state_machine(app, label) # Raises UnknownStateMachine try: row = lock_label(app, label) except UnknownLabel: return if row is None or row.deleted: return # Record the label as having been deleted and remove its metadata row.metadata = {} row.deleted = True # Add a history entry for the deletion current_state = get_current_state(app, label, state_machine) if current_state is None: raise AssertionError(f"Active label {label} has no current state!") row.history.append(History( old_state=current_state.name, new_state=None, ))
def _process_transitions_for_metadata_update( app: App, label: LabelRef, state_machine: StateMachine, state_pending_update: State, ): with app.session.begin_nested(): lock_label(app, label) current_state = get_current_state(app, label, state_machine) if state_pending_update != current_state: # We have raced with another update, and are no longer in # the state for which we needed an update, so we should # stop. return if not isinstance(current_state, Gate): # pragma: no branch # Cannot be hit because of the semantics of # `needs_gate_evaluation_for_metadata_change`. Here to # appease mypy. raise RuntimeError( # pragma: no cover "Label not in a gate", ) could_progress = process_gate( app=app, state=current_state, state_machine=state_machine, label=label, ) if could_progress: process_transitions(app, label)
def test_get_current_state_for_label_in_invalid_state(custom_app, create_label): state_to_be_removed = Gate( name='start', triggers=[], next_states=ConstantNextState('end'), exit_condition=ExitConditionProgram('false'), ) end_state = Gate( name='end', triggers=[], next_states=NoNextStates(), exit_condition=ExitConditionProgram('false'), ) app = custom_app( state_machines={ 'test_machine': StateMachine( name='test_machine', states=[state_to_be_removed, end_state], feeds=[], webhooks=[], ), }) label = create_label('foo', 'test_machine', {}) state_machine = app.config.state_machines['test_machine'] # Remove the state which we expect the label to be in from the state # machine; this is logically equivalent to loading a new config which does # not have the state del state_machine.states[0] with app.new_session(): with pytest.raises(Exception): utils.get_current_state(app, label, state_machine)
def test_does_not_need_gate_evaluation_for_metadata_change_with_action( app, create_label, mock_webhook): state_machine = app.config.state_machines['test_machine'] # Force the action to fail here so that the label is left in the action # state. with mock_webhook(WebhookResult.FAIL): label = create_label('foo', 'test_machine', {'should_progress': True}) with app.new_session(): current_state = utils.get_current_state( app, label, state_machine, ) assert current_state.name == 'perform_action' assert utils.needs_gate_evaluation_for_metadata_change( app, state_machine, label, {}, ) == (False, current_state)
def test_needs_gate_evaluation_for_metadata_change(app, create_label): label = create_label('foo', 'test_machine', {}) state_machine = app.config.state_machines['test_machine'] with app.new_session(): current_state = utils.get_current_state( app, label, state_machine, ) assert utils.needs_gate_evaluation_for_metadata_change( app, state_machine, label, {'foo': 'bar'}, ) == (False, current_state) assert utils.needs_gate_evaluation_for_metadata_change( app, state_machine, label, {'should_progress': 'bar'}, ) == (True, current_state)
def test_get_current_state_for_deleted_label(app, create_deleted_label): label = create_deleted_label('foo', 'test_machine') state_machine = app.config.state_machines['test_machine'] with app.new_session(): assert utils.get_current_state(app, label, state_machine) is None
def test_get_current_state(app, create_label): label = create_label('foo', 'test_machine', {}) state_machine = app.config.state_machines['test_machine'] state = state_machine.states[0] with app.new_session(): assert utils.get_current_state(app, label, state_machine) == state
def get_label_state(app: App, label: LabelRef) -> Optional[State]: """Finds the current state of a label.""" state_machine = get_state_machine(app, label) return get_current_state(app, label, state_machine)