finite_machine 0.11.1 → 0.11.2

Sign up to get free protection for your applications and to get access to all the features.
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