Giter Site home page Giter Site logo

piotrmurach / finite_machine Goto Github PK

View Code? Open in Web Editor NEW
804.0 18.0 39.0 4.2 MB

A minimal finite state machine with a straightforward syntax.

Home Page: https://piotrmurach.github.io/finite_machine/

License: MIT License

Ruby 100.00%
ruby-gem state-machine state-transitions conditional-transitions finite-machine

finite_machine's Introduction

finite machine logo

FiniteMachine

Gem Version Actions CI Build status Code Climate Coverage Status Inline docs Gitter

A minimal finite state machine with a straightforward and intuitive syntax. You can quickly model states and transitions and register callbacks to watch for triggered transitions.

Features

Installation

Add this line to your application's Gemfile:

gem "finite_machine"

Then execute:

$ bundle

Or install it yourself as:

$ gem install finite_machine

Contents

1. Usage

Here is a very simple example of a state machine:

fm = FiniteMachine.new do
  initial :red

  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red

  on_before(:ready) { |event| ... }
  on_exit(:yellow)  { |event| ... }
  on_enter(:green)  { |event| ... }
  on_after(:stop)   { |event| ... }
end

By calling the new method on FiniteMachine, you gain access to a powerful DSL for expressing transitions and registering callbacks.

Having declared the states and transitions, you can check current state:

fm.current # => :red

And then trigger transitions using the trigger:

fm.trigger(:ready)

Or you can use direct method calls:

fm.ready

Read States and Transitions and Callbacks sections for more details.

Alternatively, you can construct the state machine like a regular object using the same DSL methods. Similar machine could be reimplemented as follows:

fm = FiniteMachine.new(initial: :red)
fm.event(:ready, :red    => :yellow)
fm.event(:go,    :yellow => :green)
fm.event(:stop,  :green  => :red)
fm.on_before(:ready) { |event| ... }
fm.on_exit(:yellow)  { |event| ... }
fm.on_enter(:green)  { |event| ... }
fm.on_after(:stop)   { |event| ... }

2. API

2.1 new

In most cases you will want to create an instance of FiniteMachine class using the new method. At the bare minimum you need specify the transition events inside a block using the event helper:

fm = FiniteMachine.new do
  initial :green

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
end

Alternatively, you can skip block definition and instead call DSL methods directly on the state machine instance:

fm = FiniteMachine.new
fm.initial(:green)
fm.event(:slow, :green  => :yellow)
fm.event(:stop, :yellow => :red)
fm.event(:ready,:red    => :yellow)
fm.event(:go,   :yellow => :green)

As a guiding rule, any method exposed via DSL is available as a regular method call on the state machine instance.

2.2 define

To create a reusable definition for a state machine use define method. By calling define you're creating an anonymous class that can act as a factory for state machines. For example, below we create a TrafficLights class that contains our state machine definition:

TrafficLights = FiniteMachine.define do
  initial :green

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
  event :ready, :red    => :yellow
  event :go,    :yellow => :green
end

Then we can create however many instance of above class:

lights_fm_a = TrafficLights.new
lights_fm_b = TrafficLights.new

Each instance will start in consistent state:

lights_fm_a.current # => :green
lights_fm_b.current # => :green

We can then trigger event for one instance and not the other:

lights_fm_a.slow
lights_fm_a.current # => :yellow
lights_fm_b.current # => :green

2.3 current

The FiniteMachine allows you to query the current state by calling the current method.

fm.current  # => :red

2.4 initial

There are number of ways to provide the initial state in FiniteMachine depending on your requirements.

By default the FiniteMachine will be in the :none state and you will need to provide an explicit event to transition out of this state.

fm = FiniteMachine.new do
  event :init,  :none   => :green
  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end

fm.current # => :none
fm.init    # => true
fm.current # => :green

If you specify initial state using the initial helper, then the state machine will be created already in that state and an implicit init event will be created for you and automatically triggered upon the state machine initialization.

fm = FiniteMachine.new do
  initial :green   # fires init event that transitions from :none to :green state

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end

fm.current # => :green

Or by passing named argument :initial like so:

fm = FiniteMachine.new(initial: :green) do
  ...
end

If you want to defer setting the initial state, pass the :defer option to the initial helper. By default FiniteMachine will create init event that will allow to transition from :none state to the new state.

fm = FiniteMachine.new do
  initial :green, defer: true # Defer calling :init event

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end
fm.current # => :none
fm.init    # execute initial transition
fm.current # => :green

If your target object already has init method or one of the events names redefines init, you can use different name by passing :event option to initial helper.

fm = FiniteMachine.new do
  initial :green, event: :start, defer: true # Rename event from :init to :start

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end

fm.current # => :none
fm.start   # => call the renamed event
fm.current # => :green

By default the initial does not trigger any callbacks. If you need to fire callbacks and any event associated actions on initial transition, pass the silent option set to false like so:

fm = FiniteMachine.new do
  initial :green, silent: false  # callbacks are triggered

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red
end

2.5 terminal

To specify a final state FiniteMachine uses the terminal method.

fm = FiniteMachine.new do
  initial :green

  terminal :red

  event :slow, :green  => :yellow
  event :stop, :yellow => :red
  event :go,   :red    => :green
end

When the terminal state has been specified, you can use terminated? method on the state machine instance to verify if the terminal state has been reached or not.

fm.terminated?  # => false
fm.slow         # => true
fm.terminated?  # => false
fm.stop         # => true
fm.terminated?  # => true

The terminal can accept more than one state.

fm = FiniteMachine.new do
  initial :open

  terminal :close, :canceled

  event :resolve, :open => :close
  event :decline, :open => :canceled
end

And the terminal state can be checked using terminated?:

fm.decline
fm.terminated? # => true

2.6 is?

To verify whether or not a state machine is in a given state, FiniteMachine uses is? method. It returns true if the machine is found to be in the given state, or false otherwise.

fm.is?(:red)    # => true
fm.is?(:yellow) # => false

Moreover, you can use helper methods to check for current state using the state name itself like so

fm.red?     # => true
fm.yellow?  # => false

2.7 trigger

Transition events can be fired by calling the trigger method with the event name and remaining arguments as data. The return value is either true or false depending whether the transition succeeded or not:

fm.trigger(:ready) # => true
fm.trigger(:ready, "one", "two", "three") # => true

By default, the FiniteMachine automatically converts all the transition event names into methods:

fm.ready # => true
fm.ready("one", "two", "three") # => true

Please see States and Transitions for in-depth treatment of firing transitions.

2.7.1 :auto_methods

By default, all event names will be converted by FiniteMachine into method names. This also means that you won't be able to use event names such as :fail or :trigger as these are already defined on the machine instance. In situations when you wish to use any event name for your event names use :auto_methods keyword to disable automatic methods generation. For example, to define :fail event:

fm = FiniteMachine.new(auto_methods: false) do
  initial :green

  event :fail, :green => :red
end

And then you can use trigger to fire the event:

fm.trigger(:fail)
fm.current # => :red

2.8 can? and cannot?

To verify whether or not an event can be fired, FiniteMachine provides can? or cannot? methods. can? checks if FiniteMachine can fire a given event, returning true, otherwise, it will return false. The cannot? is simply the inverse of can?.

fm.can?(:ready)    # => true
fm.can?(:go)       # => false
fm.cannot?(:ready) # => false
fm.cannot?(:go)    # => true

The can? and cannot? helper methods take into account the :if and :unless conditions applied to events. The set of values that :if or :unless condition takes as block parameter can be passed in directly via can? and cannot? methods' arguments, after the name of the event. For instance,

fm = FiniteMachine.new do
  initial :green

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red, if: ->(_, param) { :breaks == param }
end

fm.can?(:slow) # => true
fm.can?(:stop) # => false

fm.slow                    # => true
fm.can?(:stop, :breaks)    # => true
fm.can?(:stop, :no_breaks) # => false

2.9 target

If you need to execute some external code in the context of the current state machine, pass that object as a first argument to new method.

Assuming we have a simple Engine class that holds an internal state whether the car's engine is on or off:

class Engine
  def initialize
    @engine = false
  end

  def turn_on
    @engine = true
  end

  def turn_off
    @engine = false
  end

  def engine_on?
    @engine
  end
end

And given an instance of Engine class:

engine = Engine.new

You can provide a context to a state machine by passing it as a first argument to a new call. You can then reference this context inside the callbacks by calling the target helper:

fm = FiniteMachine.new(engine) do
  initial :neutral

  event :start, :neutral => :one, unless: "engine_on?"
  event :stop,  :one => :neutral

  on_before_start { |event| target.turn_on }
  on_after_stop { |event| target.turn_off }
