finite_machine 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d61855a6c9bcf4a134d260eed0b9926250c165e2
4
- data.tar.gz: 8700fa2dccb31e106fa9fa7769466d4a4b22fc24
3
+ metadata.gz: 75abd57820e49c51d189b0a338477694f8ec7059
4
+ data.tar.gz: 5d983ca01af05a88d06626a2cd9f27207c87ad0d
5
5
  SHA512:
6
- metadata.gz: 199260fd23c214d00e371a803178ee8693fb9f236ca3e68f08a59f181f8d8f251e9a5e340f60deb2b5894358517bf892a8410e6f97e80a717b12ca280c3e50b6
7
- data.tar.gz: 53371bc2d260b5c9e2cf48c03769891540227b967f85039aa878731a9efe6b3be68dffce30a2b45375be409486449d410f3d521eb691fa8b9bc3296982c58402
6
+ metadata.gz: ecd18ac6229dd4293ef77c02f8390f1ad012840680526ff434a72b6dedb107fb85c237cdf028ef0c24574a376041becebeb49b0312ccf1a09a1307aafc74026e
7
+ data.tar.gz: de118ce61f3dce393546528420918b6d042b298f0c54a7d92baa853e54763e685e6deadc143954b6e894d004e4a8458f8ff1f23113f646c3a0bbf6009f52438b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.11.2] - 2015-12-30
4
+
5
+ ### Added
6
+ * Add infering of state or event name based off hook type
7
+
8
+ ### Changed
9
+ * Remove ThreadContext for global queue synchronization
10
+ * Change EventQueue to use Threadable module to sync access
11
+
12
+ ### Fixed
13
+ * Fix bug with two state machines locking up on callback events due to race condition with help from @domokos
14
+
3
15
  ## [v0.11.1] - 2015-12-17
4
16
 
5
17
  ### Fixed
@@ -225,6 +237,7 @@
225
237
  ### Fixed
226
238
  * Fix bug - callback event object returns correct from state
227
239
 
240
+ [v0.11.2]: https://github.com/peter-murach/finite_machine/compare/v0.11.1...v0.11.2
228
241
  [v0.11.1]: https://github.com/peter-murach/finite_machine/compare/v0.11.0...v0.11.1
229
242
  [v0.11.0]: https://github.com/peter-murach/finite_machine/compare/v0.10.2...v0.11.0
230
243
  [v0.10.2]: https://github.com/peter-murach/finite_machine/compare/v0.10.1...v0.10.2
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = [""]
11
11
  spec.description = %q{A minimal finite state machine with a straightforward syntax. You can quickly model states, add callbacks and use object-oriented techniques to integrate with ORMs.}
12
12
  spec.summary = %q{A minimal finite state machine with a straightforward syntax.}
13
- spec.homepage = "https://github.com/peter-murach/finite_machine"
13
+ spec.homepage = "http://peter-murach.github.io/finite_machine/"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
@@ -8,7 +8,6 @@ require "forwardable"
8
8
  require "finite_machine/version"
9
9
  require "finite_machine/threadable"
10
10
  require "finite_machine/safety"
11
- require "finite_machine/thread_context"
12
11
  require "finite_machine/callable"
13
12
  require "finite_machine/catchable"
14
13
  require "finite_machine/choice_merger"
@@ -3,6 +3,8 @@
3
3
  module FiniteMachine
4
4
  # A class responsible for running asynchronous events
5
5
  class EventQueue
6
+ include Threadable
7
+
6
8
  # Initialize an event queue
7
9
  #
8
10
  # @example
@@ -11,7 +13,6 @@ module FiniteMachine
11
13
  # @api public
12
14
  def initialize
13
15
  @queue = Queue.new
14
- @mutex = Mutex.new
15
16
  @dead = false
16
17
  @listeners = []
17
18
 
@@ -26,7 +27,7 @@ module FiniteMachine
26
27
  #
27
28
  # @api private
28
29
  def next_event
29
- @queue.pop
30
+ sync_shared { @queue.pop }
30
31
  end
31
32
 
32
33
  # Add asynchronous event to the event queue
@@ -40,7 +41,7 @@ module FiniteMachine
40
41
  #
41
42
  # @api public
42
43
  def <<(event)
43
- @mutex.synchronize { @queue << event }
44
+ sync_exclusive { @queue << event }
44
45
  self
45
46
  end
46
47
 
@@ -48,9 +49,11 @@ module FiniteMachine
48
49
  #
49
50
  # @api public
50
51
  def subscribe(*args, &block)
