conductor_ruby 0.1.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.
Files changed (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/LICENSE +190 -0
  4. data/README.md +517 -0
  5. data/examples/agentic_workflows/llm_chat.rb +106 -0
  6. data/examples/dynamic_workflow.rb +177 -0
  7. data/examples/event_handler.rb +94 -0
  8. data/examples/event_listener_examples.rb +430 -0
  9. data/examples/helloworld/greetings_worker.rb +24 -0
  10. data/examples/helloworld/helloworld.rb +99 -0
  11. data/examples/kitchensink.rb +213 -0
  12. data/examples/metadata_journey.rb +189 -0
  13. data/examples/metrics_example.rb +284 -0
  14. data/examples/new_dsl_demo.rb +141 -0
  15. data/examples/orkes/http_poll.rb +83 -0
  16. data/examples/orkes/secrets_example.rb +69 -0
  17. data/examples/orkes/wait_for_webhook.rb +90 -0
  18. data/examples/prompt_journey.rb +245 -0
  19. data/examples/rag_workflow.rb +167 -0
  20. data/examples/schedule_journey.rb +244 -0
  21. data/examples/simple_worker.rb +125 -0
  22. data/examples/simple_workflow.rb +89 -0
  23. data/examples/task_context_example.rb +257 -0
  24. data/examples/task_listener_example.rb +192 -0
  25. data/examples/worker_configuration_example.rb +282 -0
  26. data/examples/workflow_dsl.rb +316 -0
  27. data/examples/workflow_ops.rb +305 -0
  28. data/lib/conductor/client/authorization_client.rb +238 -0
  29. data/lib/conductor/client/integration_client.rb +108 -0
  30. data/lib/conductor/client/metadata_client.rb +139 -0
  31. data/lib/conductor/client/prompt_client.rb +58 -0
  32. data/lib/conductor/client/scheduler_client.rb +132 -0
  33. data/lib/conductor/client/schema_client.rb +32 -0
  34. data/lib/conductor/client/secret_client.rb +48 -0
  35. data/lib/conductor/client/task_client.rb +168 -0
  36. data/lib/conductor/client/workflow_client.rb +242 -0
  37. data/lib/conductor/configuration/authentication_settings.rb +17 -0
  38. data/lib/conductor/configuration.rb +103 -0
  39. data/lib/conductor/exceptions.rb +86 -0
  40. data/lib/conductor/http/api/application_resource_api.rb +107 -0
  41. data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
  42. data/lib/conductor/http/api/event_resource_api.rb +133 -0
  43. data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
  44. data/lib/conductor/http/api/group_resource_api.rb +76 -0
  45. data/lib/conductor/http/api/integration_resource_api.rb +145 -0
  46. data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
  47. data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
  48. data/lib/conductor/http/api/role_resource_api.rb +60 -0
  49. data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
  50. data/lib/conductor/http/api/schema_resource_api.rb +82 -0
  51. data/lib/conductor/http/api/secret_resource_api.rb +134 -0
  52. data/lib/conductor/http/api/task_resource_api.rb +321 -0
  53. data/lib/conductor/http/api/token_resource_api.rb +42 -0
  54. data/lib/conductor/http/api/user_resource_api.rb +59 -0
  55. data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
  56. data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
  57. data/lib/conductor/http/api_client.rb +437 -0
  58. data/lib/conductor/http/models/authentication_config.rb +67 -0
  59. data/lib/conductor/http/models/authorization_request.rb +39 -0
  60. data/lib/conductor/http/models/base_model.rb +162 -0
  61. data/lib/conductor/http/models/bulk_response.rb +39 -0
  62. data/lib/conductor/http/models/conductor_application.rb +39 -0
  63. data/lib/conductor/http/models/conductor_user.rb +53 -0
  64. data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
  65. data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
  66. data/lib/conductor/http/models/event_handler.rb +130 -0
  67. data/lib/conductor/http/models/generate_token_request.rb +27 -0
  68. data/lib/conductor/http/models/group.rb +36 -0
  69. data/lib/conductor/http/models/integration.rb +70 -0
  70. data/lib/conductor/http/models/integration_api.rb +53 -0
  71. data/lib/conductor/http/models/integration_api_update.rb +43 -0
  72. data/lib/conductor/http/models/integration_update.rb +36 -0
  73. data/lib/conductor/http/models/permission.rb +24 -0
  74. data/lib/conductor/http/models/poll_data.rb +33 -0
  75. data/lib/conductor/http/models/prompt_template.rb +59 -0
  76. data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
  77. data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
  78. data/lib/conductor/http/models/role.rb +27 -0
  79. data/lib/conductor/http/models/schema_def.rb +59 -0
  80. data/lib/conductor/http/models/search_result.rb +187 -0
  81. data/lib/conductor/http/models/skip_task_request.rb +27 -0
  82. data/lib/conductor/http/models/start_workflow_request.rb +68 -0
  83. data/lib/conductor/http/models/subject_ref.rb +35 -0
  84. data/lib/conductor/http/models/tag_object.rb +36 -0
  85. data/lib/conductor/http/models/target_ref.rb +39 -0
  86. data/lib/conductor/http/models/task.rb +156 -0
  87. data/lib/conductor/http/models/task_def.rb +95 -0
  88. data/lib/conductor/http/models/task_exec_log.rb +30 -0
  89. data/lib/conductor/http/models/task_result.rb +115 -0
  90. data/lib/conductor/http/models/task_result_status.rb +24 -0
  91. data/lib/conductor/http/models/token.rb +33 -0
  92. data/lib/conductor/http/models/upsert_group_request.rb +30 -0
  93. data/lib/conductor/http/models/upsert_user_request.rb +39 -0
  94. data/lib/conductor/http/models/workflow.rb +202 -0
  95. data/lib/conductor/http/models/workflow_def.rb +73 -0
  96. data/lib/conductor/http/models/workflow_schedule.rb +100 -0
  97. data/lib/conductor/http/models/workflow_state_update.rb +30 -0
  98. data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
  99. data/lib/conductor/http/models/workflow_task.rb +169 -0
  100. data/lib/conductor/http/models/workflow_test_request.rb +67 -0
  101. data/lib/conductor/http/rest_client.rb +211 -0
  102. data/lib/conductor/orkes/models/access_key.rb +56 -0
  103. data/lib/conductor/orkes/models/granted_permission.rb +27 -0
  104. data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
  105. data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
  106. data/lib/conductor/orkes/orkes_clients.rb +69 -0
  107. data/lib/conductor/version.rb +5 -0
  108. data/lib/conductor/worker/events/conductor_event.rb +40 -0
  109. data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
  110. data/lib/conductor/worker/events/http_events.rb +25 -0
  111. data/lib/conductor/worker/events/listener_registry.rb +40 -0
  112. data/lib/conductor/worker/events/listeners.rb +34 -0
  113. data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
  114. data/lib/conductor/worker/events/task_runner_events.rb +271 -0
  115. data/lib/conductor/worker/events/workflow_events.rb +49 -0
  116. data/lib/conductor/worker/fiber_executor.rb +532 -0
  117. data/lib/conductor/worker/ractor_task_runner.rb +501 -0
  118. data/lib/conductor/worker/task_context.rb +114 -0
  119. data/lib/conductor/worker/task_definition_registrar.rb +322 -0
  120. data/lib/conductor/worker/task_handler.rb +360 -0
  121. data/lib/conductor/worker/task_in_progress.rb +60 -0
  122. data/lib/conductor/worker/task_runner.rb +538 -0
  123. data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
  124. data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
  125. data/lib/conductor/worker/worker.rb +355 -0
  126. data/lib/conductor/worker/worker_config.rb +154 -0
  127. data/lib/conductor/worker/worker_registry.rb +71 -0
  128. data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
  129. data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
  130. data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
  131. data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
  132. data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
  133. data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
  134. data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
  135. data/lib/conductor/workflow/llm/chat_message.rb +47 -0
  136. data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
  137. data/lib/conductor/workflow/llm/tool_call.rb +43 -0
  138. data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
  139. data/lib/conductor/workflow/task_type.rb +68 -0
  140. data/lib/conductor/workflow/timeout_policy.rb +31 -0
  141. data/lib/conductor/workflow/workflow_executor.rb +373 -0
  142. data/lib/conductor.rb +192 -0
  143. metadata +359 -0
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../http/models/task_def'
5
+ require_relative '../client/metadata_client'
6
+
7
+ module Conductor
8
+ module Worker
9
+ # TaskDefinitionRegistrar - Handles automatic task definition registration
10
+ # Generates JSON schemas from worker function signatures and registers
11
+ # task definitions with the Conductor server.
12
+ class TaskDefinitionRegistrar
13
+ # @param configuration [Configuration] Conductor configuration
14
+ # @param logger [Logger] Logger instance
15
+ def initialize(configuration, logger: nil)
16
+ @configuration = configuration
17
+ @metadata_client = Client::MetadataClient.new(configuration)
18
+ @logger = logger || Logger.new($stdout)
19
+ end
20
+
21
+ # Register a task definition for a worker
22
+ # @param worker [Worker] The worker instance
23
+ # @return [Boolean] True if registration succeeded
24
+ def register(worker)
25
+ return false unless worker.register_task_def
26
+
27
+ task_def = build_task_definition(worker)
28
+
29
+ # Generate schemas if worker has typed parameters
30
+ input_schema = generate_input_schema(worker)
31
+ output_schema = generate_output_schema(worker)
32
+
33
+ # Register schemas if available
34
+ register_schemas(worker.task_definition_name, input_schema, output_schema) if input_schema || output_schema
35
+
36
+ # Register or update task definition
37
+ if worker.overwrite_task_def
38
+ register_or_update_task_def(task_def)
39
+ else
40
+ register_if_not_exists(task_def)
41
+ end
42
+
43
+ @logger.info("Registered task definition: #{worker.task_definition_name}")
44
+ true
45
+ rescue StandardError => e
46
+ @logger.warn("Failed to register task definition '#{worker.task_definition_name}': #{e.message}")
47
+ false
48
+ end
49
+
50
+ private
51
+
52
+ # Build a TaskDef from worker configuration
53
+ # @param worker [Worker] The worker instance
54
+ # @return [TaskDef]
55
+ def build_task_definition(worker)
56
+ task_def = worker.task_def_template&.dup || Http::Models::TaskDef.new
57
+
58
+ task_def.name = worker.task_definition_name
59
+
60
+ # Set reasonable defaults if not provided
61
+ task_def.retry_count ||= 3
62
+ task_def.retry_logic ||= Http::Models::TaskDef::RetryLogic::FIXED
63
+ task_def.timeout_policy ||= Http::Models::TaskDef::TaskTimeoutPolicy::TIME_OUT_WF
64
+ task_def.timeout_seconds ||= 60
65
+ task_def.response_timeout_seconds ||= 60
66
+
67
+ task_def
68
+ end
69
+
70
+ # Generate JSON Schema for worker input parameters
71
+ # @param worker [Worker] The worker instance
72
+ # @return [Hash, nil] JSON Schema or nil
73
+ def generate_input_schema(worker)
74
+ return nil unless worker.execute_function.respond_to?(:parameters)
75
+
76
+ params = worker.execute_function.parameters
77
+ return nil if params.empty?
78
+
79
+ # Skip if first param is a positional arg (takes full Task object)
80
+ first_type = params.first&.first
81
+ return nil if %i[req opt rest].include?(first_type)
82
+
83
+ properties = {}
84
+ required = []
85
+
86
+ params.each do |type, name|
87
+ next unless name
88
+
89
+ prop_name = name.to_s
90
+
91
+ case type
92
+ when :keyreq # Required keyword argument
93
+ properties[prop_name] = infer_property_schema(name)
94
+ required << prop_name
95
+ when :key # Optional keyword argument
96
+ properties[prop_name] = infer_property_schema(name)
97
+ when :keyrest # **kwargs
98
+ # Can't generate schema for **kwargs
99
+ return nil
100
+ end
101
+ end
102
+
103
+ return nil if properties.empty?
104
+
105
+ schema = {
106
+ '$schema' => 'http://json-schema.org/draft-07/schema#',
107
+ 'type' => 'object',
108
+ 'title' => "#{worker.task_definition_name}_input",
109
+ 'properties' => properties
110
+ }
111
+
112
+ schema['required'] = required unless required.empty?
113
+ schema['additionalProperties'] = !worker.strict_schema
114
+
115
+ schema
116
+ end
117
+
118
+ # Generate JSON Schema for worker output
119
+ # @param worker [Worker] The worker instance
120
+ # @return [Hash, nil] JSON Schema or nil
121
+ def generate_output_schema(_worker)
122
+ # Output schema is harder to infer without return type annotations
123
+ # In Ruby, we'd need Sorbet/RBS type annotations
124
+ # For now, return nil (no output schema)
125
+ nil
126
+ end
127
+
128
+ # Infer property schema from parameter name
129
+ # Uses naming conventions to guess types
130
+ # @param name [Symbol] Parameter name
131
+ # @return [Hash] Property schema
132
+ def infer_property_schema(name)
133
+ name_str = name.to_s.downcase
134
+
135
+ # Infer type from naming conventions
136
+ type = if name_str.end_with?('_id', 'id', '_count', 'count', '_num', 'num', '_index', 'index')
137
+ 'integer'
138
+ elsif name_str.end_with?('_at', '_time', '_date')
139
+ 'string' # ISO8601 date string
140
+ elsif name_str.start_with?('is_', 'has_', 'can_', 'should_', 'enable')
141
+ 'boolean'
142
+ elsif name_str.end_with?('_amount', '_price', '_rate', '_percent')
143
+ 'number'
144
+ elsif name_str.end_with?('_list', '_items', '_array', '_ids')
145
+ 'array'
146
+ elsif name_str.end_with?('_data', '_config', '_options', '_params', '_payload')
147
+ 'object'
148
+ else
149
+ 'string' # Default to string
150
+ end
151
+
152
+ schema = { 'type' => type }
153
+
154
+ # Add format hints for certain types
155
+ case name_str
156
+ when /email/
157
+ schema['format'] = 'email'
158
+ when /url/, /uri/, /href/
159
+ schema['format'] = 'uri'
160
+ when /_at$/, /_time$/
161
+ schema['format'] = 'date-time'
162
+ when /_date$/
163
+ schema['format'] = 'date'
164
+ when /uuid/, /guid/
165
+ schema['format'] = 'uuid'
166
+ end
167
+
168
+ schema
169
+ end
170
+
171
+ # Register schemas with the server
172
+ # @param task_name [String] Task definition name
173
+ # @param input_schema [Hash, nil] Input schema
174
+ # @param output_schema [Hash, nil] Output schema
175
+ def register_schemas(task_name, input_schema, output_schema)
176
+ # NOTE: Schema registration requires Orkes Conductor
177
+ # OSS Conductor may not have this endpoint
178
+
179
+ if input_schema
180
+ begin
181
+ register_schema("#{task_name}_input", input_schema)
182
+ rescue ApiError => e
183
+ @logger.debug("Schema registration not available: #{e.message}") if e.status == 404
184
+ end
185
+ end
186
+
187
+ return unless output_schema
188
+
189
+ begin
190
+ register_schema("#{task_name}_output", output_schema)
191
+ rescue ApiError => e
192
+ @logger.debug("Schema registration not available: #{e.message}") if e.status == 404
193
+ end
194
+ end
195
+
196
+ # Register a single schema
197
+ # @param name [String] Schema name
198
+ # @param schema [Hash] JSON Schema
199
+ def register_schema(name, _schema)
200
+ # This would call the schema API if available
201
+ # For now, just log
202
+ @logger.debug("Would register schema: #{name}")
203
+ end
204
+
205
+ # Register task def, update if already exists
206
+ # @param task_def [TaskDef]
207
+ def register_or_update_task_def(task_def)
208
+ @metadata_client.update_task_def(task_def)
209
+ rescue ApiError => e
210
+ raise unless e.status == 404
211
+
212
+ # Task def doesn't exist, create it
213
+ @metadata_client.register_task_def([task_def])
214
+ end
215
+
216
+ # Register task def only if it doesn't exist
217
+ # @param task_def [TaskDef]
218
+ def register_if_not_exists(task_def)
219
+ existing = @metadata_client.get_task_def(task_def.name)
220
+ @logger.info("Task definition '#{task_def.name}' already exists, skipping")
221
+ rescue ApiError => e
222
+ raise unless e.status == 404
223
+
224
+ # Task def doesn't exist, create it
225
+ @metadata_client.register_task_def([task_def])
226
+ end
227
+ end
228
+
229
+ # JsonSchemaGenerator - Utility for generating JSON Schema from Ruby types
230
+ # Can be extended to work with Sorbet types or RBS annotations
231
+ class JsonSchemaGenerator
232
+ # Ruby type to JSON Schema type mapping
233
+ TYPE_MAP = {
234
+ 'String' => 'string',
235
+ 'Integer' => 'integer',
236
+ 'Float' => 'number',
237
+ 'Numeric' => 'number',
238
+ 'TrueClass' => 'boolean',
239
+ 'FalseClass' => 'boolean',
240
+ 'Array' => 'array',
241
+ 'Hash' => 'object',
242
+ 'NilClass' => 'null',
243
+ 'Time' => 'string',
244
+ 'Date' => 'string',
245
+ 'DateTime' => 'string'
246
+ }.freeze
247
+
248
+ # Generate JSON Schema from a Ruby value (for inference)
249
+ # @param value [Object] A sample value
250
+ # @return [Hash] JSON Schema
251
+ def self.from_value(value)
252
+ case value
253
+ when String
254
+ { 'type' => 'string' }
255
+ when Integer
256
+ { 'type' => 'integer' }
257
+ when Float
258
+ { 'type' => 'number' }
259
+ when TrueClass, FalseClass
260
+ { 'type' => 'boolean' }
261
+ when Array
262
+ if value.empty?
263
+ { 'type' => 'array' }
264
+ else
265
+ { 'type' => 'array', 'items' => from_value(value.first) }
266
+ end
267
+ when Hash
268
+ generate_object_schema(value)
269
+ when Time, DateTime
270
+ { 'type' => 'string', 'format' => 'date-time' }
271
+ when Date
272
+ { 'type' => 'string', 'format' => 'date' }
273
+ when NilClass
274
+ { 'type' => 'null' }
275
+ else
276
+ { 'type' => 'object' }
277
+ end
278
+ end
279
+
280
+ # Generate schema from a hash with sample values
281
+ # @param hash [Hash] Hash with sample values
282
+ # @return [Hash] JSON Schema
283
+ def self.generate_object_schema(hash)
284
+ properties = {}
285
+ hash.each do |key, value|
286
+ properties[key.to_s] = from_value(value)
287
+ end
288
+
289
+ {
290
+ 'type' => 'object',
291
+ 'properties' => properties
292
+ }
293
+ end
294
+
295
+ # Generate a schema from a Ruby class definition
296
+ # Works with Struct, Data (Ruby 3.2+), or classes with attr_accessor
297
+ # @param klass [Class] Ruby class
298
+ # @return [Hash] JSON Schema
299
+ def self.from_class(klass)
300
+ properties = {}
301
+
302
+ # Try to get attribute names
303
+ if klass.respond_to?(:members)
304
+ # Struct or Data
305
+ klass.members.each do |attr|
306
+ properties[attr.to_s] = { 'type' => 'string' }
307
+ end
308
+ elsif klass.instance_methods.include?(:to_h)
309
+ # Has to_h, try to instantiate and inspect
310
+ # Skip this for safety
311
+ end
312
+
313
+ {
314
+ '$schema' => 'http://json-schema.org/draft-07/schema#',
315
+ 'type' => 'object',
316
+ 'title' => klass.name,
317
+ 'properties' => properties
318
+ }
319
+ end
320
+ end
321
+ end
322
+ end
@@ -0,0 +1,360 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require_relative 'worker'
5
+ require_relative 'worker_registry'
6
+ require_relative 'worker_config'
7
+ require_relative 'task_runner'
8
+ require_relative 'task_definition_registrar'
9
+ require_relative 'events/sync_event_dispatcher'
10
+ require_relative 'events/listener_registry'
11
+
12
+ module Conductor
13
+ module Worker
14
+ # TaskHandler - The top-level orchestrator that manages all workers
15
+ # Creates one Thread per worker, each running a TaskRunner
16
+ #
17
+ # Supports multiple execution modes based on worker configuration:
18
+ # - :thread (default) - Thread-based with ThreadPoolExecutor
19
+ # - :ractor - Ractor-based for true parallelism (Ruby 3.1+)
20
+ # - :fiber - Fiber-based with async gem for high I/O concurrency
21
+ class TaskHandler
22
+ attr_reader :workers, :configuration, :event_dispatcher
23
+
24
+ # Initialize TaskHandler
25
+ # @param workers [Array<Worker>, nil] Pre-created worker instances
26
+ # @param configuration [Configuration, nil] Conductor configuration
27
+ # @param scan_for_annotated_workers [Boolean] Auto-discover workers from registry
28
+ # @param import_modules [Array<String>, nil] Ruby files to require (triggers registration)
29
+ # @param event_listeners [Array<Object>, nil] Custom event listeners
30
+ # @param logger [Logger, nil] Logger instance
31
+ # @param register_task_definitions [Boolean] Auto-register task definitions on start
32
+ def initialize(
33
+ workers: nil,
34
+ configuration: nil,
35
+ scan_for_annotated_workers: true,
36
+ import_modules: nil,
37
+ event_listeners: nil,
38
+ logger: nil,
39
+ register_task_definitions: false
40
+ )
41
+ @configuration = configuration || Configuration.new
42
+ @logger = logger || create_default_logger
43
+ @event_dispatcher = Events::SyncEventDispatcher.new
44
+ @workers = []
45
+ @threads = []
46
+ @runners = []
47
+ @ractors = [] # For Ractor-based workers
48
+ @running = false
49
+ @mutex = Mutex.new
50
+ @register_task_definitions = register_task_definitions
51
+ @event_listeners = []
52
+
53
+ # Register event listeners
54
+ register_listeners(event_listeners) if event_listeners
55
+
56
+ # Import modules (triggers worker_task registrations)
57
+ import_worker_modules(import_modules) if import_modules
58
+
59
+ # Discover workers from registry
60
+ discover_registered_workers if scan_for_annotated_workers
61
+
62
+ # Add provided workers
63
+ add_workers(workers) if workers
64
+ end
65
+
66
+ # Add workers to the handler
67
+ # @param workers [Array<Worker>] Workers to add
68
+ # @return [self]
69
+ def add_workers(workers)
70
+ workers.each { |w| add_worker(w) }
71
+ self
72
+ end
73
+
74
+ # Add a single worker
75
+ # @param worker [Worker] Worker to add
76
+ # @return [self]
77
+ def add_worker(worker)
78
+ @mutex.synchronize do
79
+ @workers << worker
80
+ end
81
+ self
82
+ end
83
+
84
+ # Start all worker threads
85
+ # @return [self]
86
+ def start
87
+ @mutex.synchronize do
88
+ return self if @running
89
+
90
+ @running = true
91
+
92
+ # Register task definitions if enabled
93
+ register_all_task_definitions if @register_task_definitions
94
+
95
+ @workers.each do |worker|
96
+ start_worker(worker)
97
+ end
98
+
99
+ @logger.info("TaskHandler started with #{@workers.size} workers")
100
+ end
101
+
102
+ self
103
+ end
104
+
105
+ private
106
+
107
+ # Start a single worker with appropriate runner type
108
+ # @param worker [Worker] The worker to start
109
+ def start_worker(worker)
110
+ # Determine execution mode from worker configuration
111
+ isolation = worker.respond_to?(:isolation) ? worker.isolation : :thread
112
+ executor = worker.respond_to?(:executor) ? worker.executor : :thread_pool
113
+
114
+ case isolation
115
+ when :ractor
116
+ start_ractor_worker(worker)
117
+ else
118
+ # Thread-based execution (default)
119
+ case executor
120
+ when :fiber
121
+ start_fiber_worker(worker)
122
+ else
123
+ start_thread_worker(worker)
124
+ end
125
+ end
126
+ end
127
+
128
+ # Start a thread-based worker (default mode)
129
+ # @param worker [Worker] The worker to start
130
+ def start_thread_worker(worker)
131
+ runner = TaskRunner.new(
132
+ worker,
133
+ configuration: @configuration,
134
+ event_dispatcher: @event_dispatcher,
135
+ logger: @logger
136
+ )
137
+ @runners << runner
138
+
139
+ thread = Thread.new(runner) do |r|
140
+ Thread.current.name = "conductor-worker-#{r.worker.task_definition_name}"
141
+ r.run
142
+ rescue StandardError => e
143
+ @logger.fatal("Fatal error in worker '#{r.worker.task_definition_name}': #{e.message}")
144
+ @logger.debug(e.backtrace.join("\n")) if e.backtrace
145
+ end
146
+
147
+ @threads << thread
148
+ end
149
+
150
+ # Start a Ractor-based worker for true parallelism
151
+ # @param worker [Worker] The worker to start
152
+ def start_ractor_worker(worker)
153
+ require_relative 'ractor_task_runner'
154
+
155
+ RactorSupport.require_ractors!
156
+
157
+ thread_count = worker.respond_to?(:thread_count) ? worker.thread_count : 1
158
+
159
+ # Create event receiver Ractor to collect events from worker Ractors
160
+ event_receiver = create_event_receiver_ractor(worker.task_definition_name)
161
+
162
+ # Create multiple Ractors for parallelism
163
+ thread_count.times do |i|
164
+ ractor = Ractor.new(worker, @configuration, i, event_receiver) do |w, config, ractor_id, evt_queue|
165
+ runner = RactorTaskRunner.new(
166
+ w,
167
+ configuration: config,
168
+ ractor_id: ractor_id,
169
+ event_queue: evt_queue
170
+ )
171
+ runner.run
172
+ end
173
+ @ractors << ractor
174
+ end
175
+
176
+ @logger.info("Started #{thread_count} Ractor(s) for '#{worker.task_definition_name}'")
177
+ end
178
+
179
+ # Start a fiber-based worker for high I/O concurrency
180
+ # @param worker [Worker] The worker to start
181
+ def start_fiber_worker(worker)
182
+ require_relative 'fiber_executor'
183
+
184
+ AsyncSupport.require_async!
185
+
186
+ runner = FiberTaskRunner.new(
187
+ worker,
188
+ configuration: @configuration,
189
+ event_dispatcher: @event_dispatcher,
190
+ logger: @logger
191
+ )
192
+ @runners << runner
193
+
194
+ thread = Thread.new(runner) do |r|
195
+ Thread.current.name = "conductor-fiber-#{r.worker.task_definition_name}"
196
+ r.run
197
+ rescue StandardError => e
198
+ @logger.fatal("Fatal error in fiber worker '#{r.worker.task_definition_name}': #{e.message}")
199
+ @logger.debug(e.backtrace.join("\n")) if e.backtrace
200
+ end
201
+
202
+ @threads << thread
203
+ end
204
+
205
+ # Create event receiver Ractor to forward events to dispatcher
206
+ # @param task_name [String] Task name for logging
207
+ # @return [Ractor] Event receiver Ractor
208
+ def create_event_receiver_ractor(task_name)
209
+ dispatcher = @event_dispatcher
210
+ logger = @logger
211
+
212
+ Thread.new do
213
+ Thread.current.name = "conductor-event-receiver-#{task_name}"
214
+ # NOTE: In production, this would need proper Ractor communication
215
+ # For now, events from Ractors are logged but not dispatched
216
+ # due to Ractor isolation constraints
217
+ logger.debug("Event receiver started for #{task_name}")
218
+ end
219
+
220
+ # Return nil for now - Ractor event communication needs more work
221
+ nil
222
+ end
223
+
224
+ # Register all task definitions
225
+ def register_all_task_definitions
226
+ registrar = TaskDefinitionRegistrar.new(@configuration, logger: @logger)
227
+ @workers.each do |worker|
228
+ registrar.register(worker)
229
+ end
230
+ end
231
+
232
+ public
233
+
234
+ # Stop all workers gracefully
235
+ # @param timeout [Integer] Seconds to wait before force-killing threads
236
+ # @return [self]
237
+ def stop(timeout: 5)
238
+ @mutex.synchronize do
239
+ return self unless @running
240
+
241
+ @logger.info('Stopping TaskHandler...')
242
+
243
+ # Signal all runners to shutdown
244
+ @runners.each(&:shutdown)
245
+
246
+ # Wait for threads to finish
247
+ @threads.each do |thread|
248
+ thread.join(timeout)
249
+ thread.kill if thread.alive?
250
+ end
251
+
252
+ # Shutdown Ractors
253
+ @ractors.each do |ractor|
254
+ # Ractors don't have a clean shutdown mechanism
255
+ # They'll be GC'd when no longer referenced
256
+ ractor.take if ractor.respond_to?(:take)
257
+ rescue Ractor::ClosedError, Ractor::RemoteError
258
+ # Ractor already finished
259
+ end
260
+
261
+ @runners.clear
262
+ @threads.clear
263
+ @ractors.clear
264
+
265
+ stop_event_listeners
266
+ @running = false
267
+
268
+ @logger.info('TaskHandler stopped')
269
+ end
270
+
271
+ self
272
+ end
273
+
274
+ # Wait for all worker threads to complete (blocking)
275
+ # @return [self]
276
+ def join
277
+ @threads.each(&:join)
278
+ self
279
+ end
280
+
281
+ # Check if handler is running
282
+ # @return [Boolean]
283
+ def running?
284
+ @running
285
+ end
286
+
287
+ # Get list of worker names
288
+ # @return [Array<String>]
289
+ def worker_names
290
+ @workers.map(&:task_definition_name)
291
+ end
292
+
293
+ # Context manager pattern - execute block and stop on exit
294
+ # @yield [self]
295
+ # @return [Object] Block return value
296
+ def self.run(workers: nil, configuration: nil, **options)
297
+ handler = new(workers: workers, configuration: configuration, **options)
298
+ begin
299
+ yield handler if block_given?
300
+ ensure
301
+ handler.stop
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ # Create default logger
308
+ # @return [Logger]
309
+ def create_default_logger
310
+ logger = Logger.new($stdout)
311
+ logger.level = Logger::INFO
312
+ logger.formatter = proc do |severity, datetime, _progname, msg|
313
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- #{msg}\n"
314
+ end
315
+ logger
316
+ end
317
+
318
+ # Register event listeners
319
+ # @param listeners [Array<Object>] Listeners to register
320
+ def register_listeners(listeners)
321
+ listeners.each do |listener|
322
+ @event_listeners << listener
323
+ Events::ListenerRegistry.register_task_runner_listener(listener, @event_dispatcher)
324
+ end
325
+ end
326
+
327
+ def stop_event_listeners
328
+ @event_listeners.each do |listener|
329
+ listener.stop if listener.respond_to?(:stop)
330
+ rescue StandardError => e
331
+ @logger.debug { "Error stopping listener: #{e.class}: #{e.message}" }
332
+ end
333
+ end
334
+
335
+ # Import worker modules from file paths
336
+ # @param modules [Array<String>] File paths or module names to require
337
+ def import_worker_modules(modules)
338
+ modules.each do |mod|
339
+ require mod
340
+ rescue LoadError => e
341
+ @logger.warn("Failed to load module '#{mod}': #{e.message}")
342
+ end
343
+ end
344
+
345
+ # Discover workers from the global registry
346
+ def discover_registered_workers
347
+ WorkerRegistry.all.each do |definition|
348
+ worker = Worker.new(
349
+ definition[:task_definition_name],
350
+ definition[:execute_function],
351
+ **definition[:options]
352
+ )
353
+ @workers << worker
354
+ end
355
+
356
+ @logger.info("Discovered #{WorkerRegistry.count} workers from registry") if WorkerRegistry.count.positive?
357
+ end
358
+ end
359
+ end
360
+ end