taski 0.5.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +50 -0
- data/README.md +40 -21
- data/docs/GUIDE.md +340 -0
- data/examples/README.md +65 -17
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/group_demo.rb +113 -0
- data/examples/reexecution_demo.rb +93 -80
- data/examples/system_call_demo.rb +56 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/execution_context.rb +379 -0
- data/lib/taski/execution/executor.rb +408 -117
- data/lib/taski/execution/registry.rb +17 -1
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +216 -0
- data/lib/taski/execution/task_wrapper.rb +210 -40
- data/lib/taski/execution/tree_progress_display.rb +385 -98
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +16 -3
- data/lib/taski/task.rb +222 -36
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +138 -23
- data/sig/taski.rbs +207 -27
- metadata +13 -7
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/examples/section_progress_demo.rb +0 -78
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/taski"
|
|
5
|
+
|
|
6
|
+
# Demo: Subprocess output capture with system()
|
|
7
|
+
#
|
|
8
|
+
# This example demonstrates how Taski captures output from system() calls
|
|
9
|
+
# and displays them in the progress spinner.
|
|
10
|
+
# Run with: ruby examples/system_call_demo.rb
|
|
11
|
+
|
|
12
|
+
class SlowOutputTask < Taski::Task
|
|
13
|
+
exports :success
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
puts "Running command with streaming output..."
|
|
17
|
+
# Use a command that produces output over time
|
|
18
|
+
@success = system("for i in 1 2 3 4 5; do echo \"Processing step $i...\"; sleep 0.3; done")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class AnotherSlowTask < Taski::Task
|
|
23
|
+
exports :result
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
puts "Running another slow command..."
|
|
27
|
+
@result = system("for i in A B C; do echo \"Stage $i complete\"; sleep 0.4; done")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class MainTask < Taski::Task
|
|
32
|
+
exports :summary
|
|
33
|
+
|
|
34
|
+
def run
|
|
35
|
+
puts "Starting main task..."
|
|
36
|
+
slow1 = SlowOutputTask.success
|
|
37
|
+
slow2 = AnotherSlowTask.result
|
|
38
|
+
@summary = {slow1: slow1, slow2: slow2}
|
|
39
|
+
puts "All done!"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
puts "=" * 60
|
|
44
|
+
puts "Subprocess Output Capture Demo"
|
|
45
|
+
puts "Watch the spinner show system() output in real-time!"
|
|
46
|
+
puts "=" * 60
|
|
47
|
+
puts
|
|
48
|
+
|
|
49
|
+
Taski.progress_display&.start
|
|
50
|
+
result = MainTask.summary
|
|
51
|
+
Taski.progress_display&.stop
|
|
52
|
+
|
|
53
|
+
puts
|
|
54
|
+
puts "=" * 60
|
|
55
|
+
puts "Result: #{result.inspect}"
|
|
56
|
+
puts "=" * 60
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
require "monitor"
|
|
4
4
|
|
|
5
5
|
module Taski
|
|
6
|
-
# Runtime
|
|
6
|
+
# Runtime arguments accessible from any task.
|
|
7
7
|
# Holds user-defined options and execution metadata.
|
|
8
|
-
#
|
|
9
|
-
class
|
|
8
|
+
# Args is immutable after creation - options cannot be modified during task execution.
|
|
9
|
+
class Args
|
|
10
10
|
attr_reader :started_at, :working_directory, :root_task
|
|
11
11
|
|
|
12
12
|
# @param options [Hash] User-defined options (immutable after creation)
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require_relative "task_output_router"
|
|
5
|
+
|
|
6
|
+
module Taski
|
|
7
|
+
module Execution
|
|
8
|
+
# ExecutionContext manages execution state and notifies observers about execution events.
|
|
9
|
+
# It decouples progress display from Executor using the observer pattern.
|
|
10
|
+
#
|
|
11
|
+
# == Architecture
|
|
12
|
+
#
|
|
13
|
+
# ExecutionContext is the central hub for execution events in the Taski framework:
|
|
14
|
+
#
|
|
15
|
+
# Executor → Scheduler/WorkerPool/ExecutionContext → Observers
|
|
16
|
+
#
|
|
17
|
+
# - Executor coordinates the overall execution flow
|
|
18
|
+
# - Scheduler manages dependency state and determines execution order
|
|
19
|
+
# - WorkerPool manages worker threads that execute tasks
|
|
20
|
+
# - ExecutionContext notifies observers about execution events
|
|
21
|
+
#
|
|
22
|
+
# == Observer Pattern
|
|
23
|
+
#
|
|
24
|
+
# Observers are registered using {#add_observer} and receive notifications
|
|
25
|
+
# via duck-typed method dispatch. Observers should implement any subset of:
|
|
26
|
+
#
|
|
27
|
+
# - register_task(task_class) - Called when a task is registered
|
|
28
|
+
# - update_task(task_class, state:, duration:, error:) - Called on state changes
|
|
29
|
+
# State values for run: :pending, :running, :completed, :failed
|
|
30
|
+
# State values for clean: :cleaning, :clean_completed, :clean_failed
|
|
31
|
+
# - register_section_impl(section_class, impl_class) - Called on section impl selection
|
|
32
|
+
# - set_root_task(task_class) - Called when root task is set
|
|
33
|
+
# - set_output_capture(output_capture) - Called when output capture is configured
|
|
34
|
+
# - start - Called when execution starts
|
|
35
|
+
# - stop - Called when execution ends
|
|
36
|
+
#
|
|
37
|
+
# == Thread Safety
|
|
38
|
+
#
|
|
39
|
+
# All observer operations are synchronized using Monitor. The output capture
|
|
40
|
+
# getter is also thread-safe for access from worker threads.
|
|
41
|
+
#
|
|
42
|
+
# == Backward Compatibility
|
|
43
|
+
#
|
|
44
|
+
# TreeProgressDisplay works as an observer without any API changes.
|
|
45
|
+
# Existing task code works unchanged.
|
|
46
|
+
#
|
|
47
|
+
# @example Registering an observer
|
|
48
|
+
# context = ExecutionContext.new
|
|
49
|
+
# context.add_observer(TreeProgressDisplay.new)
|
|
50
|
+
#
|
|
51
|
+
# @example Sending notifications
|
|
52
|
+
# context.notify_task_registered(MyTask)
|
|
53
|
+
# context.notify_task_started(MyTask)
|
|
54
|
+
# context.notify_task_completed(MyTask, duration: 1.5)
|
|
55
|
+
class ExecutionContext
|
|
56
|
+
# Thread-local key for storing the current execution context
|
|
57
|
+
THREAD_LOCAL_KEY = :taski_execution_context
|
|
58
|
+
|
|
59
|
+
# Get the current execution context for this thread.
|
|
60
|
+
# @return [ExecutionContext, nil] The current context or nil if not set
|
|
61
|
+
def self.current
|
|
62
|
+
Thread.current[THREAD_LOCAL_KEY]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Set the current execution context for this thread.
|
|
66
|
+
# @param context [ExecutionContext, nil] The context to set
|
|
67
|
+
def self.current=(context)
|
|
68
|
+
Thread.current[THREAD_LOCAL_KEY] = context
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Creates a new ExecutionContext and initializes its internal synchronization and state.
|
|
73
|
+
#
|
|
74
|
+
# Initializes a monitor for thread-safe operations and sets up empty observer storage
|
|
75
|
+
# and nil defaults for execution/clean triggers and output capture related fields.
|
|
76
|
+
def initialize
|
|
77
|
+
@monitor = Monitor.new
|
|
78
|
+
@observers = []
|
|
79
|
+
@execution_trigger = nil
|
|
80
|
+
@clean_trigger = nil
|
|
81
|
+
@output_capture = nil
|
|
82
|
+
@original_stdout = nil
|
|
83
|
+
@runtime_dependencies = {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Check if output capture is already active.
|
|
87
|
+
# @return [Boolean] true if capture is active
|
|
88
|
+
def output_capture_active?
|
|
89
|
+
@monitor.synchronize { !@output_capture.nil? }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Set up output capture for inline progress display.
|
|
93
|
+
# Creates TaskOutputRouter and replaces $stdout.
|
|
94
|
+
# Should only be called when progress display is active and not already set up.
|
|
95
|
+
#
|
|
96
|
+
# @param output_io [IO] The original output IO (usually $stdout)
|
|
97
|
+
def setup_output_capture(output_io)
|
|
98
|
+
@monitor.synchronize do
|
|
99
|
+
@original_stdout = output_io
|
|
100
|
+
@output_capture = TaskOutputRouter.new(@original_stdout)
|
|
101
|
+
$stdout = @output_capture
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
notify_set_output_capture(@output_capture)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Tear down output capture and restore original $stdout.
|
|
108
|
+
def teardown_output_capture
|
|
109
|
+
@monitor.synchronize do
|
|
110
|
+
return unless @original_stdout
|
|
111
|
+
|
|
112
|
+
$stdout = @original_stdout
|
|
113
|
+
@output_capture = nil
|
|
114
|
+
@original_stdout = nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get the current output capture instance.
|
|
119
|
+
# Thread-safe accessor for worker threads.
|
|
120
|
+
#
|
|
121
|
+
# @return [TaskOutputRouter, nil] The output capture or nil if not set
|
|
122
|
+
def output_capture
|
|
123
|
+
@monitor.synchronize { @output_capture }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Set the execution trigger callback.
|
|
127
|
+
# This is used to break the circular dependency between TaskWrapper and Executor.
|
|
128
|
+
# The trigger is a callable that takes (task_class, registry) and executes the task.
|
|
129
|
+
#
|
|
130
|
+
##
|
|
131
|
+
# Sets the execution trigger used to run tasks.
|
|
132
|
+
# This stores a Proc that will be invoked with (task_class, registry) when a task is executed; setting to `nil` clears the custom trigger and restores the default execution behavior.
|
|
133
|
+
# @param [Proc, nil] trigger - A callback receiving `(task_class, registry)`, or `nil` to unset the custom trigger.
|
|
134
|
+
def execution_trigger=(trigger)
|
|
135
|
+
@monitor.synchronize { @execution_trigger = trigger }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Set the clean trigger callback.
|
|
139
|
+
# This is used to break the circular dependency between TaskWrapper and Executor.
|
|
140
|
+
# The trigger is a callable that takes (task_class, registry) and cleans the task.
|
|
141
|
+
#
|
|
142
|
+
##
|
|
143
|
+
# Sets the clean trigger callback used to run task cleaning operations.
|
|
144
|
+
# @param [Proc, nil] trigger - The callback invoked by `trigger_clean`; `nil` clears the custom clean trigger.
|
|
145
|
+
def clean_trigger=(trigger)
|
|
146
|
+
@monitor.synchronize { @clean_trigger = trigger }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Trigger execution of a task.
|
|
150
|
+
# Falls back to Executor.execute if no custom trigger is set.
|
|
151
|
+
#
|
|
152
|
+
# @param task_class [Class] The task class to execute
|
|
153
|
+
##
|
|
154
|
+
# Executes the given task class using the configured execution trigger or falls back to Executor.execute.
|
|
155
|
+
# @param task_class [Class] The task class to execute.
|
|
156
|
+
# @param registry [Registry] The task registry used during execution.
|
|
157
|
+
def trigger_execution(task_class, registry:)
|
|
158
|
+
trigger = @monitor.synchronize { @execution_trigger }
|
|
159
|
+
if trigger
|
|
160
|
+
trigger.call(task_class, registry)
|
|
161
|
+
else
|
|
162
|
+
# Fallback for backward compatibility
|
|
163
|
+
Executor.execute(task_class, registry: registry, execution_context: self)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Trigger clean of a task.
|
|
168
|
+
# Falls back to Executor.execute_clean if no custom trigger is set.
|
|
169
|
+
#
|
|
170
|
+
# @param task_class [Class] The task class to clean
|
|
171
|
+
##
|
|
172
|
+
# Triggers a clean operation for the specified task class using the configured clean trigger or a backward-compatible fallback.
|
|
173
|
+
# @param [Class] task_class - The task class to clean.
|
|
174
|
+
# @param [Registry] registry - The task registry providing task lookup and metadata.
|
|
175
|
+
# @return [Object] The result returned by the clean operation (implementation-dependent).
|
|
176
|
+
def trigger_clean(task_class, registry:)
|
|
177
|
+
trigger = @monitor.synchronize { @clean_trigger }
|
|
178
|
+
if trigger
|
|
179
|
+
trigger.call(task_class, registry)
|
|
180
|
+
else
|
|
181
|
+
# Fallback for backward compatibility
|
|
182
|
+
Executor.execute_clean(task_class, registry: registry, execution_context: self)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# ========================================
|
|
187
|
+
# Runtime Dependency Tracking
|
|
188
|
+
# ========================================
|
|
189
|
+
|
|
190
|
+
# Register a runtime dependency between task classes.
|
|
191
|
+
# Used by Section to track dynamically selected implementations.
|
|
192
|
+
# Thread-safe for access from worker threads.
|
|
193
|
+
#
|
|
194
|
+
# @param from_class [Class] The task class that depends on to_class
|
|
195
|
+
# @param to_class [Class] The dependency task class
|
|
196
|
+
def register_runtime_dependency(from_class, to_class)
|
|
197
|
+
@monitor.synchronize do
|
|
198
|
+
@runtime_dependencies[from_class] ||= Set.new
|
|
199
|
+
@runtime_dependencies[from_class].add(to_class)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Get a copy of the runtime dependencies.
|
|
204
|
+
# Returns a hash mapping from_class to Set of to_classes.
|
|
205
|
+
# Thread-safe accessor.
|
|
206
|
+
#
|
|
207
|
+
# @return [Hash{Class => Set<Class>}] Copy of runtime dependencies
|
|
208
|
+
def runtime_dependencies
|
|
209
|
+
@monitor.synchronize do
|
|
210
|
+
@runtime_dependencies.transform_values(&:dup)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Add an observer to receive execution notifications.
|
|
215
|
+
# Observers should implement the following methods (all optional):
|
|
216
|
+
# - register_task(task_class)
|
|
217
|
+
# - update_task(task_class, state:, duration:, error:)
|
|
218
|
+
# - register_section_impl(section_class, impl_class)
|
|
219
|
+
# - set_root_task(task_class)
|
|
220
|
+
# - set_output_capture(output_capture)
|
|
221
|
+
# - start
|
|
222
|
+
# - stop
|
|
223
|
+
#
|
|
224
|
+
# @param observer [Object] The observer to add
|
|
225
|
+
def add_observer(observer)
|
|
226
|
+
@monitor.synchronize { @observers << observer }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Remove an observer.
|
|
230
|
+
#
|
|
231
|
+
# @param observer [Object] The observer to remove
|
|
232
|
+
def remove_observer(observer)
|
|
233
|
+
@monitor.synchronize { @observers.delete(observer) }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Returns a copy of the current observers list.
|
|
237
|
+
#
|
|
238
|
+
# @return [Array<Object>] Copy of observers array
|
|
239
|
+
def observers
|
|
240
|
+
@monitor.synchronize { @observers.dup }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Notify observers that a task has been registered.
|
|
244
|
+
#
|
|
245
|
+
# @param task_class [Class] The task class that was registered
|
|
246
|
+
def notify_task_registered(task_class)
|
|
247
|
+
dispatch(:register_task, task_class)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Notify observers that a task has started execution.
|
|
251
|
+
#
|
|
252
|
+
# @param task_class [Class] The task class that started
|
|
253
|
+
def notify_task_started(task_class)
|
|
254
|
+
dispatch(:update_task, task_class, state: :running)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Notify observers that a task has completed.
|
|
258
|
+
#
|
|
259
|
+
# @param task_class [Class] The task class that completed
|
|
260
|
+
# @param duration [Float, nil] The execution duration in seconds
|
|
261
|
+
# @param error [Exception, nil] The error if the task failed
|
|
262
|
+
def notify_task_completed(task_class, duration: nil, error: nil)
|
|
263
|
+
state = error ? :failed : :completed
|
|
264
|
+
dispatch(:update_task, task_class, state: state, duration: duration, error: error)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Notify observers that a section implementation has been selected.
|
|
268
|
+
#
|
|
269
|
+
# @param section_class [Class] The section class
|
|
270
|
+
# @param impl_class [Class] The selected implementation class
|
|
271
|
+
def notify_section_impl_selected(section_class, impl_class)
|
|
272
|
+
dispatch(:register_section_impl, section_class, impl_class)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Notify observers to set the root task.
|
|
276
|
+
#
|
|
277
|
+
# @param task_class [Class] The root task class
|
|
278
|
+
def notify_set_root_task(task_class)
|
|
279
|
+
dispatch(:set_root_task, task_class)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Notify observers to set the output capture.
|
|
283
|
+
#
|
|
284
|
+
# @param output_capture [TaskOutputRouter] The output capture instance
|
|
285
|
+
def notify_set_output_capture(output_capture)
|
|
286
|
+
dispatch(:set_output_capture, output_capture)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Notify observers to start.
|
|
290
|
+
def notify_start
|
|
291
|
+
dispatch(:start)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
##
|
|
295
|
+
# Notify registered observers that execution has stopped.
|
|
296
|
+
def notify_stop
|
|
297
|
+
dispatch(:stop)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# ========================================
|
|
301
|
+
# Clean Lifecycle Notifications
|
|
302
|
+
# ========================================
|
|
303
|
+
|
|
304
|
+
# Notify observers that a task's clean has started.
|
|
305
|
+
#
|
|
306
|
+
##
|
|
307
|
+
# Notifies observers that cleaning of the given task has started.
|
|
308
|
+
# Dispatches an `:update_task` notification with `state: :cleaning`.
|
|
309
|
+
# @param [Class] task_class The task class that started cleaning.
|
|
310
|
+
def notify_clean_started(task_class)
|
|
311
|
+
dispatch(:update_task, task_class, state: :cleaning)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Notify observers that a task's clean has completed.
|
|
315
|
+
#
|
|
316
|
+
# @param task_class [Class] The task class that completed cleaning
|
|
317
|
+
# @param duration [Float, nil] The clean duration in milliseconds
|
|
318
|
+
##
|
|
319
|
+
# Notifies observers that a task's clean has completed, including duration and any error.
|
|
320
|
+
# Observers receive an `:update_task` notification with `state` set to `:clean_completed` when
|
|
321
|
+
# `error` is nil, or `:clean_failed` when `error` is provided.
|
|
322
|
+
# @param [Class] task_class - The task class that finished cleaning.
|
|
323
|
+
# @param [Numeric, nil] duration - The duration of the clean operation in milliseconds, or `nil` if unknown.
|
|
324
|
+
# @param [Exception, nil] error - The error raised during cleaning, or `nil` if the clean succeeded.
|
|
325
|
+
def notify_clean_completed(task_class, duration: nil, error: nil)
|
|
326
|
+
state = error ? :clean_failed : :clean_completed
|
|
327
|
+
dispatch(:update_task, task_class, state: state, duration: duration, error: error)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# ========================================
|
|
331
|
+
# Group Lifecycle Notifications
|
|
332
|
+
# ========================================
|
|
333
|
+
|
|
334
|
+
# Notify observers that a group has started within a task.
|
|
335
|
+
#
|
|
336
|
+
# @param task_class [Class] The task class containing the group
|
|
337
|
+
# @param group_name [String] The name of the group
|
|
338
|
+
def notify_group_started(task_class, group_name)
|
|
339
|
+
dispatch(:update_group, task_class, group_name, state: :running)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Notify observers that a group has completed within a task.
|
|
343
|
+
#
|
|
344
|
+
# @param task_class [Class] The task class containing the group
|
|
345
|
+
# @param group_name [String] The name of the group
|
|
346
|
+
# @param duration [Float, nil] The group duration in milliseconds
|
|
347
|
+
# @param error [Exception, nil] The error if the group failed
|
|
348
|
+
def notify_group_completed(task_class, group_name, duration: nil, error: nil)
|
|
349
|
+
state = error ? :failed : :completed
|
|
350
|
+
dispatch(:update_group, task_class, group_name, state: state, duration: duration, error: error)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
private
|
|
354
|
+
|
|
355
|
+
# Dispatch a method call to all observers that respond to the method.
|
|
356
|
+
# Uses duck typing: observers only receive calls for methods they implement.
|
|
357
|
+
#
|
|
358
|
+
# @param method_name [Symbol] The method to call on observers
|
|
359
|
+
# @param args [Array] Arguments to pass to the method
|
|
360
|
+
# @param kwargs [Hash] Keyword arguments to pass to the method
|
|
361
|
+
def dispatch(method_name, *args, **kwargs)
|
|
362
|
+
current_observers = @monitor.synchronize { @observers.dup }
|
|
363
|
+
current_observers.each do |observer|
|
|
364
|
+
next unless observer.respond_to?(method_name)
|
|
365
|
+
|
|
366
|
+
begin
|
|
367
|
+
if kwargs.empty?
|
|
368
|
+
observer.public_send(method_name, *args)
|
|
369
|
+
else
|
|
370
|
+
observer.public_send(method_name, *args, **kwargs)
|
|
371
|
+
end
|
|
372
|
+
rescue => e
|
|
373
|
+
warn "[ExecutionContext] Observer #{observer.class} raised error in #{method_name}: #{e.message}"
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|