51
- listener = Listener.new(*args)
52
- listener.on_delivery(&block)
53
- @listeners << listener
52
+ sync_exclusive do
53
+ listener = Listener.new(*args)
54
+ listener.on_delivery(&block)
55
+ @listeners << listener
56
+ end
54
57
  end
55
58
 
56
59
  # Check if there are any events to handle
@@ -60,7 +63,7 @@ module FiniteMachine
60
63
  #
61
64
  # @api public
62
65
  def empty?
63
- @queue.empty?
66
+ sync_shared { @queue.empty? }
64
67
  end
65
68
 
66
69
  # Check if the event queue is alive
@@ -72,7 +75,7 @@ module FiniteMachine
72
75
  #
73
76
  # @api public
74
77
  def alive?
75
- !@dead
78
+ sync_shared { !@dead }
76
79
  end
77
80
 
78
81
  # Join the event queue from current thread
@@ -85,8 +88,8 @@ module FiniteMachine
85
88
  # @return [nil, Thread]
86
89
  #
87
90
  # @api public
88
- def join(timeout)
89
- @thread.join timeout
91
+ def join(timeout = nil)
92
+ timeout.nil? ? @thread.join : @thread.join(timeout)
90
93
  end
91
94
 
92
95
  # Shut down this event queue and clean it up
@@ -100,15 +103,13 @@ module FiniteMachine
100
103
  def shutdown
101
104
  fail EventQueueDeadError, 'event queue already dead' if @dead
102
105
 
103
- @mutex.lock
104
- begin
106
+ queue = []
107
+ sync_exclusive do
105
108
  queue = @queue
106
109
  @queue.clear
107
110
  @dead = true
108
- ensure
109
- @mutex.unlock rescue nil
110
111
  end
111
- while(!queue.empty?)
112
+ while !queue.empty?
112
113
  Logger.debug "Discarded message: #{queue.pop}"
113
114
  end
114
115
  true
@@ -123,7 +124,7 @@ module FiniteMachine
123
124
  #
124
125
  # @api public
125
126
  def size
126
- @mutex.synchronize { @queue.size }
127
+ sync_shared { @queue.size }
127
128
  end
128
129
 
129
130
  private
@@ -134,7 +135,9 @@ module FiniteMachine
134
135
  #
135
136
  # @api private
136
137
  def notify_listeners(event)
137
- @listeners.each { |listener| listener.handle_delivery(event) }
138
+ sync_shared do
139
+ @listeners.each { |listener| listener.handle_delivery(event) }
140
+ end
138
141
  end
139
142
 
140
143
  # Process all the events
@@ -74,6 +74,18 @@ module FiniteMachine
74
74
  new(state_or_action, event_name, from)
75
75
  end
76
76
 
77
+ # Deduce default name based on even type
78
+ #
79
+ # @param [HookEvent] event_type
80
+ #
81
+ # @return [Symbol]
82
+ # out of :any or :any_event
83
+ #
84
+ # @api public
85
+ def self.infer_default_name(event_type)
86
+ event_type < Anyaction ? ANY_EVENT : ANY_STATE
87
+ end
88
+
77
89
  # Notify subscriber about this event
78
90
  #
79
91
  # @param [Observer] subscriber
@@ -30,6 +30,9 @@ module FiniteMachine
30
30
  # @example
31
31
  # hooks.register HookEvent::Enter, :green do ... end
32
32
  #
33
+ # @example
34
+ # hooks.register HookEvent::Before, :any do ... end
35
+ #
33
36
  # @return [Hash]
34
37
  #
35
38
  # @api public
@@ -30,33 +30,34 @@ module FiniteMachine
30
30
  instance_eval(&block)
31
31
  end
32
32
 
33
- # Register callback for a given event type
33
+ # Register callback for a given hook type
34
34
  #
35
- # @param [Symbol, FiniteMachine::HookEvent] event_type
36
- # @param [Array] args
37
- # @param [Proc] callback
35
+ # @param [HookEvent] hook_type
36
+ # @param [Symbol] state_or_event_name
37
+ # @param [Proc] callback
38
+ #
39
+ # @example
40
+ # observer.on HookEvent::Enter, :green
38
41
  #
39
42
  # @api public
40
- # TODO: throw error if event type isn't handled
41
- def on(event_type = HookEvent, *args, &callback)
43
+ def on(hook_type, state_or_event_name = nil, async = nil, &callback)
42
44
  sync_exclusive do