end

For more complex example see Integration section.

2.9.1 :alias_target

If you wish to better express the intention behind the context object, in particular when calling actions in callbacks, you can use the :alias_target option:

engine = Engine.new

fm = FiniteMachine.new(engine, alias_target: :engine) do
  initial :neutral

  event :start, :neutral => :one, unless: "engine_on?"
  event :stop, :none => :neutral, if: "engine_on?"

  on_before_start { |event| engine.turn_on }
  on_after_stop { |event| engine.turn_off }
end

Alternatively, you can use the alias_target helper method:

engine = Engine.new

Car = FiniteMachine.define do
  alias_target :engine

  initial :neutral

  event :start, :neutral => :one, if: "engine_on?"
  event :stop, :none => :neutral, if: "engine_on?"

  on_before_start { |event| engine.turn_on }
  on_after_stop { |event| engine.turn_off }
end

Then to link Car definition with Engine instance, pass the Engine instance as a first argument:

car = Car.new(engine)

Triggering start event will change Engine instance state from false to true:

engine.engine_on? # => false
car.start
car.current       # => :one
engine.engine_on? # => true

2.10 restore!

In order to set the machine to a given state and thus skip triggering callbacks use the restore! method:

fm.restore!(:neutral)

This method may be suitable when used testing your state machine or in restoring the state from datastore.

2.11 states

You can use the states method to return an array of all the states for a given state machine.

fm.states # => [:none, :green, :yellow, :red]

2.12 events

To find out all the event names supported by the state machine issue events method:

fm.events # => [:init, :ready, :go, :stop]

3. States and Transitions

The FiniteMachine DSL exposes the event helper to define possible state transitions.

The event helper accepts as a first argument the transition's name which will later be used to create method on the FiniteMachine instance. As a second argument the event accepts an arbitrary number of states either in the form of :from and :to hash keys or by using the state names themselves as key value pairs.

event :start, from: :neutral, to: :first
# or
event :start, :neutral => :first

Once specified, the FiniteMachine will create custom methods for transitioning between each state. The following methods trigger transitions for the example state machine.

  • ready
  • go
  • stop

You can always opt out from automatic method generation by using :auto_methods option.

3.1 Triggering transitions

In order to transition to the next reachable state, simply call the event's name on the FiniteMachine instance. If the transition succeeds the true value is returned, otherwise false.

fm.ready         # => true
fm.current       # => :yellow

If you prefer you can also use trigger method to fire any event by its name:

fm.trigger(:ready)  # => true

Furthermore, you can pass additional parameters with the method call that will be available in the triggered callback as well as used by any present guarding conditions.

fm.go("Piotr!")  # => true
fm.current       # => :green

By default FiniteMachine will swallow all exceptions when and return false on failure. If you prefer to be notified when illegal transition occurs see Dangerous transitions.

3.2 Dangerous transitions

When you declare event, for instance ready, the FiniteMachine will provide a dangerous version with a bang ready!. In the case when you attempt to perform illegal transition or FiniteMachine throws internal error, the state machine will propagate the errors. You can use handlers to decide how to handle errors on case by case basis see 6. Error Handling

fm.ready!  #  => raises FiniteMachine::InvalidStateError

If you prefer you can also use trigger! method to fire event:

fm.trigger!(:ready)

3.3 Multiple from states

If an event transitions from multiple states to the same state then all the states can be grouped into an array. Alternatively, you can create separate events under the same name for each transition that needs combining.

fm = FiniteMachine.new do
  initial :neutral

  event :start,  :neutral             => :one
  event :shift,  :one                 => :two
  event :shift,  :two                 => :three
  event :shift,  :three               => :four
  event :slow,   [:one, :two, :three] => :one
end

3.4 any_state transitions

The FiniteMachine offers few ways to transition out of any state. This is particularly useful when the machine already defines many states.

You can use any_state as the name for a given state, for instance:

event :run, from: any_state, to: :green
# or
event :run, any_state => :green

Alternatively, you can skip the any_state call and just specify to state:

event :run, to: :green

All the above run event definitions will always transition the state machine into :green state.

3.5 Collapsing transitions

Another way to specify state transitions under single event name is to group all your state transitions into a single hash like so:

fm = FiniteMachine.new do
  initial :initial

  event :bump, :initial => :low,
               :low     => :medium,
               :medium  => :high
end

The same can be more naturally rewritten also as:

fm = FiniteMachine.new do
  initial :initial

  event :bump, :initial => :low
  event :bump, :low     => :medium
  event :bump, :medium  => :high
end

3.6 Silent transitions

The FiniteMachine allows to selectively silence events and thus prevent any callbacks from firing. Using the silent option passed to event definition like so:

fm = FiniteMachine.new do
  initial :yellow

  event :go    :yellow => :green, silent: true
  event :stop, :green => :red
end

fm.go   # no callbacks
fm.stop # callbacks are fired

3.7 Logging transitions

To help debug your state machine, FiniteMachine provides :log_transitions option.

FiniteMachine.new(log_transitions: true) do
  ...
end

3.8 Conditional transitions

Each event takes an optional :if and :unless options which act as a predicate for the transition. The :if and :unless can take a symbol, a string, a Proc or an array. Use :if option when you want to specify when the transition should happen. If you want to specify when the transition should not happen then use :unless option.

3.8.1 Using a Proc

You can associate the :if and :unless options with a Proc object that will get called right before transition happens. Proc object gives you ability to write inline condition instead of separate method.

fm = FiniteMachine.new do
  initial :green

  event :slow, :green => :yellow, if: -> { return false }
end

fm.slow    # doesn't transition to :yellow state
fm.current # => :green

Condition by default receives the current context, which is the current state machine instance, followed by extra arguments.

fm = FiniteMachine.new do
  initial :red

  event :go, :red => :green,
        if: ->(context, a) { context.current == a }
end

fm.go(:yellow) # doesn't transition
fm.go          # raises ArgumentError

Note If you specify condition with a given number of arguments then you need to call an event with the exact number of arguments, otherwise you will get ArgumentError. Thus in above scenario to prevent errors specify condition like so:

if: ->(context, *args) { ... }

Provided your FiniteMachine is associated with another object through target helper. Then the target object together with event arguments will be passed to the :if or :unless condition scope.

class Engine
  def initialize
    @engine = false
  end

  def turn_on
    @engine = true
  end

  def turn_off
    @engine = false
  end

  def engine_on?
    @engine
  end
end

engine = Engine.new
engine.turn_on

car = FiniteMachine.new(engine) do
  initial :neutral

  event :start, :neutral => :one, if: ->(target, state) do
    state ? target.engine_on : target.engine_off
  end
end

fm.start(false)
fm.current        # => :neutral
engine.engine_on? # => false

fm.start(true)
fm.current        # => :one
engine.engine_on? # => true

When the one-liner conditions are not enough for your needs, you can perform conditional logic inside the callbacks. See 4.9 Cancelling callbacks

3.8.2 Using a Symbol

You can also use a symbol corresponding to the name of a method that will get called right before transition happens.

fm = FiniteMachine.new(engine) do
  initial :neutral

  event :start, :neutral => :one, if: :engine_on?
end

3.8.3 Using a String

Finally, it's possible to use string that will be evaluated using eval and needs to contain valid Ruby code. It should only be used when the string represents a short condition.

fm = FiniteMachine.new(engine) do
  initial :neutral

  event :start, :neutral => :one, if: "engine_on?"
end

3.8.4 Combining transition conditions

When multiple conditions define whether or not a transition should happen, an Array can be used. Furthermore, you can apply both :if and :unless to the same transition.

fm = FiniteMachine.new do
  initial :green

  event :slow, :green => :yellow,
    if: [ -> { return true }, -> { return true} ],
    unless: -> { return true }
  event :stop, :yellow => :red
end

The transition only runs when all the :if conditions and none of the unless conditions are evaluated to true.

3.9 Choice pseudostates

Choice pseudostate allows you to implement conditional branch. The conditions of an event's transitions are evaluated in order to select only one outgoing transition.

You can implement the conditional branch as ordinary events grouped under the same name and use familiar :if/:unless conditions:

fm = FiniteMachine.define do
  initial :green

  event :next, :green => :yellow, if: -> { false }
  event :next, :green => :red,    if: -> { true }
end

fm.current # => :green
fm.next
fm.current # => :red

The same conditional logic can be implemented using much shorter and more descriptive style using choice method:

fm = FiniteMachine.new do
  initial :green

  event :next, from: :green do
    choice :yellow, if: -> { false }
    choice :red,    if: -> { true }
  end
end

