state_machines 0.30.0 → 0.40.0

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
  SHA256:
3
- metadata.gz: 5c732f34387da18f4dc1c3d78cad1fbb111cc88d06d9fe3edcda912adc5e655a
4
- data.tar.gz: ef0079a89a1b0e4ddea45dd4a79e11de12740d4eb0c2aa98ad95b5ca7ae3eab8
3
+ metadata.gz: bc7dae3a88c68f1327da47debac2546fde7c4459346dbfefaabbc4bed5dc5554
4
+ data.tar.gz: 2fbfc7157580635bd546b9096138677d44b58a92578d6341667a1722c91ccb66
5
5
  SHA512:
6
- metadata.gz: d71a5bd20b0de0b19e4684b4251954d0cf648e5454b57af96555372bbdee59a9ac855bb27d182e018f87ffa0525b5cd3daff5c3ce53c483f3dec4954d076a331
7
- data.tar.gz: 179e0cb6c2a31d14c4078820d254ce35e26d9b5aaa2646e4766b842127411cf49e5ad697d0a764ba79a0036bce5dc288ef113b8bef57c3cac64de8816a23f49b
6
+ metadata.gz: 4b5ba6ecdeb4dd612c6865657e0a0b590d3fa67fed522359c7b388ab88401283d953d857fc73fab2fbeb97388c5bb71b5f4e125d70366a03547aea91b991ab35
7
+ data.tar.gz: 07c96c5556ae74d933de3cf1b132cfda9144404361cca87c24c14694e2158e5f0437d65a4ab08b857dca8ad0c453d7fdc674abeb770c528d7ec45e4b060c8ddd
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ![Build Status](https://github.com/state-machines/state_machines/actions/workflows/ruby.yml/badge.svg)
2
+
2
3
  # State Machines
3
4
 
4
5
  State Machines adds support for creating state machines for attributes on any Ruby class.
@@ -9,15 +10,21 @@ State Machines adds support for creating state machines for attributes on any Ru
9
10
 
10
11
  Add this line to your application's Gemfile:
11
12
 
12
- gem 'state_machines'
13
+ ```ruby
14
+ gem 'state_machines'
15
+ ```
13
16
 
14
17
  And then execute:
15
18
 
16
- $ bundle
19
+ ```sh
20
+ bundle
21
+ ```
17
22
 
18
23
  Or install it yourself as:
19
24
 
20
- $ gem install state_machines
25
+ ```sh
26
+ gem install state_machines
27
+ ```
21
28
 
22
29
  ## Usage
23
30
 
@@ -38,7 +45,7 @@ Class definition:
38
45
 
39
46
  ```ruby
40
47
  class Vehicle
41
- attr_accessor :seatbelt_on, :time_used, :auto_shop_busy
48
+ attr_accessor :seatbelt_on, :time_used, :auto_shop_busy, :parking_meter_number
42
49
 
43
50
  state_machine :state, initial: :parked do
44
51
  before_transition parked: any - :parked, do: :put_on_seatbelt
@@ -61,6 +68,18 @@ class Vehicle
61
68
  transition [:idling, :first_gear] => :parked
62
69
  end
63
70
 
71
+ before_transition on: :park do |vehicle, transition|
72
+ # If using Rails:
73
+ # options = transition.args.extract_options!
74
+
75
+ options = transition.args.last.is_a?(Hash) ? transition.args.pop : {}
76
+ meter_number = options[:meter_number]
77
+
78
+ unless meter_number.nil?
79
+ vehicle.parking_meter_number = meter_number
80
+ end
81
+ end
82
+
64
83
  event :ignite do
65
84
  transition stalled: same, parked: :idling
66
85
  end
@@ -130,6 +149,7 @@ class Vehicle
130
149
  @seatbelt_on = false
131
150
  @time_used = 0
132
151
  @auto_shop_busy = true
152
+ @parking_meter_number = nil
133
153
  super() # NOTE: This *must* be called, otherwise states won't get initialized
134
154
  end
135
155
 
@@ -200,6 +220,11 @@ vehicle.park! # => StateMachines:InvalidTransition: Cannot tra
200
220
  vehicle.state?(:parked) # => false
201
221
  vehicle.state?(:invalid) # => IndexError: :invalid is an invalid name
202
222
 
223
+ # Transition callbacks can receive arguments
224
+ vehicle.park(meter_number: '12345') # => true
225
+ vehicle.parked? # => true
226
+ vehicle.parking_meter_number # => "12345"
227
+
203
228
  # Namespaced machines have uniquely-generated methods
204
229
  vehicle.alarm_state # => 1
205
230
  vehicle.alarm_state_name # => :active
@@ -790,7 +815,7 @@ For RSpec testing, use the custom RSpec matchers:
790
815
 
791
816
  ## Contributing
792
817
 
793
- 1. Fork it ( https://github.com/state-machines/state_machines/fork )
818
+ 1. Fork it ( <https://github.com/state-machines/state_machines/fork> )
794
819
  2. Create your feature branch (`git checkout -b my-new-feature`)
795
820
  3. Commit your changes (`git commit -am 'Add some feature'`)
796
821
  4. Push to the branch (`git push origin my-new-feature`)
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module AsyncMode
5
+ # Extensions to Event class for async bang methods
6
+ module AsyncEventExtensions
7
+ # Generate async bang methods for events when async mode is enabled
8
+ def define_helper(scope, method, *args, &block)
9
+ result = super
10
+
11
+ # If this is an async-enabled machine and we're defining an event method
12
+ if scope == :instance && method !~ /_async[!]?$/ && machine.async_mode_enabled?
13
+ qualified_name = method.to_s
14
+
15
+ # Create async version that returns a task
16
+ machine.define_helper(scope, "#{qualified_name}_async") do |machine, object, *method_args, **kwargs|
17
+ # Find the machine that has this event
18
+ target_machine = object.class.state_machines.values.find { |m| m.events[name] }
19
+
20
+ unless defined?(::Async::Task) && ::Async::Task.current?
21
+ raise RuntimeError, "#{qualified_name}_async must be called within an Async context"
22
+ end
23
+
24
+ Async do
25
+ target_machine.events[name].fire(object, *method_args, **kwargs)
26
+ end
27
+ end
28
+
29
+ # Create async bang version that raises exceptions when awaited
30
+ machine.define_helper(scope, "#{qualified_name}_async!") do |machine, object, *method_args, **kwargs|
31
+ # Find the machine that has this event
32
+ target_machine = object.class.state_machines.values.find { |m| m.events[name] }
33
+
34
+ unless defined?(::Async::Task) && ::Async::Task.current?
35
+ raise RuntimeError, "#{qualified_name}_async! must be called within an Async context"
36
+ end
37
+
38
+ Async do
39
+ # Use fire method which will raise exceptions on invalid transitions
40
+ target_machine.events[name].fire(object, *method_args, **kwargs) || raise(StateMachines::InvalidTransition.new(object, target_machine, name))
41
+ end
42
+ end
43
+ end
44
+
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module AsyncMode
5
+ # Async-aware event firing capabilities using the async gem
6
+ module AsyncEvents
7
+ # Fires an event asynchronously using Async
8
+ # Returns an Async::Task that can be awaited for the result
9
+ #
10
+ # Example:
11
+ # Async do
12
+ # task = vehicle.async_fire_event(:ignite)
13
+ # result = task.wait # => true/false
14
+ # end
15
+ def async_fire_event(event_name, *args)
16
+ # Find the machine that has this event
17
+ machine = self.class.state_machines.values.find { |m| m.events[event_name] }
18
+
19
+ unless machine
20
+ raise ArgumentError, "Event #{event_name} not found in any state machine"
21
+ end
22
+
23
+ # Must be called within an Async context
24
+ unless defined?(::Async::Task) && ::Async::Task.current?
25
+ raise RuntimeError, "async_fire_event must be called within an Async context. Use: Async { vehicle.async_fire_event(:event) }"
26
+ end
27
+
28
+ Async do
29
+ machine.events[event_name].fire(self, *args)
30
+ end
31
+ end
32
+
33
+ # Fires multiple events asynchronously across different state machines
34
+ # Returns an array of Async::Tasks for concurrent execution
35
+ #
36
+ # Example:
37
+ # Async do
38
+ # tasks = vehicle.async_fire_events(:ignite, :buy_insurance)
39
+ # results = tasks.map(&:wait) # => [true, true]
40
+ # end
41
+ def async_fire_events(*event_names)
42
+ event_names.map { |event_name| async_fire_event(event_name) }
43
+ end
44
+
45
+ # Fires an event asynchronously and waits for completion
46
+ # This is a convenience method that creates and waits for the task
47
+ #
48
+ # Example:
49
+ # result = vehicle.fire_event_async(:ignite) # => true/false
50
+ def fire_event_async(event_name, *args)
51
+ raise NoMethodError, "undefined method `fire_event_async' for #{self}" unless has_async_machines?
52
+ # Find the machine that has this event
53
+ machine = self.class.state_machines.values.find { |m| m.events[event_name] }
54
+
55
+ unless machine
56
+ raise ArgumentError, "Event #{event_name} not found in any state machine"
57
+ end
58
+
59
+ if defined?(::Async::Task) && ::Async::Task.current?
60
+ # Already in async context, just fire directly
61
+ machine.events[event_name].fire(self, *args)
62
+ else
63
+ # Create async context and wait for result
64
+ Async do
65
+ machine.events[event_name].fire(self, *args)
66
+ end.wait
67
+ end
68
+ end
69
+
70
+ # Fires multiple events asynchronously and waits for all completions
71
+ # Returns results in the same order as the input events
72
+ #
73
+ # Example:
74
+ # results = vehicle.fire_events_async(:ignite, :buy_insurance) # => [true, true]
75
+ def fire_events_async(*event_names)
76
+ raise NoMethodError, "undefined method `fire_events_async' for #{self}" unless has_async_machines?
77
+ if defined?(::Async::Task) && ::Async::Task.current?
78
+ # Already in async context, run concurrently
79
+ tasks = event_names.map { |event_name| async_fire_event(event_name) }
80
+ tasks.map(&:wait)
81
+ else
82
+ # Create async context and run concurrently
83
+ Async do
84
+ tasks = event_names.map { |event_name| async_fire_event(event_name) }
85
+ tasks.map(&:wait)
86
+ end.wait
87
+ end
88
+ end
89
+
90
+ # Fires an event asynchronously using Async and raises exception on failure
91
+ # Returns an Async::Task that raises StateMachines::InvalidTransition when awaited
92
+ #
93
+ # Example:
94
+ # Async do
95
+ # begin
96
+ # task = vehicle.async_fire_event!(:ignite)
97
+ # result = task.wait
98
+ # puts "Event fired successfully!"
99
+ # rescue StateMachines::InvalidTransition => e
100
+ # puts "Transition failed: #{e.message}"
101
+ # end
102
+ # end
103
+ def async_fire_event!(event_name, *args)
104
+ # Find the machine that has this event
105
+ machine = self.class.state_machines.values.find { |m| m.events[event_name] }
106
+
107
+ unless machine
108
+ raise ArgumentError, "Event #{event_name} not found in any state machine"
109
+ end
110
+
111
+ # Must be called within an Async context
112
+ unless defined?(::Async::Task) && ::Async::Task.current?
113
+ raise RuntimeError, "async_fire_event! must be called within an Async context. Use: Async { vehicle.async_fire_event!(:event) }"
114
+ end
115
+
116
+ Async do
117
+ # Use the bang version which raises exceptions on failure
118
+ machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
119
+ end
120
+ end
121
+
122
+ # Fires an event asynchronously and waits for result, raising exceptions on failure
123
+ # This is a convenience method that creates and waits for the task
124
+ #
125
+ # Example:
126
+ # begin
127
+ # result = vehicle.fire_event_async!(:ignite)
128
+ # puts "Event fired successfully!"
129
+ # rescue StateMachines::InvalidTransition => e
130
+ # puts "Transition failed: #{e.message}"
131
+ # end
132
+ def fire_event_async!(event_name, *args)
133
+ raise NoMethodError, "undefined method `fire_event_async!' for #{self}" unless has_async_machines?
134
+ # Find the machine that has this event
135
+ machine = self.class.state_machines.values.find { |m| m.events[event_name] }
136
+
137
+ unless machine
138
+ raise ArgumentError, "Event #{event_name} not found in any state machine"
139
+ end
140
+
141
+ if defined?(::Async::Task) && ::Async::Task.current?
142
+ # Already in async context, just fire directly with bang behavior
143
+ machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
144
+ else
145
+ # Create async context and wait for result (may raise exception)
146
+ Async do
147
+ machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
148
+ end.wait
149
+ end
150
+ end
151
+
152
+ # Dynamically handle individual event async methods
153
+ # This provides launch_async, launch_async!, arm_weapons_async, etc.
154
+ def method_missing(method_name, *args, **kwargs, &block)
155
+ method_str = method_name.to_s
156
+
157
+ # Check if this is an async event method
158
+ if method_str.end_with?('_async!')
159
+ # Remove the _async! suffix to get the base event method
160
+ base_method = method_str.chomp('_async!').to_sym
161
+
162
+ # Check if the base method exists and this machine is async-enabled
163
+ if respond_to?(base_method) && async_method_for_event?(base_method)
164
+ return handle_individual_event_async_bang(base_method, *args, **kwargs)
165
+ end
166
+ elsif method_str.end_with?('_async')
167
+ # Remove the _async suffix to get the base event method
168
+ base_method = method_str.chomp('_async').to_sym
169
+
170
+ # Check if the base method exists and this machine is async-enabled
171
+ if respond_to?(base_method) && async_method_for_event?(base_method)
172
+ return handle_individual_event_async(base_method, *args, **kwargs)
173
+ end
174
+ end
175
+
176
+ # If not an async method, call the original method_missing
177
+ super
178
+ end
179
+
180
+ # Check if we should respond to async methods for this event
181
+ def respond_to_missing?(method_name, include_private = false)
182
+ # Only provide async methods if this object has async-enabled machines
183
+ return super unless has_async_machines?
184
+
185
+ method_str = method_name.to_s
186
+
187
+ if method_str.end_with?('_async!') || method_str.end_with?('_async')
188
+ base_method = method_str.chomp('_async!').chomp('_async').to_sym
189
+ return respond_to?(base_method) && async_method_for_event?(base_method)
190
+ end
191
+
192
+ super
193
+ end
194
+
195
+ # Check if this object has any async-enabled state machines
196
+ def has_async_machines?
197
+ self.class.state_machines.any? { |name, machine| machine.async_mode_enabled? }
198
+ end
199
+
200
+ private
201
+
202
+ # Check if this event method should have async versions
203
+ def async_method_for_event?(event_method)
204
+ # Find which machine contains this event
205
+ self.class.state_machines.each do |name, machine|
206
+ if machine.async_mode_enabled?
207
+ # Check if this event method belongs to this machine
208
+ machine.events.each do |event|
209
+ qualified_name = event.qualified_name
210
+ if qualified_name.to_sym == event_method || "#{qualified_name}!".to_sym == event_method
211
+ return true
212
+ end
213
+ end
214
+ end
215
+ end
216
+ false
217
+ end
218
+
219
+ # Handle individual event async methods (returns task)
220
+ def handle_individual_event_async(event_method, *args, **kwargs)
221
+
222
+ unless defined?(::Async::Task) && ::Async::Task.current?
223
+ raise RuntimeError, "#{event_method}_async must be called within an Async context"
224
+ end
225
+
226
+ Async do
227
+ send(event_method, *args, **kwargs)
228
+ end
229
+ end
230
+
231
+ # Handle individual event async bang methods (returns task, raises on failure)
232
+ def handle_individual_event_async_bang(event_method, *args, **kwargs)
233
+ # Extract event name from method and use bang version
234
+ bang_method = "#{event_method}!".to_sym
235
+
236
+ unless defined?(::Async::Task) && ::Async::Task.current?
237
+ raise RuntimeError, "#{event_method}_async! must be called within an Async context"
238
+ end
239
+
240
+ Async do
241
+ send(bang_method, *args, **kwargs)
242
+ end
243
+ end
244
+
245
+ # Extract event name from method name, handling namespaced events
246
+ def extract_event_name(method_name)
247
+ method_str = method_name.to_s
248
+
249
+ # Find the machine and event for this method
250
+ self.class.state_machines.each do |name, machine|
251
+ machine.events.each do |event|
252
+ qualified_name = event.qualified_name
253
+ if qualified_name.to_s == method_str || "#{qualified_name}!".to_s == method_str
254
+ return event.name
255
+ end
256
+ end
257
+ end
258
+
259
+ # Fallback: assume the method name is the event name
260
+ method_str.chomp('!').to_sym
261
+ end
262
+
263
+ public
264
+
265
+ # Fires multiple events concurrently within an async context
266
+ # This method should be called from within an Async block
267
+ #
268
+ # Example:
269
+ # Async do
270
+ # results = vehicle.fire_events_concurrent(:ignite, :buy_insurance)
271
+ # end
272
+ def fire_events_concurrent(*event_names)
273
+ unless defined?(::Async::Task) && ::Async::Task.current?
274
+ raise RuntimeError, "fire_events_concurrent must be called within an Async context"
275
+ end
276
+
277
+ tasks = async_fire_events(*event_names)
278
+ tasks.map(&:wait)
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module AsyncMode
5
+ # Enhanced machine class with async capabilities
6
+ module AsyncMachine
7
+ # Thread-safe state reading for machines
8
+ def read_safely(object, attribute, ivar = false)
9
+ if object.respond_to?(:read_state_safely)
10
+ object.read_state_safely(self, attribute, ivar)
11
+ else
12
+ read(object, attribute, ivar)
13
+ end
14
+ end
15
+
16
+ # Thread-safe state writing for machines
17
+ def write_safely(object, attribute, value, ivar = false)
18
+ if object.respond_to?(:write_state_safely)
19
+ object.write_state_safely(self, attribute, value, ivar)
20
+ else
21
+ write(object, attribute, value, ivar)
22
+ end
23
+ end
24
+
25
+ # Fires an event asynchronously on the given object
26
+ # Returns an Async::Task for concurrent execution
27
+ def async_fire_event(object, event_name, *args)
28
+ unless defined?(::Async::Task) && ::Async::Task.current?
29
+ raise RuntimeError, "async_fire_event must be called within an Async context"
30
+ end
31
+
32
+ Async do
33
+ events[event_name].fire(object, *args)
34
+ end
35
+ end
36
+
37
+ # Creates an async-aware transition collection
38
+ # Supports concurrent transition execution with proper synchronization
39
+ def create_async_transition_collection(transitions, options = {})
40
+ if defined?(AsyncTransitionCollection)
41
+ AsyncTransitionCollection.new(transitions, options)
42
+ else
43
+ # Fallback to regular collection if async collection isn't available
44
+ TransitionCollection.new(transitions, options)
45
+ end
46
+ end
47
+
48
+ # Thread-safe callback execution for async operations
49
+ def run_callbacks_safely(type, object, context, transition)
50
+ if object.respond_to?(:state_machine_mutex)
51
+ object.state_machine_mutex.with_read_lock do
52
+ callbacks[type].each { |callback| callback.call(object, context, transition) }
53
+ end
54
+ else
55
+ callbacks[type].each { |callback| callback.call(object, context, transition) }
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module AsyncMode
5
+ # Error class for async-specific transition failures
6
+ class AsyncTransitionError < StateMachines::Error
7
+ def initialize(object, machines, failed_events)
8
+ @object = object
9
+ @machines = machines
10
+ @failed_events = failed_events
11
+ super("Failed to perform async transitions: #{failed_events.join(', ')}")
12
+ end
13
+
14
+ attr_reader :object, :machines, :failed_events
15
+ end
16
+
17
+ # Async-aware transition collection that can execute transitions concurrently
18
+ class AsyncTransitionCollection < TransitionCollection
19
+ # Performs transitions asynchronously using Async
20
+ # Provides better concurrency for I/O-bound operations
21
+ def perform_async(&block)
22
+ reset
23
+
24
+ unless defined?(::Async::Task) && ::Async::Task.current?
25
+ return Async do
26
+ perform_async(&block)
27
+ end.wait
28
+ end
29
+
30
+ if valid?
31
+ # Create async tasks for each transition
32
+ tasks = map do |transition|
33
+ Async do
34
+ if use_event_attributes? && !block_given?
35
+ transition.transient = true
36
+ transition.machine.write_safely(object, :event_transition, transition)
37
+ run_actions
38
+ transition
39
+ else
40
+ within_transaction do
41
+ catch(:halt) { run_callbacks(&block) }
42
+ rollback unless success?
43
+ end
44
+ transition
45
+ end
46
+ end
47
+ end
48
+
49
+ # Wait for all tasks to complete
50
+ completed_transitions = []
51
+ tasks.each do |task|
52
+ begin
53
+ result = task.wait
54
+ completed_transitions << result if result
55
+ rescue StandardError => e
56
+ # Handle individual transition failures
57
+ rollback
58
+ raise AsyncTransitionError.new(object, map(&:machine), [e.message])
59
+ end
60
+ end
61
+
62
+ # Check if all transitions succeeded
63
+ @success = completed_transitions.length == length
64
+ end
65
+
66
+ success?
67
+ end
68
+
69
+ # Performs transitions concurrently using threads
70
+ # Better for CPU-bound operations but requires more careful synchronization
71
+ def perform_threaded(&block)
72
+ reset
73
+
74
+ if valid?
75
+ # Use basic thread approach
76
+ threads = []
77
+ results = []
78
+ results_mutex = Concurrent::ReentrantReadWriteLock.new
79
+
80
+ each do |transition|
81
+ threads << Thread.new do
82
+ begin
83
+ result = if use_event_attributes? && !block_given?
84
+ transition.transient = true
85
+ transition.machine.write_safely(object, :event_transition, transition)
86
+ run_actions
87
+ transition
88
+ else
89
+ within_transaction do
90
+ catch(:halt) { run_callbacks(&block) }
91
+ rollback unless success?
92
+ end
93
+ transition
94
+ end
95
+
96
+ results_mutex.with_write_lock { results << result }
97
+ rescue StandardError => e
98
+ # Handle individual transition failures
99
+ rollback
100
+ raise AsyncTransitionError.new(object, [transition.machine], [e.message])
101
+ end
102
+ end
103
+ end
104
+
105
+ # Wait for all threads to complete
106
+ threads.each(&:join)
107
+ @success = results.length == length
108
+ end
109
+
110
+ success?
111
+ end
112
+
113
+ private
114
+
115
+ # Override run_actions to be thread-safe when needed
116
+ def run_actions(&block)
117
+ catch_exceptions do
118
+ @success = if block_given?
119
+ result = yield
120
+ actions.each { |action| results[action] = result }
121
+ !!result
122
+ else
123
+ actions.compact.each do |action|
124
+ next if skip_actions
125
+
126
+ # Use thread-safe write for results
127
+ if object.respond_to?(:state_machine_mutex)
128
+ object.state_machine_mutex.with_write_lock do
129
+ results[action] = object.send(action)
130
+ end
131
+ else
132
+ results[action] = object.send(action)
133
+ end
134
+ end
135
+ results.values.all?
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ module AsyncMode
5
+ # Thread-safe state operations for async-enabled state machines
6
+ # Uses concurrent-ruby for enterprise-grade thread safety
7
+ module ThreadSafeState
8
+ # Gets or creates a reentrant mutex for thread-safe state operations on an object
9
+ # Each object gets its own mutex to avoid global locking
10
+ # Uses Concurrent::ReentrantReadWriteLock for better performance
11
+ def state_machine_mutex
12
+ @_state_machine_mutex ||= Concurrent::ReentrantReadWriteLock.new
13
+ end
14
+
15
+ # Thread-safe version of state reading
16
+ # Ensures atomic read operations across concurrent threads
17
+ def read_state_safely(machine, attribute, ivar = false)
18
+ state_machine_mutex.with_read_lock do
19
+ machine.read(self, attribute, ivar)
20
+ end
21
+ end
22
+
23
+ # Thread-safe version of state writing
24
+ # Ensures atomic write operations across concurrent threads
25
+ def write_state_safely(machine, attribute, value, ivar = false)
26
+ state_machine_mutex.with_write_lock do
27
+ machine.write(self, attribute, value, ivar)
28
+ end
29
+ end
30
+
31
+ # Handle marshalling by excluding the mutex (will be recreated when needed)
32
+ def marshal_dump
33
+ # Get instance variables excluding the mutex
34
+ vars = instance_variables.reject { |var| var == :@_state_machine_mutex }
35
+ vars.map { |var| [var, instance_variable_get(var)] }
36
+ end
37
+
38
+ # Restore marshalled object, mutex will be lazily recreated when needed
39
+ def marshal_load(data)
40
+ data.each do |var, value|
41
+ instance_variable_set(var, value)
42
+ end
43
+ # Don't set @_state_machine_mutex - let it be lazily created
44
+ end
45
+ end
46
+ end
47
+ end