43
- name, async, _ = args
44
- if name.nil?
45
- name = event_type < HookEvent::Anyaction ? ANY_EVENT : ANY_STATE
45
+ if state_or_event_name.nil?
46
+ state_or_event_name = HookEvent.infer_default_name(hook_type)
46
47
  end
47
48
  async = false if async.nil?
48
- ensure_valid_callback_name!(event_type, name)
49
+ ensure_valid_callback_name!(hook_type, state_or_event_name)
49
50
  callback.extend(Async) if async == :async
50
- hooks.register event_type, name, callback
51
+ hooks.register(hook_type, state_or_event_name, callback)
51
52
  end
52
53
  end
53
54
 
54
55
  # Unregister callback for a given event
55
56
  #
56
57
  # @api public
57
- def off(event_type, name = ANY_STATE, &callback)
58
+ def off(hook_type, name = ANY_STATE, &callback)
58
59
  sync_exclusive do
59
- hooks.unregister event_type, name, callback
60
+ hooks.unregister hook_type, name, callback
60
61
  end
61
62
  end
62
63
 
@@ -116,11 +117,11 @@ module FiniteMachine
116
117
  # @api public
117
118
  def emit(event, *data)
118
119
  sync_exclusive do
119
- [event.type].each do |event_type|
120
+ [event.type].each do |hook_type|
120
121
  [event.name, ANY_STATE, ANY_EVENT].each do |event_name|
121
- hooks.call(event_type, event_name) do |hook|
122
+ hooks.call(hook_type, event_name) do |hook|
122
123
  handle_callback(hook, event, *data)
123
- off(event_type, event_name, &hook) if hook.is_a?(Once)
124
+ off(hook_type, event_name, &hook) if hook.is_a?(Once)
124
125
  end
125
126
  end
126
127
  end
@@ -5,7 +5,6 @@ module FiniteMachine
5
5
  class StateMachine
6
6
  include Threadable
7
7
  include Catchable
8
- include ThreadContext
9
8
  extend Forwardable
10
9
 
11
10
  # Initial state, defaults to :none
@@ -58,6 +57,13 @@ module FiniteMachine
58
57
  # @api private
59
58
  attr_threadsafe :subscribers
60
59
 
60
+ # The queue for asynchronoous events
61
+ #
62
+ # @return [EventQueue]
63
+ #
64
+ # @api private
65
+ attr_threadsafe :event_queue
66
+
61
67
  # Allow or not logging of transitions
62
68
  attr_threadsafe :log_transitions
63
69
 
@@ -71,8 +77,8 @@ module FiniteMachine
71
77
  # @api private
72
78
  def initialize(*args, &block)
73
79
  attributes = args.last.is_a?(Hash) ? args.pop : {}
74
- self.event_queue = EventQueue.new
75
80
 
81
+ @event_queue = EventQueue.new
76
82
  @initial_state = DEFAULT_STATE
77
83
  @async_proxy = AsyncProxy.new(self)
78
84
  @subscribers = Subscribers.new
@@ -4,7 +4,6 @@ module FiniteMachine
4
4
  # A mixin to allow instance methods to be synchronized
5
5
  module Threadable
6
6
  module InstanceMethods
7
-
8
7
  # Exclusive lock
9
8
  #
10
9
  # @return [nil]
@@ -11,13 +11,13 @@ module FiniteMachine
11
11
  #
12
12
  # @api private
13
13
  def sync
14
- @sync ||= Sync.new
14
+ @sync = Sync.new
15
15
  end
16
16
 
17
17
  # Synchronize given block of code
18
18
  #
19
19
  # @param [Symbol] mode
20
- # the synchronization mode out of :SH and :EX
20
+ # the lock mode out of :SH, :EX, :UN
21
21
  #
22
22
  # @return [nil]