fm.current # => :green
fm.next
fm.current # => :red

3.9.1 Dynamic choice conditions

Just as with event conditions you can make conditional logic dynamic and dependent on parameters passed in:

fm = FiniteMachine.new do
  initial :green

  event :next, from: :green do
    choice :yellow, if: ->(context, a) { a < 1 }
    choice :red,    if: ->(context, a) { a > 1 }
    default :red
  end
end

fm.current # => :green
fm.next(0)
fm.current # => :yellow

If more than one of the conditions evaluates to true, a first matching one is chosen. If none of the conditions evaluate to true, then the default state is matched. However if default state is not present and non of the conditions match, no transition is performed. To avoid such situation always specify default choice.

3.9.2 Multiple from states

Similarly to event definitions, you can specify the event to transition from a group of states:

FiniteMachine.new do
  initial :red

  event :next, from: [:yellow, :red] do
    choice :pink, if: -> { false }
    choice :green
  end
end

Or from any state using the :any state name like so:

FiniteMachine.new do
  initial :red

  event :next, from: :any do
    choice :pink, if: -> { false }
    choice :green
  end
end

4. Callbacks

You can register a callback to listen for state transitions and events triggered, and based on these perform custom actions. There are five callbacks available in FiniteMachine:

  • on_before - triggered before any transition
  • on_exit - triggered when leaving any state
  • on_transition - triggered during any transition
  • on_enter - triggered when entering any state
  • on_after - triggered after any transition

Use the state or event name as a first parameter to the callback helper followed by block with event argument and a list arguments that you expect to receive like so:

on_enter(:green) { |event, a, b, c| ... }

When you subscribe to the :green state change, the callback will be called whenever someone triggers event that transitions in or out of that state. The same will happen on subscription to event ready, namely, the callback will be called each time the state transition method is triggered regardless of the states it transitions from or to.

fm = FiniteMachine.new do
  initial :red

  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red

  on_before :ready do |event, time1, time2, time3|
    puts "#{time1} #{time2} #{time3} Go!" }
  end
  on_before :go do |event, name|
    puts "Going fast #{name}"
  end
  on_before(:stop) { |event| ... }
end

fm.ready(1, 2, 3)
fm.go("Piotr!")

Note Regardless of how the state is entered or exited, all the associated callbacks will be executed. This provides means for guaranteed initialization and cleanup.

4.1 on_(enter|transition|exit)

The on_enter callback is executed before given state change is fired. By passing state name you can narrow down the listener to only watch out for enter state changes. Otherwise, all enter state changes will be watched.

The on_transition callback is executed when given state change happens. By passing state name you can narrow down the listener to only watch out for transition state changes. Otherwise, all transition state changes will be watched.

The on_exit callback is executed after a given state change happens. By passing state name you can narrow down the listener to only watch out for exit state changes. Otherwise, all exit state changes will be watched.

4.2 on_(before|after)

The on_before callback is executed before a given event happens. By default it will listen out for all events, you can also listen out for specific events by passing event's name.

This callback is executed after a given event happened. By default it will listen out for all events, you can also listen out for specific events by passing event's name.

4.3 once_on

FiniteMachine allows you to listen on initial state change or when the event is fired first time by using the following 5 types of callbacks:

  • once_on_enter
  • once_on_transition
  • once_on_exit
  • once_before
  • once_after

4.4 Execution sequence

Assuming we have the following event specified:

event :go, :red => :yellow

Then by calling go event the following callbacks sequence will be executed:

  • on_before - generic callback before any event
  • on_before :go - callback before the go event
  • on_exit - generic callback for exit from any state
  • on_exit :red - callback for the :red state exit
  • on_transition - callback for transition from any state to any state
  • on_transition :yellow - callback for the :red to :yellow transition
  • on_enter - generic callback for entry to any state
  • on_enter :yellow - callback for the :yellow state entry
  • on_after - generic callback after any event
  • on_after :go - callback after the go event

4.5 Callback parameters

