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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +142 -0
- data/LICENSE +190 -0
- data/README.md +517 -0
- data/examples/agentic_workflows/llm_chat.rb +106 -0
- data/examples/dynamic_workflow.rb +177 -0
- data/examples/event_handler.rb +94 -0
- data/examples/event_listener_examples.rb +430 -0
- data/examples/helloworld/greetings_worker.rb +24 -0
- data/examples/helloworld/helloworld.rb +99 -0
- data/examples/kitchensink.rb +213 -0
- data/examples/metadata_journey.rb +189 -0
- data/examples/metrics_example.rb +284 -0
- data/examples/new_dsl_demo.rb +141 -0
- data/examples/orkes/http_poll.rb +83 -0
- data/examples/orkes/secrets_example.rb +69 -0
- data/examples/orkes/wait_for_webhook.rb +90 -0
- data/examples/prompt_journey.rb +245 -0
- data/examples/rag_workflow.rb +167 -0
- data/examples/schedule_journey.rb +244 -0
- data/examples/simple_worker.rb +125 -0
- data/examples/simple_workflow.rb +89 -0
- data/examples/task_context_example.rb +257 -0
- data/examples/task_listener_example.rb +192 -0
- data/examples/worker_configuration_example.rb +282 -0
- data/examples/workflow_dsl.rb +316 -0
- data/examples/workflow_ops.rb +305 -0
- data/lib/conductor/client/authorization_client.rb +238 -0
- data/lib/conductor/client/integration_client.rb +108 -0
- data/lib/conductor/client/metadata_client.rb +139 -0
- data/lib/conductor/client/prompt_client.rb +58 -0
- data/lib/conductor/client/scheduler_client.rb +132 -0
- data/lib/conductor/client/schema_client.rb +32 -0
- data/lib/conductor/client/secret_client.rb +48 -0
- data/lib/conductor/client/task_client.rb +168 -0
- data/lib/conductor/client/workflow_client.rb +242 -0
- data/lib/conductor/configuration/authentication_settings.rb +17 -0
- data/lib/conductor/configuration.rb +103 -0
- data/lib/conductor/exceptions.rb +86 -0
- data/lib/conductor/http/api/application_resource_api.rb +107 -0
- data/lib/conductor/http/api/authorization_resource_api.rb +56 -0
- data/lib/conductor/http/api/event_resource_api.rb +133 -0
- data/lib/conductor/http/api/gateway_auth_resource_api.rb +48 -0
- data/lib/conductor/http/api/group_resource_api.rb +76 -0
- data/lib/conductor/http/api/integration_resource_api.rb +145 -0
- data/lib/conductor/http/api/metadata_resource_api.rb +231 -0
- data/lib/conductor/http/api/prompt_resource_api.rb +81 -0
- data/lib/conductor/http/api/role_resource_api.rb +60 -0
- data/lib/conductor/http/api/scheduler_resource_api.rb +211 -0
- data/lib/conductor/http/api/schema_resource_api.rb +82 -0
- data/lib/conductor/http/api/secret_resource_api.rb +134 -0
- data/lib/conductor/http/api/task_resource_api.rb +321 -0
- data/lib/conductor/http/api/token_resource_api.rb +42 -0
- data/lib/conductor/http/api/user_resource_api.rb +59 -0
- data/lib/conductor/http/api/workflow_bulk_resource_api.rb +91 -0
- data/lib/conductor/http/api/workflow_resource_api.rb +451 -0
- data/lib/conductor/http/api_client.rb +437 -0
- data/lib/conductor/http/models/authentication_config.rb +67 -0
- data/lib/conductor/http/models/authorization_request.rb +39 -0
- data/lib/conductor/http/models/base_model.rb +162 -0
- data/lib/conductor/http/models/bulk_response.rb +39 -0
- data/lib/conductor/http/models/conductor_application.rb +39 -0
- data/lib/conductor/http/models/conductor_user.rb +53 -0
- data/lib/conductor/http/models/create_or_update_application_request.rb +24 -0
- data/lib/conductor/http/models/create_or_update_role_request.rb +27 -0
- data/lib/conductor/http/models/event_handler.rb +130 -0
- data/lib/conductor/http/models/generate_token_request.rb +27 -0
- data/lib/conductor/http/models/group.rb +36 -0
- data/lib/conductor/http/models/integration.rb +70 -0
- data/lib/conductor/http/models/integration_api.rb +53 -0
- data/lib/conductor/http/models/integration_api_update.rb +43 -0
- data/lib/conductor/http/models/integration_update.rb +36 -0
- data/lib/conductor/http/models/permission.rb +24 -0
- data/lib/conductor/http/models/poll_data.rb +33 -0
- data/lib/conductor/http/models/prompt_template.rb +59 -0
- data/lib/conductor/http/models/prompt_template_test_request.rb +43 -0
- data/lib/conductor/http/models/rerun_workflow_request.rb +37 -0
- data/lib/conductor/http/models/role.rb +27 -0
- data/lib/conductor/http/models/schema_def.rb +59 -0
- data/lib/conductor/http/models/search_result.rb +187 -0
- data/lib/conductor/http/models/skip_task_request.rb +27 -0
- data/lib/conductor/http/models/start_workflow_request.rb +68 -0
- data/lib/conductor/http/models/subject_ref.rb +35 -0
- data/lib/conductor/http/models/tag_object.rb +36 -0
- data/lib/conductor/http/models/target_ref.rb +39 -0
- data/lib/conductor/http/models/task.rb +156 -0
- data/lib/conductor/http/models/task_def.rb +95 -0
- data/lib/conductor/http/models/task_exec_log.rb +30 -0
- data/lib/conductor/http/models/task_result.rb +115 -0
- data/lib/conductor/http/models/task_result_status.rb +24 -0
- data/lib/conductor/http/models/token.rb +33 -0
- data/lib/conductor/http/models/upsert_group_request.rb +30 -0
- data/lib/conductor/http/models/upsert_user_request.rb +39 -0
- data/lib/conductor/http/models/workflow.rb +202 -0
- data/lib/conductor/http/models/workflow_def.rb +73 -0
- data/lib/conductor/http/models/workflow_schedule.rb +100 -0
- data/lib/conductor/http/models/workflow_state_update.rb +30 -0
- data/lib/conductor/http/models/workflow_status_constants.rb +57 -0
- data/lib/conductor/http/models/workflow_task.rb +169 -0
- data/lib/conductor/http/models/workflow_test_request.rb +67 -0
- data/lib/conductor/http/rest_client.rb +211 -0
- data/lib/conductor/orkes/models/access_key.rb +56 -0
- data/lib/conductor/orkes/models/granted_permission.rb +27 -0
- data/lib/conductor/orkes/models/metadata_tag.rb +15 -0
- data/lib/conductor/orkes/models/rate_limit_tag.rb +15 -0
- data/lib/conductor/orkes/orkes_clients.rb +69 -0
- data/lib/conductor/version.rb +5 -0
- data/lib/conductor/worker/events/conductor_event.rb +40 -0
- data/lib/conductor/worker/events/global_dispatcher.rb +37 -0
- data/lib/conductor/worker/events/http_events.rb +25 -0
- data/lib/conductor/worker/events/listener_registry.rb +40 -0
- data/lib/conductor/worker/events/listeners.rb +34 -0
- data/lib/conductor/worker/events/sync_event_dispatcher.rb +78 -0
- data/lib/conductor/worker/events/task_runner_events.rb +271 -0
- data/lib/conductor/worker/events/workflow_events.rb +49 -0
- data/lib/conductor/worker/fiber_executor.rb +532 -0
- data/lib/conductor/worker/ractor_task_runner.rb +501 -0
- data/lib/conductor/worker/task_context.rb +114 -0
- data/lib/conductor/worker/task_definition_registrar.rb +322 -0
- data/lib/conductor/worker/task_handler.rb +360 -0
- data/lib/conductor/worker/task_in_progress.rb +60 -0
- data/lib/conductor/worker/task_runner.rb +538 -0
- data/lib/conductor/worker/telemetry/metrics_collector.rb +196 -0
- data/lib/conductor/worker/telemetry/prometheus_backend.rb +224 -0
- data/lib/conductor/worker/worker.rb +355 -0
- data/lib/conductor/worker/worker_config.rb +154 -0
- data/lib/conductor/worker/worker_registry.rb +71 -0
- data/lib/conductor/workflow/dsl/input_ref.rb +37 -0
- data/lib/conductor/workflow/dsl/output_ref.rb +44 -0
- data/lib/conductor/workflow/dsl/parallel_builder.rb +49 -0
- data/lib/conductor/workflow/dsl/switch_builder.rb +74 -0
- data/lib/conductor/workflow/dsl/task_ref.rb +178 -0
- data/lib/conductor/workflow/dsl/workflow_builder.rb +1016 -0
- data/lib/conductor/workflow/dsl/workflow_definition.rb +150 -0
- data/lib/conductor/workflow/llm/chat_message.rb +47 -0
- data/lib/conductor/workflow/llm/embedding_model.rb +19 -0
- data/lib/conductor/workflow/llm/tool_call.rb +43 -0
- data/lib/conductor/workflow/llm/tool_spec.rb +46 -0
- data/lib/conductor/workflow/task_type.rb +68 -0
- data/lib/conductor/workflow/timeout_policy.rb +31 -0
- data/lib/conductor/workflow/workflow_executor.rb +373 -0
- data/lib/conductor.rb +192 -0
- metadata +359 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../http/models/task'
|
|
4
|
+
require_relative '../http/models/task_result'
|
|
5
|
+
require_relative '../http/models/task_result_status'
|
|
6
|
+
require_relative '../exceptions'
|
|
7
|
+
require_relative 'worker_registry'
|
|
8
|
+
require_relative 'task_in_progress'
|
|
9
|
+
|
|
10
|
+
module Conductor
|
|
11
|
+
module Worker
|
|
12
|
+
# Worker class that wraps an execute function
|
|
13
|
+
# Handles various return types and keyword argument mapping
|
|
14
|
+
class Worker
|
|
15
|
+
# @return [String] Task definition name in Conductor
|
|
16
|
+
attr_reader :task_definition_name
|
|
17
|
+
|
|
18
|
+
# @return [Proc, Method, nil] Function to execute tasks
|
|
19
|
+
attr_reader :execute_function
|
|
20
|
+
|
|
21
|
+
# Configuration attributes
|
|
22
|
+
attr_accessor :poll_interval, :thread_count, :domain, :worker_id,
|
|
23
|
+
:poll_timeout, :register_task_def, :overwrite_task_def,
|
|
24
|
+
:strict_schema, :paused, :isolation, :executor,
|
|
25
|
+
:task_def_template
|
|
26
|
+
|
|
27
|
+
# Default configuration values
|
|
28
|
+
DEFAULTS = {
|
|
29
|
+
poll_interval: 100, # milliseconds
|
|
30
|
+
thread_count: 1,
|
|
31
|
+
domain: nil,
|
|
32
|
+
worker_id: nil,
|
|
33
|
+
poll_timeout: 100, # milliseconds
|
|
34
|
+
register_task_def: false,
|
|
35
|
+
overwrite_task_def: true,
|
|
36
|
+
strict_schema: false,
|
|
37
|
+
paused: false,
|
|
38
|
+
isolation: :thread,
|
|
39
|
+
executor: :thread_pool
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
# Initialize a worker
|
|
43
|
+
# @param task_definition_name [String] Task definition name in Conductor
|
|
44
|
+
# @param execute_function [Proc, Method, nil] Function to execute tasks
|
|
45
|
+
# @param options [Hash] Worker configuration options
|
|
46
|
+
# @yield [task] Block to execute tasks (alternative to execute_function)
|
|
47
|
+
def initialize(task_definition_name, execute_function = nil, **options, &block)
|
|
48
|
+
@task_definition_name = task_definition_name
|
|
49
|
+
@execute_function = execute_function || block
|
|
50
|
+
|
|
51
|
+
raise ArgumentError, 'execute_function or block required' unless @execute_function
|
|
52
|
+
|
|
53
|
+
# Apply options with defaults
|
|
54
|
+
DEFAULTS.each do |key, default|
|
|
55
|
+
value = options.key?(key) ? options[key] : default
|
|
56
|
+
send("#{key}=", value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@task_def_template = options[:task_def_template]
|
|
60
|
+
|
|
61
|
+
# Analyze the execute function for parameter mapping
|
|
62
|
+
@takes_task_object = analyze_execute_function
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Alias for task_definition_name (compatibility)
|
|
66
|
+
def task_type
|
|
67
|
+
@task_definition_name
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Get polling interval in seconds
|
|
71
|
+
# @return [Float]
|
|
72
|
+
def polling_interval_seconds
|
|
73
|
+
@poll_interval / 1000.0
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Execute a task
|
|
77
|
+
# Handles keyword argument mapping and various return types
|
|
78
|
+
# @param task [Task] The task to execute
|
|
79
|
+
# @return [TaskResult, TaskInProgress] Execution result
|
|
80
|
+
def execute(task)
|
|
81
|
+
# Convert task if needed
|
|
82
|
+
task_obj = task.is_a?(Http::Models::Task) ? task : Http::Models::Task.from_hash(task)
|
|
83
|
+
|
|
84
|
+
# Call the execute function with appropriate arguments
|
|
85
|
+
output = call_execute_function(task_obj)
|
|
86
|
+
|
|
87
|
+
# Handle different return types
|
|
88
|
+
convert_output_to_result(output, task_obj)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Define a worker using a block
|
|
92
|
+
# Registers the worker in the global registry
|
|
93
|
+
# @param task_definition_name [String] Task definition name
|
|
94
|
+
# @param options [Hash] Worker configuration options
|
|
95
|
+
# @yield [task] Block to execute tasks
|
|
96
|
+
# @return [Worker] The created worker
|
|
97
|
+
def self.define(task_definition_name, **options, &block)
|
|
98
|
+
worker = new(task_definition_name, nil, **options, &block)
|
|
99
|
+
WorkerRegistry.register(task_definition_name, block, options)
|
|
100
|
+
worker
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
# Analyze the execute function to determine how to call it
|
|
106
|
+
# @return [Boolean] True if function takes a Task object directly
|
|
107
|
+
def analyze_execute_function
|
|
108
|
+
return true unless @execute_function.respond_to?(:parameters)
|
|
109
|
+
|
|
110
|
+
params = @execute_function.parameters
|
|
111
|
+
return true if params.empty?
|
|
112
|
+
|
|
113
|
+
# Check if first parameter is a positional arg (likely task)
|
|
114
|
+
first_param_type, first_param_name = params.first
|
|
115
|
+
|
|
116
|
+
# If it's a positional arg (required or optional), assume it takes task
|
|
117
|
+
return true if %i[req opt rest].include?(first_param_type)
|
|
118
|
+
|
|
119
|
+
# If it's a keyword arg named 'task', it takes task
|
|
120
|
+
return true if first_param_name == :task && %i[keyreq key].include?(first_param_type)
|
|
121
|
+
|
|
122
|
+
# Otherwise, it uses keyword args from input_data
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Call the execute function with appropriate arguments
|
|
127
|
+
# @param task [Task] The task object
|
|
128
|
+
# @return [Object] Raw output from the execute function
|
|
129
|
+
def call_execute_function(task)
|
|
130
|
+
if @takes_task_object
|
|
131
|
+
# Pass the full task object
|
|
132
|
+
@execute_function.call(task)
|
|
133
|
+
else
|
|
134
|
+
# Map input_data to keyword arguments
|
|
135
|
+
kwargs = extract_keyword_args(task)
|
|
136
|
+
@execute_function.call(**kwargs)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Extract keyword arguments from task input_data
|
|
141
|
+
# @param task [Task] The task object
|
|
142
|
+
# @return [Hash] Keyword arguments
|
|
143
|
+
def extract_keyword_args(task)
|
|
144
|
+
input_data = task.input_data || {}
|
|
145
|
+
kwargs = {}
|
|
146
|
+
|
|
147
|
+
return kwargs unless @execute_function.respond_to?(:parameters)
|
|
148
|
+
|
|
149
|
+
@execute_function.parameters.each do |type, name|
|
|
150
|
+
key = name.to_s
|
|
151
|
+
sym_key = name.to_sym
|
|
152
|
+
|
|
153
|
+
case type
|
|
154
|
+
when :keyreq # Required keyword arg
|
|
155
|
+
kwargs[sym_key] = if input_data.key?(key)
|
|
156
|
+
input_data[key]
|
|
157
|
+
elsif input_data.key?(sym_key)
|
|
158
|
+
input_data[sym_key]
|
|
159
|
+
end
|
|
160
|
+
when :key # Optional keyword arg
|
|
161
|
+
if input_data.key?(key)
|
|
162
|
+
kwargs[sym_key] = input_data[key]
|
|
163
|
+
elsif input_data.key?(sym_key)
|
|
164
|
+
kwargs[sym_key] = input_data[sym_key]
|
|
165
|
+
end
|
|
166
|
+
# Don't include if not in input_data (use default)
|
|
167
|
+
when :keyrest # **kwargs
|
|
168
|
+
# Include all remaining input_data
|
|
169
|
+
input_data.each do |k, v|
|
|
170
|
+
kwargs[k.to_sym] = v unless kwargs.key?(k.to_sym)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
kwargs
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Convert execute function output to TaskResult
|
|
179
|
+
# @param output [Object] Raw output from execute function
|
|
180
|
+
# @param task [Task] The task object
|
|
181
|
+
# @return [TaskResult, TaskInProgress]
|
|
182
|
+
def convert_output_to_result(output, task)
|
|
183
|
+
task_result = case output
|
|
184
|
+
when Http::Models::TaskResult
|
|
185
|
+
output
|
|
186
|
+
when TaskInProgress
|
|
187
|
+
result = Http::Models::TaskResult.in_progress
|
|
188
|
+
result.callback_after_seconds = output.callback_after_seconds
|
|
189
|
+
result.output_data = output.output if output.output
|
|
190
|
+
result
|
|
191
|
+
when Hash
|
|
192
|
+
result = Http::Models::TaskResult.complete
|
|
193
|
+
result.output_data = output
|
|
194
|
+
result
|
|
195
|
+
when true
|
|
196
|
+
Http::Models::TaskResult.complete
|
|
197
|
+
when false
|
|
198
|
+
Http::Models::TaskResult.failed('Worker returned false')
|
|
199
|
+
when nil
|
|
200
|
+
Http::Models::TaskResult.complete
|
|
201
|
+
else
|
|
202
|
+
result = Http::Models::TaskResult.complete
|
|
203
|
+
result.output_data = { 'result' => output }
|
|
204
|
+
result
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Set task identifiers
|
|
208
|
+
task_result.task_id = task.task_id
|
|
209
|
+
task_result.workflow_instance_id = task.workflow_instance_id
|
|
210
|
+
|
|
211
|
+
task_result
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Mixin module for class-based workers
|
|
216
|
+
# Include this in your worker class to use the worker_task DSL
|
|
217
|
+
#
|
|
218
|
+
# @example
|
|
219
|
+
# class MyWorker
|
|
220
|
+
# include Conductor::Worker::WorkerMixin
|
|
221
|
+
#
|
|
222
|
+
# worker_task 'my_task', poll_interval: 200, thread_count: 5
|
|
223
|
+
#
|
|
224
|
+
# def execute(task)
|
|
225
|
+
# { result: 'success' }
|
|
226
|
+
# end
|
|
227
|
+
# end
|
|
228
|
+
module WorkerMixin
|
|
229
|
+
def self.included(base)
|
|
230
|
+
base.extend(ClassMethods)
|
|
231
|
+
base.class_eval do
|
|
232
|
+
attr_accessor :poll_interval, :thread_count, :domain, :worker_id,
|
|
233
|
+
:poll_timeout, :register_task_def, :overwrite_task_def,
|
|
234
|
+
:strict_schema, :paused, :isolation, :executor
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
module ClassMethods
|
|
239
|
+
# Define a worker for a specific task type
|
|
240
|
+
# @param task_definition_name [String] Task definition name
|
|
241
|
+
# @param options [Hash] Worker options
|
|
242
|
+
def worker_task(task_definition_name, **options)
|
|
243
|
+
@task_definition_name = task_definition_name
|
|
244
|
+
@worker_options = options
|
|
245
|
+
|
|
246
|
+
# Apply defaults
|
|
247
|
+
Worker::DEFAULTS.each do |key, default|
|
|
248
|
+
instance_variable_set("@#{key}", options.fetch(key, default))
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# @return [String] Task definition name
|
|
253
|
+
def task_definition_name
|
|
254
|
+
@task_definition_name
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Alias for compatibility
|
|
258
|
+
def task_type
|
|
259
|
+
@task_definition_name
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# @return [Hash] Worker options
|
|
263
|
+
def worker_options
|
|
264
|
+
@worker_options || {}
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Configuration readers
|
|
268
|
+
Worker::DEFAULTS.each_key do |key|
|
|
269
|
+
define_method(key) do
|
|
270
|
+
instance_variable_get("@#{key}") || Worker::DEFAULTS[key]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Execute the task - must be overridden by worker class
|
|
276
|
+
# @param task [Task] The task to execute
|
|
277
|
+
# @return [TaskResult, Hash, Boolean, nil] Execution result
|
|
278
|
+
def execute(task)
|
|
279
|
+
raise NotImplementedError, 'Worker must implement #execute method'
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Get polling interval in seconds
|
|
283
|
+
# @return [Float]
|
|
284
|
+
def polling_interval_seconds
|
|
285
|
+
(self.class.poll_interval || Worker::DEFAULTS[:poll_interval]) / 1000.0
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Convenience method to get task input data
|
|
289
|
+
# @param task [Task] The task
|
|
290
|
+
# @return [Hash] Input data
|
|
291
|
+
def get_input_data(task)
|
|
292
|
+
task.input_data || {}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Convenience method to get a specific input value
|
|
296
|
+
# @param task [Task] The task
|
|
297
|
+
# @param key [String, Symbol] The input key
|
|
298
|
+
# @param default [Object] Default value if key not found
|
|
299
|
+
# @return [Object] The input value
|
|
300
|
+
def get_input(task, key, default = nil)
|
|
301
|
+
get_input_data(task)[key.to_s] || default
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Module for method annotation style workers
|
|
306
|
+
# Extend this in your module to use worker_task on methods
|
|
307
|
+
#
|
|
308
|
+
# @example
|
|
309
|
+
# module MyWorkers
|
|
310
|
+
# extend Conductor::Worker::Annotatable
|
|
311
|
+
#
|
|
312
|
+
# worker_task 'greet_user', poll_interval: 100
|
|
313
|
+
# def self.greet(name:, greeting: 'Hello')
|
|
314
|
+
# "#{greeting}, #{name}!"
|
|
315
|
+
# end
|
|
316
|
+
# end
|
|
317
|
+
module Annotatable
|
|
318
|
+
# Mark the next defined method as a worker
|
|
319
|
+
# @param task_definition_name [String] Task definition name
|
|
320
|
+
# @param options [Hash] Worker options
|
|
321
|
+
def worker_task(task_definition_name, **options)
|
|
322
|
+
@pending_worker_task = {
|
|
323
|
+
task_definition_name: task_definition_name,
|
|
324
|
+
options: options
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Hook called when a method is added
|
|
329
|
+
def singleton_method_added(method_name)
|
|
330
|
+
super
|
|
331
|
+
return unless @pending_worker_task
|
|
332
|
+
|
|
333
|
+
pending = @pending_worker_task
|
|
334
|
+
@pending_worker_task = nil
|
|
335
|
+
|
|
336
|
+
# Get the method and register it
|
|
337
|
+
method_obj = method(method_name)
|
|
338
|
+
WorkerRegistry.register(
|
|
339
|
+
pending[:task_definition_name],
|
|
340
|
+
method_obj,
|
|
341
|
+
pending[:options]
|
|
342
|
+
)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Module-level worker_task for defining workers at the top level
|
|
347
|
+
# @param task_definition_name [String] Task definition name
|
|
348
|
+
# @param options [Hash] Worker options
|
|
349
|
+
# @yield [task] Block to execute tasks
|
|
350
|
+
# @return [Worker] The created worker
|
|
351
|
+
def self.worker_task(task_definition_name, **options, &block)
|
|
352
|
+
Worker.define(task_definition_name, **options, &block)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
|
|
5
|
+
module Conductor
|
|
6
|
+
module Worker
|
|
7
|
+
# Configuration resolver for workers with 3-tier hierarchy
|
|
8
|
+
# Priority (highest to lowest):
|
|
9
|
+
# 1. Worker-specific env var: conductor.worker.{task_name}.{property}
|
|
10
|
+
# 2. Global worker env var: conductor.worker.all.{property}
|
|
11
|
+
# 3. Code-level default from worker definition
|
|
12
|
+
class WorkerConfig
|
|
13
|
+
# Configuration properties with their types and default values
|
|
14
|
+
PROPERTIES = {
|
|
15
|
+
poll_interval: { type: :integer, default: 100 }, # milliseconds
|
|
16
|
+
thread_count: { type: :integer, default: 1 },
|
|
17
|
+
domain: { type: :string, default: nil },
|
|
18
|
+
worker_id: { type: :string, default: nil }, # auto-generated if nil
|
|
19
|
+
poll_timeout: { type: :integer, default: 100 }, # milliseconds
|
|
20
|
+
register_task_def: { type: :boolean, default: false },
|
|
21
|
+
overwrite_task_def: { type: :boolean, default: true },
|
|
22
|
+
strict_schema: { type: :boolean, default: false },
|
|
23
|
+
paused: { type: :boolean, default: false },
|
|
24
|
+
isolation: { type: :symbol, default: :thread }, # :thread or :ractor
|
|
25
|
+
executor: { type: :symbol, default: :thread_pool } # :thread_pool or :fiber
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
class << self
|
|
29
|
+
# Resolve configuration for a worker
|
|
30
|
+
# @param worker_name [String] Task definition name
|
|
31
|
+
# @param defaults [Hash] Code-level defaults from worker definition
|
|
32
|
+
# @return [Hash] Resolved configuration
|
|
33
|
+
def resolve(worker_name, defaults = {})
|
|
34
|
+
result = {}
|
|
35
|
+
|
|
36
|
+
PROPERTIES.each do |property, config|
|
|
37
|
+
# Try to get from environment variables (3-tier hierarchy)
|
|
38
|
+
env_value = get_env_value(worker_name, property)
|
|
39
|
+
|
|
40
|
+
result[property] = if env_value
|
|
41
|
+
convert_value(env_value, config[:type])
|
|
42
|
+
elsif defaults.key?(property)
|
|
43
|
+
defaults[property]
|
|
44
|
+
else
|
|
45
|
+
config[:default]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Auto-generate worker_id if not set
|
|
50
|
+
result[:worker_id] ||= generate_worker_id
|
|
51
|
+
|
|
52
|
+
result
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Generate a unique worker ID
|
|
56
|
+
# @return [String]
|
|
57
|
+
def generate_worker_id
|
|
58
|
+
hostname = begin
|
|
59
|
+
Socket.gethostname
|
|
60
|
+
rescue StandardError
|
|
61
|
+
'unknown'
|
|
62
|
+
end
|
|
63
|
+
pid = Process.pid
|
|
64
|
+
thread_id = Thread.current.object_id.to_s(16)
|
|
65
|
+
"#{hostname}-#{pid}-#{thread_id}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Get a value from environment variables using the 3-tier hierarchy
|
|
71
|
+
# @param worker_name [String] Task definition name
|
|
72
|
+
# @param property [Symbol] Property name
|
|
73
|
+
# @return [String, nil] Value from environment or nil
|
|
74
|
+
def get_env_value(worker_name, property)
|
|
75
|
+
property_str = property.to_s
|
|
76
|
+
worker_name_normalized = normalize_worker_name(worker_name)
|
|
77
|
+
|
|
78
|
+
# Priority 1: Worker-specific env vars
|
|
79
|
+
# conductor.worker.{task_name}.{property} (dotted format)
|
|
80
|
+
value = ENV.fetch("conductor.worker.#{worker_name}.#{property_str}", nil)
|
|
81
|
+
return value if value
|
|
82
|
+
|
|
83
|
+
# CONDUCTOR_WORKER_{TASK_NAME}_{PROPERTY} (uppercase format)
|
|
84
|
+
value = ENV.fetch("CONDUCTOR_WORKER_#{worker_name_normalized}_#{property_str.upcase}", nil)
|
|
85
|
+
return value if value
|
|
86
|
+
|
|
87
|
+
# Priority 2: Global worker env vars
|
|
88
|
+
# conductor.worker.all.{property} (dotted format)
|
|
89
|
+
value = ENV.fetch("conductor.worker.all.#{property_str}", nil)
|
|
90
|
+
return value if value
|
|
91
|
+
|
|
92
|
+
# CONDUCTOR_WORKER_ALL_{PROPERTY} (uppercase format)
|
|
93
|
+
value = ENV.fetch("CONDUCTOR_WORKER_ALL_#{property_str.upcase}", nil)
|
|
94
|
+
return value if value
|
|
95
|
+
|
|
96
|
+
# Priority 3: Legacy format
|
|
97
|
+
# CONDUCTOR_WORKER_{PROPERTY} (old global format)
|
|
98
|
+
value = ENV.fetch("CONDUCTOR_WORKER_#{property_str.upcase}", nil)
|
|
99
|
+
return value if value
|
|
100
|
+
|
|
101
|
+
# Special backward compatibility for poll_interval
|
|
102
|
+
if property == :poll_interval
|
|
103
|
+
value = ENV.fetch('POLLING_INTERVAL', nil)
|
|
104
|
+
return value if value
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Normalize worker name for environment variable lookup
|
|
111
|
+
# Converts task names like "my-task" to "MY_TASK"
|
|
112
|
+
# @param name [String] Worker name
|
|
113
|
+
# @return [String] Normalized name
|
|
114
|
+
def normalize_worker_name(name)
|
|
115
|
+
name.to_s.gsub(/[^a-zA-Z0-9]/, '_').upcase
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Convert a string value to the appropriate type
|
|
119
|
+
# @param value [String] String value from environment
|
|
120
|
+
# @param type [Symbol] Target type (:integer, :boolean, :string, :symbol)
|
|
121
|
+
# @return [Object] Converted value
|
|
122
|
+
def convert_value(value, type)
|
|
123
|
+
case type
|
|
124
|
+
when :integer
|
|
125
|
+
value.to_i
|
|
126
|
+
when :boolean
|
|
127
|
+
parse_boolean(value)
|
|
128
|
+
when :symbol
|
|
129
|
+
value.to_sym
|
|
130
|
+
when :string
|
|
131
|
+
value
|
|
132
|
+
else
|
|
133
|
+
value
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Parse a boolean value from string
|
|
138
|
+
# Accepts: true/1/yes and false/0/no (case-insensitive)
|
|
139
|
+
# @param value [String] String value
|
|
140
|
+
# @return [Boolean]
|
|
141
|
+
def parse_boolean(value)
|
|
142
|
+
case value.to_s.downcase
|
|
143
|
+
when 'true', '1', 'yes'
|
|
144
|
+
true
|
|
145
|
+
when 'false', '0', 'no'
|
|
146
|
+
false
|
|
147
|
+
else
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conductor
|
|
4
|
+
module Worker
|
|
5
|
+
# Global registry for workers defined via worker_task DSL
|
|
6
|
+
# Workers are registered when their defining code is loaded
|
|
7
|
+
class WorkerRegistry
|
|
8
|
+
class << self
|
|
9
|
+
# Register a worker definition
|
|
10
|
+
# @param task_definition_name [String] Task definition name
|
|
11
|
+
# @param execute_function [Proc, Method] Function to execute tasks
|
|
12
|
+
# @param options [Hash] Worker configuration options
|
|
13
|
+
# @return [void]
|
|
14
|
+
def register(task_definition_name, execute_function, options = {})
|
|
15
|
+
key = [task_definition_name, options[:domain]]
|
|
16
|
+
registry[key] = {
|
|
17
|
+
task_definition_name: task_definition_name,
|
|
18
|
+
execute_function: execute_function,
|
|
19
|
+
options: options
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Get all registered worker definitions
|
|
24
|
+
# @return [Array<Hash>] Array of worker definition hashes
|
|
25
|
+
def all
|
|
26
|
+
registry.values
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Get a specific worker definition
|
|
30
|
+
# @param task_definition_name [String] Task definition name
|
|
31
|
+
# @param domain [String, nil] Optional domain
|
|
32
|
+
# @return [Hash, nil] Worker definition or nil
|
|
33
|
+
def get(task_definition_name, domain: nil)
|
|
34
|
+
registry[[task_definition_name, domain]]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if a worker is registered
|
|
38
|
+
# @param task_definition_name [String] Task definition name
|
|
39
|
+
# @param domain [String, nil] Optional domain
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def registered?(task_definition_name, domain: nil)
|
|
42
|
+
registry.key?([task_definition_name, domain])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Clear all registered workers (primarily for testing)
|
|
46
|
+
# @return [void]
|
|
47
|
+
def clear
|
|
48
|
+
@registry = {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get the count of registered workers
|
|
52
|
+
# @return [Integer]
|
|
53
|
+
def count
|
|
54
|
+
registry.size
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get all registered task definition names
|
|
58
|
+
# @return [Array<String>]
|
|
59
|
+
def task_names
|
|
60
|
+
registry.keys.map(&:first).uniq
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def registry
|
|
66
|
+
@registry ||= {}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conductor
|
|
4
|
+
module Workflow
|
|
5
|
+
module Dsl
|
|
6
|
+
# InputRef provides access to workflow inputs and variables
|
|
7
|
+
# wf[:order_id] => "${workflow.input.order_id}"
|
|
8
|
+
# wf.var(:counter) => "${workflow.variables.counter}"
|
|
9
|
+
class InputRef
|
|
10
|
+
# Access workflow input by field name
|
|
11
|
+
# @param field [String, Symbol] The input field name
|
|
12
|
+
# @return [OutputRef] An OutputRef pointing to the workflow input
|
|
13
|
+
def [](field)
|
|
14
|
+
OutputRef.new("workflow.input.#{field}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Access workflow variable by name
|
|
18
|
+
# @param name [String, Symbol] The variable name
|
|
19
|
+
# @return [OutputRef] An OutputRef pointing to the workflow variable
|
|
20
|
+
def var(name)
|
|
21
|
+
OutputRef.new("workflow.variables.#{name}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Access workflow output (for sub-workflows)
|
|
25
|
+
# @param field [String, Symbol, nil] Optional field name
|
|
26
|
+
# @return [OutputRef] An OutputRef pointing to workflow output
|
|
27
|
+
def output(field = nil)
|
|
28
|
+
if field
|
|
29
|
+
OutputRef.new("workflow.output.#{field}")
|
|
30
|
+
else
|
|
31
|
+
OutputRef.new('workflow.output')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Conductor
|
|
4
|
+
module Workflow
|
|
5
|
+
module Dsl
|
|
6
|
+
# OutputRef enables chained [] access for nested output paths
|
|
7
|
+
# task[:response][:body][:items] => "${task_ref.output.response.body.items}"
|
|
8
|
+
class OutputRef
|
|
9
|
+
attr_reader :path
|
|
10
|
+
|
|
11
|
+
def initialize(path)
|
|
12
|
+
@path = path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Enable chained [] access
|
|
16
|
+
# @param field [String, Symbol] The field name
|
|
17
|
+
# @return [OutputRef] A new OutputRef with the extended path
|
|
18
|
+
def [](field)
|
|
19
|
+
OutputRef.new("#{@path}.#{field}")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Convert to expression string for use in input parameters
|
|
23
|
+
# @return [String] The expression in ${...} format
|
|
24
|
+
def to_s
|
|
25
|
+
"${#{@path}}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Allow use in string interpolation
|
|
29
|
+
alias to_str to_s
|
|
30
|
+
|
|
31
|
+
# Compare OutputRefs by their paths
|
|
32
|
+
def ==(other)
|
|
33
|
+
other.is_a?(OutputRef) && @path == other.path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias eql? ==
|
|
37
|
+
|
|
38
|
+
def hash
|
|
39
|
+
@path.hash
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|