23
23
  #
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module FiniteMachine
4
- VERSION = "0.11.1"
4
+ VERSION = "0.11.2"
5
5
  end
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe FiniteMachine, 'system' do
4
+
5
+ it "doesn't share state between machine callbacks" do
6
+ callbacks = []
7
+ stub_const("FSM_A", Class.new(FiniteMachine::Definition) do
8
+ events {
9
+ event :init, :none => :green
10
+ event :green, :any => :green
11
+ }
12
+ callbacks {
13
+ on_before do |event|
14
+ callbacks << "fsmA on_before(#{event.to})"
15
+ end
16
+ on_enter_green do |event|
17
+ target.fire
18
+ callbacks << "fsmA on_enter_green"
19
+ end
20
+ once_on_enter_green do |event|
21
+ callbacks << "fsmA once_on_enter_green"
22
+ end
23
+ }
24
+ end)
25
+
26
+ stub_const("FSM_B", Class.new(FiniteMachine::Definition) do
27
+ events {
28
+ event :init, :none => :stopped
29
+ event :start, :stopped => :started
30
+ }
31
+ callbacks {
32
+ on_before do |event|
33
+ callbacks << "fsmB on_before(#{event.to})"
34
+ end
35
+ on_enter_started do |event|
36
+ callbacks << "fsmB on_enter_started"
37
+ end
38
+ }
39
+ end)
40
+
41
+ class Backend
42
+ def initialize
43
+ @fsmB = FSM_B.new
44
+ @fsmB.init
45
+ @signal = Mutex.new
46
+ end
47
+
48
+ def operate
49
+ @signal.unlock if @signal.locked?
50
+ @worker = Thread.new do
51
+ while !@signal.locked? do
52
+ sleep 0.01
53
+ end
54
+ Thread.current.abort_on_exception = true
55
+ @fsmB.start
56
+ end
57
+ end
58
+
59
+ def stopit
60
+ @signal.lock
61
+ @worker.join
62
+ end
63
+ end
64
+
65
+ class Fire
66
+ def initialize
67
+ @fsmA = FSM_A.new
68
+ @fsmA.target(self)
69
+
70
+ @backend = Backend.new
71
+ @backend.operate
72
+ end
73
+
74
+ def fire
75
+ @backend.stopit
76
+ end
77
+
78
+ def operate
79
+ @fsmA.green
80
+ end
81
+ end
82
+
83
+ fire = Fire.new
84
+ fire.operate
85
+
86
+ expect(callbacks).to match_array([
87
+ 'fsmA on_before(green)',
88
+ 'fsmA on_enter_green',
89
+ 'fsmA once_on_enter_green',
90
+ 'fsmB on_before(stopped)',
91
+ 'fsmB on_before(started)',
92
+ 'fsmB on_enter_started'
93
+ ])
94
+ end
95
+ end
@@ -56,13 +56,13 @@ RSpec.describe FiniteMachine, 'async_events' do
56
56
  end
57
57
  expect(fsm.current).to eql(:none)
58
58
  fsm.async.go(:foo)
59
- fsm.event_queue.join 0.01
59
+ fsm.event_queue.join 0.02
60
60
  expect(fsm.current).to eql(:green)
61
61
  expect(called).to eql(["cond_none_green(#{fsm},foo)"])
62
62
 
63
63
  expect(fsm.current).to eql(:green)
64
64
  fsm.async.stop(:bar)
65
- fsm.event_queue.join 0.01
65
+ fsm.event_queue.join 0.02
66
66
  expect(fsm.current).to eql(:red)