All callbacks as a first argument yielded to a block receive the TransitionEvent object with the following attributes:

  • name - the event name`
  • from - the state transitioning from`
  • to - the state transitioning to`

followed by the rest of arguments that were passed to the event method.

fm = FiniteMachine.new do
  initial :red

  event :ready, :red => :yellow

  on_before_ready do |event, time|
    puts "lights switching from #{event.from} to #{event.to} in #{time} seconds"
  end
end

fm.ready(3)
#  => "lights switching from red to yellow in 3 seconds"

4.6 Duplicate callbacks

You can define any number of the same kind of callback. These callbacks will be executed in the order they are specified.

Given the following state machine instance:

fm = FiniteMachine.new do
  initial :green

  event :slow, :green => :yellow

  on_enter(:yellow) { puts "this is run first" }
  on_enter(:yellow) { puts "then this is run" }
end

Triggerring the :slow event results in:

fm.slow
# => "this is run first"
# => "then this is run"

4.7 Fluid callbacks

Callbacks can also be specified as full method calls separated with underscores:

fm = FiniteMachine.define do
  initial :red

  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red

  on_before_ready { |event| ... }
  on_before_go    { |event| ... }
  on_before_stop  { |event| ... }
end

4.8 Methods inside callbacks

Given a class Car:

class Car
  attr_accessor :reverse_lights

  def turn_reverse_lights_off
    @reverse_lights = false
  end

  def turn_reverse_lights_on
    @reverse_lights = true
  end
end

We can easily manipulate state for an instance of a Car class:

car = Car.new

By defining finite machine using the instance:

fm = FiniteMachine.new(car) do
  initial :neutral

  event :forward, [:reverse, :neutral] => :one
  event :back,    [:neutral, :one] => :reverse

  on_enter_reverse { |event| target.turn_reverse_lights_on }
  on_exit_reverse  { |event| target.turn_reverse_lights_off }
end

Note that you can also fire events from callbacks.

fm = FiniteMachine.new do
  initial :neutral

  event :forward, [:reverse, :neutral] => :one
  event :back,    [:neutral, :one] => :reverse

  on_enter_reverse { |event| forward("Piotr!") }
  on_exit_reverse  { |event, name| puts "Go #{name}" }
end

Then triggerring :back event gives:

fm.back  # => Go Piotr!

For more complex example see Integration section.

4.9 Cancelling callbacks

A simple way to prevent transitions is to use 3 Conditional transitions.

There are times when you want to cancel transition in a callback. For example, you have logic which allows transition to happen only under certain complex conditions. Using cancel_event inside the on_(enter|transition|exit) or on_(before|after) callbacks will stop all the callbacks from firing and prevent current transition from happening.

For example, the following state machine cancels any event leaving :red state:

fm = FiniteMachine.new do
  initial :red

  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red

  on_exit :red do |event|
    ...
    cancel_event
  end
end

Then firing :ready event will not transition out of the current :red state:

fm.current  # => :red
fm.ready
fm.current  # => :red

4.10 Asynchronous callbacks

By default all callbacks are run synchronously. In order to add a callback that runs asynchronously, you need to pass second :async argument like so:

on_enter(:green, :async) do |event| ... end
# or
on_enter_green(:async) { |event| }

This will ensure that when the callback is fired it will run in separate thread outside of the main execution thread.

4.11 Instance callbacks

When defining callbacks you are not limited to the FiniteMachine block definition. After creating an instance, you can register callbacks the same way as before by calling on and supplying the type of notification and state/event you are interested in.

For example, given the following state machine:

fm = FiniteMachine.new do
  initial :red

  event :ready, :red    => :yellow
  event :go,    :yellow => :green
  event :stop,  :green  => :red
end

We can add callbacks as follows:

fm.on_enter(:yellow) { |event| ... }
# or
fm.en_enter_yellow { |event| ... }

5. Error Handling

By default, the FiniteMachine will throw an exception whenever the machine is in invalid state or fails to transition.

  • FiniteMachine::TransitionError
  • FiniteMachine::InvalidStateError
  • FiniteMachine::InvalidCallbackError

You can attach specific error handler using the 'handle' with the name of the error as a first argument and a callback to be executed when the error happens. The handle receives a list of exception class or exception class names, and an option :with with a name of the method or a Proc object to be called to handle the error. As an alternative, you can pass a block.

fm = FiniteMachine.new do
  initial :green, event: :start

  event :slow,  :green  => :yellow
  event :stop,  :yellow => :red

  handle FiniteMachine::InvalidStateError do |exception|
    # run some custom logging
    raise exception
  end

  handle FiniteMachine::TransitionError, with: -> { |exception| ... }
end

5.1 Using target

You can pass an external context as a first argument to the FiniteMachine initialization that will be available as context in the handler block or :with value. For example, the log_error method is made available when :with option key is used:

class Logger
  def log_error(exception)
    puts "Exception : #{exception.message}"
  end
end

fm = FiniteMachine.new(logger) do
  initial :green

  event :slow, :green  => :yellow
  event :stop, :yellow => :red

  handle "InvalidStateError", with: :log_error
end

6. Stand-alone

FiniteMachine allows you to separate your state machine from the target class so that you can keep your concerns broken in small maintainable pieces.

6.1 Creating a Definition

You can turn a class into a FiniteMachine by simply subclassing FiniteMachine::Definition. As a rule of thumb, every single public method of the FiniteMachine is available inside your class:

class Engine < FiniteMachine::Definition
  initial :neutral

  event :forward, [:reverse, :neutral] => :one
  event :shift, :one => :two
  event :back,  [:neutral, :one] => :reverse

  on_enter :reverse do |event|
    target.turn_reverse_lights_on
  end

  on_exit :reverse do |event|
    target.turn_reverse_lights_off
  end

  handle FiniteMachine::InvalidStateError do |exception|
    ...
  end
end

6.2 Targeting definition

The next step is to instantiate your state machine and use a custom class instance to load specific context.

For example, having the following Car class:

class Car
  def turn_reverse_lights_off
    @reverse_lights = false
  end

  def turn_reverse_lights_on
    @reverse_lights = true
  end

  def reverse_lights?
    @reverse_lights ||= false
  end
end

Thus, to associate Engine to Car do:

car = Car.new
engine = Engine.new(car)

car.reverse_lignts?  # => false
engine.back
car.reverse_lights?  # => true

Alternatively, create method inside the Car that will do the integration like so:

class Car
  ... #  as above

  def engine
    @engine ||= Engine.new(self)
  end
end

6.3 Definition inheritance

You can create more specialised versions of a generic definition by using inheritance. Assuming a generic state machine definition:

class GenericStateMachine < FiniteMachine::Definition
  initial :red

  event :start, :red => :green

  on_enter { |event| ... }
end

You can easily create a more specific definition that adds new events and more specific callbacks to the mix.

class SpecificStateMachine < GenericStateMachine
  event :stop, :green => :yellow

  on_enter(:yellow) { |event| ... }
end

Finally to use the specific state machine definition do:

specific_fsm = SpecificStateMachine.new

7. Integration

Since FiniteMachine is an object in its own right, it leaves integration with other systems up to you. In contrast to other Ruby libraries, it does not extend from models (i.e. ActiveRecord) to transform them into a state machine or require mixing into existing classes.

7.1 Plain Ruby Objects

In order to use FiniteMachine with an object, you need to define a method that will construct the state machine. You can implement the state machine using the new DSL or create a separate object that can be instantiated. To complete integration you will need to specify target context to allow state machine to communicate with the other methods inside the class like so:

class Car
  def turn_reverse_lights_off
    @reverse_lights = false
  end

  def turn_reverse_lights_on
    @reverse_lights = true
  end

  def reverse_lights_on?
    @reverse_lights || false
  end

  def gears
    @gears ||= FiniteMachine.new(self) do
      initial :neutral

      event :start, :neutral => :one
      event :shift, :one => :two
      event :shift, :two => :one
      event :back,  [:neutral, :one] => :reverse

      on_enter :reverse do |event|
        target.turn_reverse_lights_on
      end

      on_exit :reverse do |event|
        target.turn_reverse_lights_off
      end

      on_transition do |event|
        puts "shifted from #{event.from} to #{event.to}"
      end
    end
  end
end

Having written the class, you can use it as follows:

car = Car.new
car.gears.current      # => :neutral
car.reverse_lights_on? # => false

car.gears.start        # => "shifted from neutral to one"

car.gears.back         # => "shifted from one to reverse"
car.gears.current      # => :reverse
car.reverse_lights_on? # => true

7.2 ActiveRecord

In order to integrate FiniteMachine with ActiveRecord simply add a method with state machine definition. You can also define the state machine in separate module to aid reusability. Once the state machine is defined use the target helper to reference the current class. Having defined target you call ActiveRecord methods inside the callbacks to persist the state.

You can use the restore! method to specify which state the FiniteMachine should be put back into as follows:

class Account < ActiveRecord::Base
  validates :state, presence: true

  before_validation :set_initial_state, on: :create

  def set_initial_state
    self.state = manage.current
  end

  after_find :restore_state
  after_initialize :restore_state

  def restore_state
    manage.restore!(state.to_sym) if state.present?
  end

  def manage
    @manage ||= FiniteMachine.new(self) do
      initial :unapproved

      event :enqueue, :unapproved => :pending
      event :authorize, :pending => :access

      on_enter do |event|
        target.state = event.to
      end
    end
  end
end

account = Account.new
account.state   # => :unapproved
account.manage.enqueue
account.state   # => :pending
account.manage.authorize
account.state   # => :access

Please note that you do not need to call target.save inside callback, it is enough to just set the state. It is much more preferable to let the ActiveRecord object to persist when it makes sense for the application and thus keep the state machine focused on managing the state transitions.

7.3 Transactions

When using FiniteMachine with ActiveRecord it advisable to trigger state changes inside transactions to ensure integrity of the database. Given Account example from section 7.2 one can run event in transaction in the following way:

ActiveRecord::Base.transaction do
  account.manage.enqueue
end

If the transition fails it will raise TransitionError which will cause the transaction to rollback.

Please check the ORM of your choice if it supports database transactions.

8 Tips

Creating a standalone FiniteMachine brings a number of benefits, one of them being easier testing. This is especially true if the state machine is extremely complex itself. Ideally, you would test the machine in isolation and then integrate it with other objects or ORMs.

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Code of Conduct

Everyone interacting in the FiniteMachine project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Copyright

Copyright (c) 2014 Piotr Murach. See LICENSE for further details.

finite_machine's People

Contributors

ahorek avatar bradgessler avatar craiglittle avatar dceluis avatar eppo avatar gitter-badger avatar igorpolyakov avatar mensfeld avatar piotrmurach avatar rabelmarte avatar reggieb avatar rrrene avatar rud avatar shioyama avatar stavro avatar vkononov avatar wiiikiii avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

finite_machine's Issues

State transitions should not include states prior to initial

Ok this seems like it has to be a bug.

In my model:

def get_state
  log "Setting current state to {self.current_state}"
  self.current_state
end

def fm
  context = self
  @fm ||= FiniteMachine.define to
    target context
    initial (context.get_state || :pending)
    #...more follows

Among others, I have the following event:

event  :finalized [:ready, :valid] => :final

And the following callbacks --

on_enter :ready do |event|
  log "current state: #{state}"
  log "In :ready callback doing #{event.from} to #{event.to}"
end

on_enter :final |event|
  log "In :final callback"
end

When I call

item.fm.finalized 

for an item that is already in the :ready state, here's what the log looks like:

Setting current state to ready
current state: ready
In :ready callback doing none to ready
In :final callback

I don't understand why the item starting at the none state (and you can see from the log that it's in the :ready state.) I assume it's doing the none -> ready callback as part of the initialization, but is that right? I don't believe any callbacks should be done until after the initial state is set.

Event names collide with instance names

Some event names collide with instance methods in FiniteMachine.

events {
    event :subscribe, [:zero, :time, :count] => :subscription_refundable
}
You tried to define an event named "subscribe", however this would generate "instance" method "subscribe", which is already defined by FiniteMachine

self-transition problem

I expect this code to output 3 digits (911). But it doesn't. Is the self transition :dialing => :dialing causing problems?

require 'finite_machine'

phone = FiniteMachine.define do
  initial :on_hook

  digits = []

  events {
    # On Hook
    event :digit, :on_hook => :dialing

    # Dialing
    event :digit,    :dialing => :dialing
    event :off_hook, :dialing => :alerting
  }

  callbacks {
    on_enter(:digit)    { |event, digit| digits << digit; puts "digit: #{digit}" }
    on_enter(:off_hook) { |event| puts "Dialling #{digits.join}" }

    on_transition { |event|
      puts "%s [ %s -> %s ]" %
        [event.name, event.from.capitalize, event.to.capitalize]
    }
  }
end

phone.digit(9)
phone.digit(1)
phone.digit(1)
phone.off_hook

conditional doesn't work or I don't understand

I don't expect the following code to transition at all. It appears the conditional transition (false) is taken anyway. Why?

require 'finite_machine'

def false?
  false
end

bug = FiniteMachine.define do
  initial :initial

  events {
    event :bump,  :initial => :low, if: :false?
    event :bump,  :low     => :medium
  }

  callbacks {
    on_enter_event { |event| puts "#{event.name}\t#{event.from}\t=>\t#{event.to}" }
  }
end

bug.bump
bug.bump

Errors with Ruby 3.1.2

Describe the problem

Lots of warnings triggered when used with ruby 3.1.2

Steps to reproduce the problem

With ruby 3.1.2

bundle exec rspec

Actual behaviour

Error:

lib/finite_machine/observer.rb:22:in block in cleanup_callback_queue': undefined local variable or method callback_queue' for FiniteMachine::Observer:Class (NameError)

Expected behaviour

Specs pass without warnings

Describe your environment

  • OS version: osx 12
  • Ruby version: 3.1.2
  • FiniteMachine version: master

initial state from target class

Hello

I like your coding style and the separation of the state machine and the target class.

I'd like the state machine to set the initial value according to a value from the target class.

Something like

require 'finite_machine'

class Video
  attr_accessor :state

  def initialize
    self.state = 'pending'
  end

  def state_machine
    @state_machine ||= FiniteMachine.define do
      initial self.state

      target self

      events do
        event :enqueue_generate, :pending => :waiting_for_generation
        event :generate, :waiting_for_generation => :generating
        event :finish_generation, :generating => :waiting_for_upload
        event :enqueue_youtube_upload, :waiting_for_upload => :uploading
        event :finish_upload, :uploading => :uploaded
      end

      callbacks do
        on_enter do |obj, event|
          puts "on_enter from #{event.from} to #{event.to}"
        end

        on_exit do |obj, event|
          puts "on_exit from #{event.from} to #{event.to}"
        end

        on_transition do |obj, event|
          puts "on_transition from #{event.from} to #{event.to}"
        end
      end
    end
  end
end

v = Video.new
v.state
# "pending"
v.state_machine.generate
# FiniteMachine::TransitionError: inappropriate current state 'none'

How can I sync the state of the machine with the state of a variable in my target class ?
Basically I'd like to save the state of the state machine in an ActiveRecord class.

Thanks !

Geoffroy

Event deferral

@kjwierenga I'm planning to add event deferring reference

My suggestion for syntax would be to do

wait_until_transition :state_name { |event| .... }

This would save the callback in internal queue and wait until the particular condition is met. In similar manner I would have wait_unitl_enter, wait_until_exit etc... Does it sound reasonable?

Invalid TransitionEvent properties

The transition event object isn't populated with the proper to state.

Example Machine:

class TestMachine < FiniteMachine::Definition
  initial  :pending

  events {
    event :confirm, from: :pending do
      choice :confirmed, if: -> { false }
      choice :manual_review
    end

    event :confirm, from: :manual_review do
      choice :confirmed, if: -> { true }
      choice :manual_review
    end
  }

  callbacks {
    on_enter do |event|
      puts event.inspect
    end
  }
end

Usage:

t = TestMachine.new
t.state #=> :pending

t.confirm
#<FiniteMachine::TransitionEvent:0x007fdeb9a20a68 @name=:confirm, @from=:pending, @to=:manual_review>
t.state #=> :manual_review

t.confirm
#<FiniteMachine::TransitionEvent:0x007fdeb9a5e8e0 @name=:confirm, @from=:manual_review, @to=nil>  ** INCORRECT - @to should be :confirmed **
t.state #=> :confirmed

In the example above, @to should be :confirmed in the second TransitionEvent

alias_target doesn't seem to work

Describe the problem

I'm upgrading a Rails app from FiniteState 0.11.3 to 0.13.0 (the upgrade seems to be needed to work with Ruby 2.7.1).

Currently, I have the following code:

#app/models/concerns/with_state.rb
MyStateMachine.state_machine.new(self)

#app/state_machines/my_state_machine.rb
class MyStateMachine < FiniteMachine::Definition
  alias_target :model

  callbacks do
    on_transition do |event|
      model.state = event.to
    end
  end
end

Which, due the change in the API, has been updated as follows (based on https://github.com/piotrmurach/finite_machine#291-alias_target)

Steps to reproduce the problem

#app/models/concerns/with_state.rb
MyStateMachine.state_machine.new(self, alias_target: :model)

#app/state_machines/my_state_machine.rb
class MyStateMachine < FiniteMachine::Definition
  on_transition do |event|
    model.state = event.to
  end
end

Actual behavior

But the change raises the following error

NameError:
undefined local variable or method `model' for #FiniteMachine::StateMachine:0x00007f8e12c073b0

