Code Monkey home page Code Monkey logo

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

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

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?

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!

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?

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?

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.

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]

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.

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

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

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

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

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

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

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

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.

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?

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.

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.

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)

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.

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.

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

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

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?

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.

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/

😄

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?

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.

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

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?

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?

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

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)

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

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

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

[ 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.

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.

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

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.