67
67
  expect(called).to match_array([
68
68
  "cond_none_green(#{fsm},foo)",
@@ -97,7 +97,8 @@ RSpec.describe FiniteMachine, 'async_events' do
97
97
  fsmBar.async.slow(:bar)
98
98
  }
99
99
  ThreadsWait.all_waits(foo_thread, bar_thread)
100
- sleep 0.01
100
+ fsmFoo.event_queue.join(0.01)
101
+ fsmBar.event_queue.join(0.01)
101
102
  expect(called).to match_array([
102
103
  '(foo)on_enter_yellow_foo',
103
104
  '(bar)on_enter_yellow_bar'
@@ -52,6 +52,4 @@ RSpec.describe FiniteMachine, 'define' do
52
52
  expect(called).to match_array(['on_enter_yellow', 'error_handler'])
53
53
  end
54
54
  end
55
-
56
- xit "creates multiple machines"
57
55
  end
@@ -13,7 +13,7 @@ RSpec.describe FiniteMachine::EventQueue do
13
13
  expect(event_queue.size).to be_zero
14
14
  event_queue << event1
15
15
  event_queue << event2
16
- sleep 0.001
16
+ event_queue.join(0.001)
17
17
  expect(called).to match_array(['event1_dispatched', 'event2_dispatched'])
18
18
  end
19
19
 
@@ -21,7 +21,7 @@ RSpec.describe FiniteMachine::EventQueue do
21
21
  event = double(:event)
22
22
  expect(FiniteMachine::Logger).to receive(:error)
23
23
  event_queue << event
24
- sleep 0.01
24
+ event_queue.join(0.01)
25
25
  expect(event_queue).to be_empty
26
26
  end
27
27
 
@@ -32,7 +32,8 @@ RSpec.describe FiniteMachine::EventQueue do
32
32
  event3 = double(:event3, dispatch: true)
33
33
  event_queue.subscribe(:listener1) { |event| called << event }
34
34
  event_queue << event1 << event2 << event3
35
- sleep 0.01
35
+ event_queue.join(0.02)
36
+ event_queue.shutdown
36
37
  expect(called).to match_array([event1, event2, event3])
37
38
  end
38
39
 
@@ -45,7 +46,7 @@ RSpec.describe FiniteMachine::EventQueue do
45
46
  event_queue << event2
46
47
  event_queue.shutdown
47
48
  event_queue << event3
48
- sleep 0.001
49
+ event_queue.join(0.001)
49
50
  expect(event_queue.alive?).to be(false)
50
51
  end
51
52
  end
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ RSpec.describe FiniteMachine::HookEvent, '#infer_default_name' do
4
+ it "infers default name for state" do
5
+ hook_event = described_class::Enter
6
+ expect(described_class.infer_default_name(hook_event)).to eq(:any)
7
+ end
8
+
9
+ it "infers default name for event" do
10
+ hook_event = described_class::Before
11
+ expect(described_class.infer_default_name(hook_event)).to eq(:any_event)
12
+ end
13
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: finite_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-17 00:00:00.000000000 Z
11
+ date: 2015-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -83,7 +83,6 @@ files:
83
83
  - lib/finite_machine/state_machine.rb
84
84
  - lib/finite_machine/state_parser.rb
85
85
  - lib/finite_machine/subscribers.rb
86
- - lib/finite_machine/thread_context.rb
87
86
  - lib/finite_machine/threadable.rb
88
87
  - lib/finite_machine/transition.rb
89
88
  - lib/finite_machine/transition_builder.rb
@@ -91,6 +90,7 @@ files:
91
90
  - lib/finite_machine/two_phase_lock.rb
92
91
  - lib/finite_machine/undefined_transition.rb
93
92
  - lib/finite_machine/version.rb
93
+ - spec/integration/system_spec.rb
94
94
  - spec/spec_helper.rb
95
95
  - spec/unit/alias_target_spec.rb
96
96
  - spec/unit/async_events_spec.rb
@@ -115,6 +115,7 @@ files:
115
115
  - spec/unit/handlers_spec.rb
116
116
  - spec/unit/hook_event/build_spec.rb
117
117
  - spec/unit/hook_event/eql_spec.rb
118
+ - spec/unit/hook_event/infer_default_name_spec.rb
118
119
  - spec/unit/hook_event/initialize_spec.rb
119
120
  - spec/unit/hook_event/notify_spec.rb
120
121
  - spec/unit/hooks/call_spec.rb
@@ -144,7 +145,7 @@ files:
144
145
  - tasks/console.rake
145
146
  - tasks/coverage.rake
146
147
  - tasks/spec.rake
147
- homepage: https://github.com/peter-murach/finite_machine
148
+ homepage: http://peter-murach.github.io/finite_machine/
148
149
  licenses:
149
150
  - MIT
150
151
  metadata: {}
@@ -169,6 +170,7 @@ signing_key:
169
170
  specification_version: 4
170
171
  summary: A minimal finite state machine with a straightforward syntax.
171
172
  test_files:
173
+ - spec/integration/system_spec.rb
172
174
  - spec/spec_helper.rb
173
175
  - spec/unit/alias_target_spec.rb
174
176
  - spec/unit/async_events_spec.rb
@@ -193,6 +195,7 @@ test_files:
193
195
  - spec/unit/handlers_spec.rb
194
196
  - spec/unit/hook_event/build_spec.rb
195
197
  - spec/unit/hook_event/eql_spec.rb
198
+ - spec/unit/hook_event/infer_default_name_spec.rb
196
199
  - spec/unit/hook_event/initialize_spec.rb
197
200
  - spec/unit/hook_event/notify_spec.rb
198
201
  - spec/unit/hooks/call_spec.rb
@@ -1,17 +0,0 @@
1
- # encoding: utf-8
2
-
3
- module FiniteMachine
4
- # A mixin to allow sharing of thread context
5
- module ThreadContext
6
-
7
- # @api public
8
- def event_queue
9
- Thread.current[:finite_machine_event_queue]
10
- end
11
-
12
- # @api public
13
- def event_queue=(value)
14
- Thread.current[:finite_machine_event_queue] = value
15
- end
16
- end # ThreadContext
17
- end # FiniteMachine