Expected behavior

This is not really a big issue for me since it can be fixed with target.state = event.to but perhaps it can cause some problems for other users.

Is the error something expected?

Describe your environment

  • OS version: MacOS 10.11.6
  • Ruby version: 2.7.1
  • Loaf version:

HIGH_PRIORITY_ANY_STATE and HIGH_PRIORITY_ANY_EVENT

I needed a general callbacks to be fired before specific ones, so I created a monkey patch to specify high priority any event/state:

HIGH_PRIORITY_ANY_EVENT = :high_priority_any_event
HIGH_PRIORITY_ANY_STATE = :high_priority_any_state

  def trigger(event, *args, &block)
      sync_exclusive do
        [event.type].each do |event_type|
          [HIGH_PRIORITY_ANY_STATE, HIGH_PRIORITY_ANY_EVENT, event.name, ANY_STATE, ANY_EVENT].each do |event_name|
            hooks.call(event_type, event_name) do |hook|
              handle_callback(hook, event)
              off(event_type, event_name, &hook) if hook.is_a?(Once)
            end
          end
        end
      end
    end

and I am using it like that:

on_enter :high_priority_any_state do |event|
end

It would be nice if I could specify a priority for callbacks, for example the default priority could be 0, and if I would like to "general" callback to be fired before "specific" one I could do something like:

on_enter priority: 1 do |event|
end

or to be more explicit

on_enter :any, priority: 1 do |event|
end

When an error occurs on transition the current state is not "rollback"

Hi, I do believe there is a misbehaving of the Finite Machine. When I am transitioning to a new state and if an error occurs and raises an exception in the callback I am expecting that the transition does not happen. However what is happening is that the Finite Machine current state after failure is the new state instead of the initial one.

Code sample:

require 'finite_machine'

fsm = FiniteMachine.define do
  initial :green

  events { event :slow, :green => :yellow }

  callbacks { on_enter { raise RuntimeError } }
end

begin
  puts fsm.current
  # green
  fsm.slow
rescue RuntimeError
  puts fsm.current
  # yellow (I would expect the "green" state here)
end

Note: Using the on_before callback it works but that is because it does not change the current state to the new one.

Persistence

Pardon me for making this an issue. It's really a question.

I'm having trouble persisting the state. More specifically, I'd like to instantiate the state machine with the prior saved state.

All I've been able to find is the DSL that lets me define an initial state, or define an event that transitions to an initial state. Both require me to define the initial state at coding time.

fm = FiniteMachine.define do
  initial :red
fm = FiniteMachine.define do
  events {
    event :start, :none   => :green

In practice, I'm defining a "standalone" along the lines of,

class Engine < FiniteMachine::Definition
  initial :neutral

What I'd like is to define the initial state in the initializer for that class, something like:

class Engine < FiniteMachine::Definition
     def initialize(car, state)
       initial state
       target car
     end

However that does not work. Should it? I get :none as the current state after initialization. Still reading the code.

prepend FiniteMachine instance methods with a prefix/postfix like "__"

There might be a reason to define events like "success" or "fail". For "fail" event that error appear:

FiniteMachine::AlreadyDefinedError: You tried to define an event named "fail", however this would generate "instance" method "fail", which is already defined by FiniteMachine

v0.12.1 missing sync dependency

Describe the problem

The currently released version of finite_machine - v0.12.1 - raises LoadError (cannot load such file -- sync) on requiring the library.

This was fixed in (an unreleased) v0.13.0 here.

It seems that the problem slipped through the CI, since coveralls also depends on sync transitively through tins. But that gem is part of a "metrics" group in the Gemfile and not installed at runtime.

Steps to reproduce the problem

Ensure that coveralls & sync gem are not installed.

git clone [email protected]:piotrmurach/finite_machine.git -b v0.12.1
cd finite_machine
bundle install --without=metrics
ruby -r./lib/finite_machine -e 'puts "works"'

Actual behaviour

LoadError is raised due to sync dependency missing.

Expected behaviour

No LoadError is raised.

Describe your environment

  • OS version: Linux dev 4.15.0-76-generic #86-Ubuntu SMP Fri Jan 17 17:24:28 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
  • Ruby version: ruby 2.7.0p0 (2019-12-25 revision 647ee6f091)

Transition does not happen if any_state is used in the definition.

Describe the problem

Transition does not happen if any_state is used in the definition

Steps to reproduce the problem

class MySM < FiniteMachine::Definition

    # Does not work - definition accepted but blocks transition
    event :shouldturnon, from: any_state, to: :on

    # These work fine
    event :turnson, to: :on
    event :init, none: :off

    # Log state transitions
    on_before do |event|
        if event.from != event.to
            puts 'Working state change from '\
                "#{event.from} to #{event.to}" 
        else
            puts 'Impossible state change from '\
                "#{event.from} to #{event.to}"
        end
    end

    on_enter(:off) do |event|
      puts 'Off fired OK'
    end

    on_enter(:on) do
      puts 'This is never triggered if any_state is used in definition'
    end

  end

  mymachine = MySM.new

  #Works
  mymachine.init

  #Does not work
  mymachine.shouldturnon

  #Same thing works
  mymachine.turnson

Actual behaviour

Transition does not happen if any_state is used in the from definition

Expected behaviour

Transition should happen

Describe your environment

  • OS version: ubuntu 22.04, armbian 22.08
  • Ruby version: 3.0.2
  • FiniteMachine version: 0.14.0

This worked fine with earlier ruby/finite_machine where I used the

event :turnoff, any: :off

notation. Bit me when I upgraded.

Conditional branching

@kjwierenga As requested in this issue I came around to think that this should be implemented. My current thoughts on semantics would be to allow specifying branch states as an array, like so:

event :next, :company_form => [:agreement_form,:promo_form],
              if: -> (registrant, user) { user.has_promo? }

Depending on the event condition we would choose appropriate state. One important question to ask is how to distinguish between event conditional logic and state condition. Currently the :if/:unless allow you to guard the transition, that is, ensure the transition succeeds or fails. However, the branching condition would allow to choose between possible states, e.i. it would always succeed. One suggestion would be to use another key argument

event :next, :company_form => [:agreement_form,:promo_form],
              branch: -> (registrant, user) { user.has_promo? }

Any thoughts?

Substates vs sub machines

@kjwierenga Another area I was thinking about is the ability to allow for substates or sub machines. Of course, according to UML you could have one or the other for a given state. I think this would allow to build far more complex state machines. On the other hand, I believe that this would require a lot of work implementation wise since even the definition of current state gets really blurry. Any thoughts?

Error when conditional transition is undefined

Hey there!

I'm getting an exception when attempting to make a transition that is not defined in my state machine. I'm expecting it to not transition silently as stated in the docs under the "Dynamic choice conditions" section:

However if default state is not present and non of the conditions match, no transition is performed.

Here's the relevant code:

FiniteMachine.define do
  initial :inactive

  target ticket

  alias_target :ticket

  events {
    event :advance, :from => [:inactive, :paused, :fulfilled] do
      choice :active, :if => -> {
        ticket.working? && !ticket.pending? && !ticket.on_hold?
      }
    end

    event :advance, :from => [:inactive, :active, :fulfilled] do
      choice :paused, :if => -> { ticket.pending? || ticket.on_hold? }
    end

    event :advance, :from => [:inactive, :active, :paused] do
      choice :fulfilled, :if => -> { ticket.finished? }
    end
  }
end

For example, when in the paused state and calling advance while the ticket is still in the paused state, I'm getting the following exception:

FiniteMachine::TransitionError: NoMethodError: undefined method `to_states' for nil:NilClass
    occured at /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/transition.rb:202:in `update_state'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/transition.rb:232:in `block in call'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:233:in `block in sync_synchronize'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `handle_interrupt'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `sync_synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/two_phase_lock.rb:26:in `synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/threadable.rb:14:in `sync_exclusive'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/transition.rb:229:in `call'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/state_machine.rb:283:in `block in transition'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:233:in `block in sync_synchronize'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `handle_interrupt'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `sync_synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/two_phase_lock.rb:26:in `synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/threadable.rb:14:in `sync_exclusive'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/state_machine.rb:274:in `transition'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/event.rb:96:in `block in call'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:233:in `block in sync_synchronize'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `handle_interrupt'
    /opt/.rbenv/versions/2.1.6/lib/ruby/2.1.0/sync.rb:230:in `sync_synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/two_phase_lock.rb:26:in `synchronize'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/threadable.rb:14:in `sync_exclusive'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/event.rb:91:in `call'
    /home/vagrant/bundle/ruby/2.1.0/gems/finite_machine-0.10.0/lib/finite_machine/event_builder.rb:61:in `block in define_event_transition'
    (irb):3:in `irb_binding'

I'm able to avoid the exception by adding a few more conditional transitions:

event :advance, :from => :active do
  choice :active, :silent => true
end

event :advance, :from => :paused do
  choice :paused, :silent => true
end

event :advance, :from => :fulfilled do
  choice :fulfilled, :silent => true
end

However, it'd be nice if I didn't have to add this boilerplate.

Am I doing something wrong here? Please let me know if you'd like me to provide any more context.

Thanks!

state machines definition inheritance

Why inherited state machines doesn't inherit callbacks and what can be done to fix it?

For example SpecificStateMachine that inherits from GenericStateMachine doesn't inherit "on_enter" callback:

  class GenericStateMachine < FiniteMachine::Definition
    callbacks do
      on_enter do |event|
        target.state = event.to
      end
    end
  end

  class SpecificStateMachine < GenericStateMachine
    callbacks do
      on_after :accept do |event|
        #TODO
      end
    end
  end

Setting target that responds to to_hash

Describe the problem

I am using Sequel models as a target for FiniteMachine. The trouble is that when I initialize the finite_machine object ruby recognizes the to_hash method (I believe introduced by the json_serializer plugin) and puts the model's values into **options instead of putting the model itself into *args

This would be an issue for anything that Ruby recognizes as a hash, not just Sequel models.

Steps to reproduce the problem

my_model = MyModel.create(a: 1, b: 2)
my_machine = FiniteMachine.new(my_model) do
            # etc.
end
my_machine.target == my_model # false

Workaround

I'm setting my_machine.env.target = my_model immediately after creating the machine.

Possible Fixes

Allow FiniteMachine.new(target: my_model) ?
my_machine.set_target(target) method ?
Warn if there are keys in **options that aren't recognized ?

Describe your environment

  • OS version: Windows 10
  • Ruby version: ruby 2.5.5p157 (2019-03-15 revision 67260) [x64-mingw32]

Lockup of two SM instances

I spent quite some time until I figured that FM instances are not independent, they share a common queue/event source, which actually causes a lockup when two independent threads (foo, bar) use two - seemingly independent - SM instances (A & B) where thread foo from the callback of SM A wants to

  1. signal thread bar to exit and wait for it to exit
  2. bar on receiving the signal wants to trigger a state change in FM B and exit

In this case the SM will lock up in a deadlock.

Here's the code snippet attached to reproduce the issue - you can set WORKAROUND_ACTIVE to activate a workaround with a trade-off. You can signal TTIN to the app while being locked up to see the issue.

This may be a design limitation but then it should be noted in the - otherwise excellent - documentation. I found this out the 'hard way' by getting an unexpected and seemingly inexplicable thread lockup.

code.zip

callback order

Given the following program to show the order of callbacks:

require 'finite_machine'

fsm = FiniteMachine.define do
  initial :previous

  events {
    event :go, :previous => :next, if: -> { puts "checking condition"; true }
  }

  callbacks {
    on_exit_state  { |event| puts "exit_#{event.from}" }
    on_enter_event { |event| puts "\tbefore_#{event.name}"    }
    on_enter(:any) { |event| puts "\t\ttransition: #{event.name}: #{event.from} -> #{event.to}" }
    on_enter_state { |event| puts "\tenter_#{event.to}"  }
    on_exit_event  { |event| puts "after_#{event.name}"     }
  }
end

fsm.go

The output is:

checking condition
exit_previous
    before_go
        transition: go: previous -> next
    enter_next
after_go

I would prefer and expect the following order:

before_go
    checking condition
    exit_previous
        transition: go: previous -> next
    enter_next
after_go

For reference see http://en.wikipedia.org/wiki/UML_state_machine#Transition_execution_sequence.

ActiveRecord example in README doesn't seem to work.

I'm building a Job object that I want to persist via ActiveRecord. I'm using Rails 4.1.1 and Ruby 1.9.3.

So following the README I've created this:

class Job < ActiveRecord::Base

  validates :state, presence: true

  def initialize(attrs = {})
    super
    @manage.restore!(state) if state
  end

  def manage
    context = self
    @manage ||= FiniteMachine.define do
      target context

      initial :unapproved

      events {
        event :enqueue, :unapproved => :pending
        event :authorize, :pending => :access
      }

      callbacks {
        on_enter_state do |event|
          target.state = event.to
          target.save
        end
      }
    end
  end

end

When I do this:

j = Job.new(state: :unapproved)

I get:

NoMethodError: undefined method `restore!' for nil:NilClass

I can fix that by changing the initialize method to:

  def initialize(attrs = {})
    super
    manage.restore!(state) if state
  end

But then I get this error

NoMethodError: undefined method `on_enter_state' for #<FiniteMachine::Observer:0x0000000459cb18>

I can fix this by changing the callbacks declaration to

      callbacks {
        on_transition do |event|
          target.state = event.to
          target.save
        end
      }

This means that every time there is a transition, the new state gets saved. However, it doesn't set the initial state. So: Job.create fails because the state is still nil, and so the validation fail.

I'm tempted to just do:

  before_validation :set_initial_state, on: :create

  def set_initial_state
    self.state = manage.initial_state unless state
  end

Should callbacks call on_enter_state, and if not how should I set the initial state?

[ Feature Request ] Handling For Multiple State Machines

I understand the basics of creating state machines, although what I'm noticing is a state machine for one state machine. In some of the stuff I do, I might have three different states to check for. Is there currently a way to implement this in the normal way, or should I just approach it like I have been:

Create three classes in a module. Within each class have three self referenced methods. In each method, create a state machine that detect the state for different "method states", and point to each defined method state. For simple stuff this is fine, but for more complex things, this can get kind of complicated.

This is less an issue with a specific algorithm, and more something I'd like to do: have handling for different states for different parts of the system. For example, per analogy I used:

Fold paper air plane.
Check if paper air plane soars.
If paper air planes is able to fly, keep model type in data model.
Else if paper air plane crashes, restart algorithm with different folded model.
Repeat until working plane model is found.

Granted this is an evolutionary model in a different sense than is found in Machine Learning. But each model would theoretically have its own state machine.

NoMethodError problem

NoMethodError: undefined method <=' for #<NoMethodError:0x0000003c773d50> from /home/jason/.rvm/gems/ruby-2.1.2/gems/finite_machine-0.7.1/lib/finite_machine/catchable.rb:58:inblock in handler_for_error'

Just started getting these, and haven't had any time to figure out why.

version of can? and cannot? that takes into account conditions

I'd love to see a version of can? and cannot? that takes into account the conditional transitions (if: and :unless). It seems this gem is set up for this perfectly since the logic of if a state can transition has been separated from the actual transition code.

Cancelling inside callbacks

@craiglittle Cancelling inside callbacks is buggy, i.e. returning integer matching CANCELLED halts the callbacks stack. Which leads me to think, that either the CANCELLED should be a unique value or when callbacks evaluate to false the stack is halted. Any thoughts?

on_transition is not setting @to correctly

on_transition is not setting @to correctly

Code:

require 'finite_machine'
require 'pp'

class StopLight < FiniteMachine::Definition
   events {
      initial :red
      event :power_on, :off => :red
      event :power_off, :any => :off
      event :change, :red    => :green
      event :change, :green  => :yellow
      event :change, :yellow => :red
   }
   callbacks {
      on_transition do |event|
         pp event
      end
   }
end


sl = StopLight.new

loop do
   puts "***Change"
   sl.change
   if rand(5) == 0
      puts "*** Whoops. Lost power!"
      sl.power_off
   end
   sleep 1
   puts
end

This exception at the the following output is excepted and correct. The unexpected behavior is @to is nil in TransitionEvent

jblack@comet:~$ ruby light.rb 
***Change
#<FiniteMachine::TransitionEvent:0x00000001fa0658
 @from=:red,
 @name=:change,
 @to=:green>

***Change
#<FiniteMachine::TransitionEvent:0x00000001eff690
 @from=:green,
 @name=:change,
 @to=:yellow>

***Change
#<FiniteMachine::TransitionEvent:0x00000001ea98a8
 @from=:yellow,
 @name=:change,
 @to=:red>

***Change
#<FiniteMachine::TransitionEvent:0x00000001f6a800
 @from=:red,
 @name=:change,
 @to=:green>
*** Whoops. Lost power!
#<FiniteMachine::TransitionEvent:0x00000001ec98b0
 @from=:green,
 @name=:power_off,
 @to=nil>

***Change
/var/lib/gems/1.9.1/gems/finite_machine-0.9.0/lib/finite_machine/state_machine.rb:251:in `valid_state?': inappropriate current state 'off' (FiniteMachine::InvalidStateError)

Ruby 2.6.0 support (ruby-2.6.0-preview3)

Does this gem support ruby 2.6.0 version?

Found that model_object.finite_machine.current with ruby "ruby-2.6.0-preview3" returns an array of Proc instead if current state.

Different behaviors:

  • rails 5.2.1, ruby 2.5.0, finite_machine 0.12.0 - works fine:
2.5.0 :005 > Order.first.finite_machine
  Order Load (0.9ms)  SELECT  "orders".* FROM "orders" ORDER BY "orders"."number" DESC LIMIT $1  [["LIMIT", 1]]
 => <#FiniteMachine::StateMachine:0x3ff971d6b294 @states=[:none, :cart, :pending, :prepared, :received, :delivered, :sent, :any, :canceled], @events=[:init, :create, :prepare, :receive, :dispatch, :deliver, :cancel], @transitions=[{:none=>:cart}, {:cart=>:pending}, {:pending=>:prepared}, {:prepared=>:received}, {:delivered=>:received}, {:prepared=>:sent}, {:sent=>:delivered}, {:any=>:canceled}]> 
2.5.0 :006 > Order.first.finite_machine.current
  Order Load (0.7ms)  SELECT  "orders".* FROM "orders" ORDER BY "orders"."number" DESC LIMIT $1  [["LIMIT", 1]]
 => :cart 
  • rails 5.2.1, ruby-2.6.0-preview3, finite_machine 0.12.0 - broken:
2.6.0-preview3 :009 > Order.first.finite_machine
  Order Load (0.8ms)  SELECT  "orders".* FROM "orders" ORDER BY "orders"."number" DESC LIMIT $1  [["LIMIT", 1]]
 => #<Class:0x00007fd1f921aef8> 
2.6.0-preview3 :010 > Order.first.finite_machine.current
  Order Load (0.7ms)  SELECT  "orders".* FROM "orders" ORDER BY "orders"."number" DESC LIMIT $1  [["LIMIT", 1]]
 => [#<Proc:0x00007fd1f92416c0>, #<Proc:0x00007fd1f9240748>, #<Proc:0x00007fd1f924bc38>, #<Proc:0x00007fd1f924ad60>, #<Proc:0x00007fd1f924a428>, #<Proc:0x00007fd1f9249000>, #<Proc:0x00007fd1f9248768>] 

Code example (model and concern with finite machine):

module OrderFiniteMachine
  extend ActiveSupport::Concern

  included do
    before_validation :set_initial_state, on: :create
    after_initialize :restore_state
    after_find :restore_state
  end

  def finite_machine
    context = self

    @state_engine ||= FiniteMachine.define do
      target context

      initial :cart

      events do
        event :create, cart: :pending
        event :prepare, pending: :prepared
        event :receive, prepared: :received
        event :dispatch, prepared: :sent
        event :deliver, sent: :delivered
        event :receive, delivered: :received
        event :cancel, any: :canceled
      end
    end
  end

  private

  def set_initial_state
    self.state = finite_machine.current
  end
end

# == Schema Information
#
# Table name: orders
#
#  id                :integer          not null, primary key
#  state             :string
#  created_at        :datetime         not null
#  updated_at        :datetime         not null
#
class Order < ApplicationRecord
  include OrderFiniteMachine
end

Base class events are not overrided in subclass.

Description of the problem

Hi, I m using the inheritance approach for my problem. Both the base class and subclass share the same event. On triggering the event, I expected the transition in my subclass to get fired. But only the base class transition happens.

How would the new feature work?

Ideally, it would be effective to see the transitions and callbacks in the subclass overrides the base class incase of same event.

Example:

class GenericStateMachine < FiniteMachine::Definition
  initial :red

  event :start, :red => :green

  on_after_start { target.first_callback() }
end


class SpecificStateMachine < GenericStateMachine
  event :start, :red => :yellow

  on_after_start { target.second_callback() }
end

On triggering the event start, the state turns to yellow and second_callback method is invoked

Java::JavaUtilConcurrent::RejectedExecutionException on jruby

Describe the problem

a random exception after the build
https://travis-ci.org/github/piotrmurach/finite_machine/jobs/684379413

Steps to reproduce the problem

test suite

Actual behaviour

Coverage report generated for spec to /home/travis/build/piotrmurach/finite_machine/coverage. 884 / 895 LOC (98.77%) covered.

[Coveralls] Submitting to https://coveralls.io/api/v1
Coveralls encountered an exception:
Java::JavaUtilConcurrent::RejectedExecutionException
Task java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask@4057babc[Not completed, task = java.util.concurrent.Executors$RunnableAdapter@db9ecd[Wrapped task = org.jruby.ext.timeout.Timeout$TimeoutTask@23ac164d]] rejected from java.util.concurrent.ScheduledThreadPoolExecutor@274de4bc[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2055)
java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:825)
java.base/java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:340)
java.base/java.util.concurrent.ScheduledThreadPoolExecutor.schedule(ScheduledThreadPoolExecutor.java:562)
org.jruby.dist/org.jruby.ext.timeout.Timeout.yieldWithTimeout(Timeout.java:143)
org.jruby.dist/org.jruby.ext.timeout.Timeout.timeout(Timeout.java:116)
org.jruby.dist/org.jruby.ext.timeout.Timeout.timeout(Timeout.java:92)
org.jruby.dist/org.jruby.ext.timeout.Timeout$INVOKER$s$timeout.call(Timeout$INVOKER$s$timeout.gen)
...

Expected behaviour

should calculate the coverage

Describe your environment

travis

Potential memory leak in v0.11.x

It appears there's a memory leak in the v0.11 series of the gem. When we bumped to it in our app, we started to experience severe memory issues in production.

I don't have time at the moment to do a deeper dive, but I wanted to make you aware of the issue in case you might have an inkling off the top of your head about where the problem might be. For the time being, we've had to revert back to v0.10.x until we can get to the bottom of it.

Unable to define constructors on custom classes

Describe the problem

I'm trying to extend the FiniteMachine::Definition class and am unable to define my own constructor (#initialize). The #new method is directly overridden in definition.rb and its superclass in state_machine.rb, which prevents developers from creating their own constructors on classes extending either FiniteMachine::Definition or FiniteMachine.

Steps to reproduce the problem

class StateManager < FiniteMachine::Definition
  attr_accessor :state_attr

  def initialize(*args)
    @state_attr = args[0]
  end
end

# trying to initialize the class above by calling `StateManager.new(:some_attr)` will invoke 
# FiniteMachine::Definition's implementation of `#new`, short-circuiting any `initialize` method 
# created on the custom class

Actual behaviour

Custom classes are unable to define their own #initialize method (constructor) on classes extending classes from the FiniteMachine gem.

Expected behaviour

Custom classes should be able to define their own #initialize method (constructor) on classes extending classes from the FiniteMachine gem.

Describe your environment

  • OS version: macOS Big Sur 11.1
  • Ruby version: 3.0.0p0
  • FiniteMachine version: 0.14.0

Incorrect and inconsistent state transitions

According to the docs:

you can create separate events under the same name for each transition that needs combining.

Example finite machine:

class TestMachine < FiniteMachine::Definition
  initial :state_1

  events do
    event :event_1, from: :state_3, to: :state_4

    event :event_1, from: :state_1 do
      choice :state_2, if: -> { false }
      choice :state_3
    end
  end
end

a = TestMachine.new
a.state #=> :state_1
a.event_1 #=> expected transition to :state_3
a.state #=> :state_4

Interestingly enough, switching the order of the defined events fixes the problem:

class TestMachine < FiniteMachine::Definition
  initial :state_1

  events do
    event :event_1, from: :state_1 do
      choice :state_2, if: -> { false }
      choice :state_3
    end

    event :event_1, from: :state_3, to: :state_4
  end
end

a = TestMachine.new
a.state #=> :state_1
a.event_1
a.state #=> :state_3

Terminal state

Hi,

I have a little problem because I would like read terminated state but I can't If:

terminal [:failed, :finished]

  1. I don't see any callback for terminated
state_machine.on_terminated do |event|
...
end
  1. If I have few states of terminal then method terminated? is always false. In my opinion if one of states is in terminal array then should be true. What are you thinking about it?

Thanks
Peter

Repo description url is broken

The repo description url at the top of the page is broken.

It should be: http://piotrmurach.github.io/finite_machine/

πŸ˜„

hard crash when naming event :transition

Apparently :transition is not a valid event name. Rather than crashing the ruby interpreter it would be nice to get an exception.

require 'finite_machine'

fsm = FiniteMachine.define do
  event :transition, :a => :b
end

fsm.transition
$ ruby examples/finite_machine/callbacks.rb 
/Users/kjw/.rvm/gems/ruby-1.9.3-p484@fsmtalk/gems/finite_machine-0.6.1/lib/finite_machine/transition.rb:130: [BUG] Segmentation fault
ruby 1.9.3p484 (2013-11-22 revision 43786) [x86_64-darwin12.5.0]

-- Control frame information -----------------------------------------------
c:2657 p:0018 s:9297 b:7969 l:001468 d:007968 LAMBDA /Users/kjw/.rvm/gems/ruby-1.9.3-p484@fsmtalk/gems/finite_machine-0.6.1/lib/finite_machine/transition.rb:130
c:2656 p:---- s:7965 b:7965 l:007964 d:007964 FINISH

It looks like this code results in infinite recursion.

Transition to initial state should trigger callbacks.

Is the initial pseudo state implicit? I think it would be wise to have a way to initiate the transition into the first state with an explicit call to initial!.

require 'finite_machine'

phone = FiniteMachine.define do
  initial :first

  callbacks {
    on_before { |event| puts "event #{event.name}" }
    on_enter(:first) { |event| puts "transition #{event.name} #{event.from} -> #{event.to}" }
  }
end

phone.start!

This would produce output:

event start!
transition INITIAL -> first

This initial transition could also be done as the last step in define but by having an explicit start! call the user has some control over when the initial transition is triggered.

Does this make sense?

repeatedly sending the same event doesn't work?

I'm playing with finite_machine and I can’t seem to get this very simple example to work. It pushes through a couple of states on a single (repeated) event. Why doesn’t this work?

require 'finite_machine'

bug = FiniteMachine.define do
  initial :initial

  events {
    event :bump,  :initial => :low
    event :bump,  :low     => :medium
    event :bump,  :medium  => :high
  }

  callbacks {
    on_enter_event { |event| puts "#{event.name}\t#{event.from}\t=>\t#{event.to}" }
  }
end

bug.bump
bug.bump
bug.bump

Output is:

$ ruby examples/bug.rb
bump    medium  =>  high
bump    initial =>  high
bump    low =>  high

What am I doing wrong? Or is this a bug?

Defining helper methods on machine, not on target

Great gem! Really nicely designed with the level of customization I was looking for. πŸ˜„

Now to my one little problem...

Problem

I have a lot of custom methods which I use in if conditionals, etc. which do not belong in the target and which in any case ideally I'd like to access directly in transition blocks. In the readme the recommendation is to write code like this:

on_enter_start do |event|
  target.turn_engine_on
end
on_exit_start  do |event|
  target.turn_engine_off
end

But as complexity is added to the machine, I would really prefer to just call turn_engine_on etc. here and define that method on the machine itself. Especially if (as in my case) turn_engine_on is not a method that only relates to the state machine logic and does not belong on the target.

I can do this by subclassing FiniteMachine::StateMachine, which works more or less, but it means that to define the machine itself I need to override initialize and define the block in there, which doesn't seem right. The recommended way is to use FiniteMachine::Definition, but doing that means that you can't define these local methods on the finite machine itself, since internally FiniteMachine::Definition.new hard-codes FiniteMachine here:

def self.new(*args)
context = self
FiniteMachine.new(*args) do
context.deferreds.each { |d| d.call(self) }
end
end

What would be the recommended way to define methods for the machine which are locally accessible in block contexts? I feel this is an important issue when thinking about encapsulating machine-specific logic within the machine itself rather than in the target.

Thanks for reading πŸ˜„ And keep up the great work on the gem.

separate state and event callbacks

In my opinion the on_enter and on_exit callbacks for events are confusing.

States and events are different concepts. By allowing on_enter and on_exit on events a single namespace is created for states and events leading to ambiguous code. I.e. when you have an event :digit and a state :digit what does the callback on_enter_digit mean?

I propose to have separate before and after callbacks for events:

callbacks {
  before_digit  { |event, digit| digits << digit }
  after_on_hook { |event|        disconnect! }
}

What was your rationale for merging callbacks for states and events?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.