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,177 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Dynamic Workflow Example
|
|
5
|
+
# =========================
|
|
6
|
+
#
|
|
7
|
+
# Demonstrates creating and executing workflows at runtime without pre-registration.
|
|
8
|
+
#
|
|
9
|
+
# What it does:
|
|
10
|
+
# -------------
|
|
11
|
+
# - Creates a workflow programmatically using Ruby code
|
|
12
|
+
# - Defines two workers: get_user_email and send_email
|
|
13
|
+
# - Chains tasks together using the >> operator
|
|
14
|
+
# - Executes the workflow with input data
|
|
15
|
+
#
|
|
16
|
+
# Use Cases:
|
|
17
|
+
# ----------
|
|
18
|
+
# - Workflows that cannot be defined statically (structure depends on runtime data)
|
|
19
|
+
# - Programmatic workflow generation based on business rules
|
|
20
|
+
# - Testing workflows without registering definitions
|
|
21
|
+
# - Rapid prototyping and development
|
|
22
|
+
#
|
|
23
|
+
# Key Concepts:
|
|
24
|
+
# -------------
|
|
25
|
+
# - ConductorWorkflow: Build workflows in code
|
|
26
|
+
# - Task chaining: Use >> operator to define task sequence
|
|
27
|
+
# - Dynamic execution: Create and run workflows on-the-fly
|
|
28
|
+
# - Worker tasks: Ruby methods/blocks that execute task logic
|
|
29
|
+
#
|
|
30
|
+
# Usage:
|
|
31
|
+
# bundle exec ruby examples/dynamic_workflow.rb
|
|
32
|
+
#
|
|
33
|
+
# Prerequisites:
|
|
34
|
+
# - Conductor server running (set CONDUCTOR_SERVER_URL)
|
|
35
|
+
# - For Orkes: Set CONDUCTOR_AUTH_KEY and CONDUCTOR_AUTH_SECRET
|
|
36
|
+
|
|
37
|
+
require_relative '../lib/conductor'
|
|
38
|
+
|
|
39
|
+
# Include workflow DSL for shorter class names
|
|
40
|
+
include Conductor::Workflow
|
|
41
|
+
|
|
42
|
+
# ============================================================================
|
|
43
|
+
# WORKERS - Define task implementations
|
|
44
|
+
# ============================================================================
|
|
45
|
+
|
|
46
|
+
# Worker 1: Get user email by user ID
|
|
47
|
+
class GetUserEmailWorker
|
|
48
|
+
include Conductor::Worker::WorkerModule
|
|
49
|
+
|
|
50
|
+
worker_task 'get_user_email'
|
|
51
|
+
|
|
52
|
+
def execute(task)
|
|
53
|
+
userid = get_input(task, 'userid', 'unknown')
|
|
54
|
+
email = "#{userid}@example.com"
|
|
55
|
+
|
|
56
|
+
puts "[GetUserEmailWorker] Generated email for userid=#{userid}: #{email}"
|
|
57
|
+
|
|
58
|
+
# Return the email as output
|
|
59
|
+
{ 'result' => email }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Worker 2: Send email
|
|
64
|
+
class SendEmailWorker
|
|
65
|
+
include Conductor::Worker::WorkerModule
|
|
66
|
+
|
|
67
|
+
worker_task 'send_email'
|
|
68
|
+
|
|
69
|
+
def execute(task)
|
|
70
|
+
email = get_input(task, 'email', '')
|
|
71
|
+
subject = get_input(task, 'subject', 'No Subject')
|
|
72
|
+
body = get_input(task, 'body', '')
|
|
73
|
+
|
|
74
|
+
puts "[SendEmailWorker] Sending email to #{email}"
|
|
75
|
+
puts " Subject: #{subject}"
|
|
76
|
+
puts " Body: #{body}"
|
|
77
|
+
|
|
78
|
+
# Simulate sending email
|
|
79
|
+
{ 'status' => 'sent', 'to' => email }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def main
|
|
84
|
+
# Configuration from environment variables
|
|
85
|
+
# CONDUCTOR_SERVER_URL: Conductor server URL (e.g., https://developer.orkescloud.com/api)
|
|
86
|
+
# CONDUCTOR_AUTH_KEY: API Authentication Key (optional for OSS)
|
|
87
|
+
# CONDUCTOR_AUTH_SECRET: API Auth Secret (optional for OSS)
|
|
88
|
+
config = Conductor::Configuration.new
|
|
89
|
+
|
|
90
|
+
puts '=' * 70
|
|
91
|
+
puts 'Conductor Ruby SDK - Dynamic Workflow Example'
|
|
92
|
+
puts '=' * 70
|
|
93
|
+
puts
|
|
94
|
+
puts "Server: #{config.server_url}"
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
# Create clients using OrkesClients factory
|
|
98
|
+
clients = Conductor::Orkes::OrkesClients.new(config)
|
|
99
|
+
workflow_executor = clients.get_workflow_executor
|
|
100
|
+
|
|
101
|
+
# Start workers in the background
|
|
102
|
+
task_handler = Conductor::Worker::TaskRunner.new(config)
|
|
103
|
+
task_handler.register_worker(GetUserEmailWorker.new)
|
|
104
|
+
task_handler.register_worker(SendEmailWorker.new)
|
|
105
|
+
task_handler.start
|
|
106
|
+
|
|
107
|
+
puts 'Workers started...'
|
|
108
|
+
puts
|
|
109
|
+
|
|
110
|
+
# ============================================================================
|
|
111
|
+
# BUILD WORKFLOW DYNAMICALLY
|
|
112
|
+
# ============================================================================
|
|
113
|
+
|
|
114
|
+
# Create workflow with executor for dynamic execution
|
|
115
|
+
workflow = ConductorWorkflow.new(
|
|
116
|
+
clients.get_workflow_client,
|
|
117
|
+
'dynamic_workflow_ruby',
|
|
118
|
+
version: 1,
|
|
119
|
+
executor: workflow_executor
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Define tasks
|
|
123
|
+
# Task 1: Get user email - uses workflow input for userid
|
|
124
|
+
get_email = SimpleTask.new('get_user_email', 'get_user_email_ref')
|
|
125
|
+
.input('userid', workflow.input('userid'))
|
|
126
|
+
|
|
127
|
+
# Task 2: Send email - uses output from get_email task
|
|
128
|
+
sendmail = SimpleTask.new('send_email', 'send_email_ref')
|
|
129
|
+
.input('email', get_email.output('result'))
|
|
130
|
+
.input('subject', 'Hello from Conductor Ruby SDK')
|
|
131
|
+
.input('body', 'This is a test email from a dynamic workflow')
|
|
132
|
+
|
|
133
|
+
# Chain tasks: workflow >> task1 >> task2
|
|
134
|
+
workflow >> get_email >> sendmail
|
|
135
|
+
|
|
136
|
+
# Configure the output of the workflow
|
|
137
|
+
workflow.output_parameter('email', get_email.output('result'))
|
|
138
|
+
|
|
139
|
+
# ============================================================================
|
|
140
|
+
# EXECUTE WORKFLOW
|
|
141
|
+
# ============================================================================
|
|
142
|
+
|
|
143
|
+
puts 'Executing dynamic workflow...'
|
|
144
|
+
puts
|
|
145
|
+
|
|
146
|
+
# Execute workflow synchronously with input
|
|
147
|
+
workflow_run = workflow.execute(
|
|
148
|
+
input: { 'userid' => 'user_a' },
|
|
149
|
+
wait_for_seconds: 30
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
puts
|
|
153
|
+
puts 'Workflow completed!'
|
|
154
|
+
puts '-' * 70
|
|
155
|
+
puts "Workflow ID: #{workflow_run.workflow_id}"
|
|
156
|
+
puts "Status: #{workflow_run.status}"
|
|
157
|
+
puts "Output: #{workflow_run.output.inspect}"
|
|
158
|
+
puts
|
|
159
|
+
puts "Check the workflow execution at: #{config.ui_host}/execution/#{workflow_run.workflow_id}"
|
|
160
|
+
|
|
161
|
+
# Stop workers
|
|
162
|
+
task_handler.stop
|
|
163
|
+
puts
|
|
164
|
+
puts 'Workers stopped. Goodbye!'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if __FILE__ == $PROGRAM_NAME
|
|
168
|
+
begin
|
|
169
|
+
main
|
|
170
|
+
rescue Conductor::ApiError => e
|
|
171
|
+
puts "API Error: #{e.message}"
|
|
172
|
+
puts e.backtrace.first(5).join("\n")
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
puts "Error: #{e.message}"
|
|
175
|
+
puts e.backtrace.first(5).join("\n")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Event Handler Example
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates creating and managing event handlers that trigger
|
|
7
|
+
# workflows based on external events.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bundle exec ruby examples/event_handler.rb
|
|
11
|
+
|
|
12
|
+
require_relative '../lib/conductor'
|
|
13
|
+
|
|
14
|
+
def main
|
|
15
|
+
config = Conductor::Configuration.new
|
|
16
|
+
api_client = Conductor::Http::ApiClient.new(configuration: config)
|
|
17
|
+
event_api = Conductor::Http::Api::EventResourceApi.new(api_client)
|
|
18
|
+
|
|
19
|
+
puts '=' * 70
|
|
20
|
+
puts 'Event Handler Example'
|
|
21
|
+
puts '=' * 70
|
|
22
|
+
puts "Server: #{config.server_url}"
|
|
23
|
+
puts
|
|
24
|
+
|
|
25
|
+
handler_name = "ruby_order_event_handler_#{Time.now.to_i}"
|
|
26
|
+
|
|
27
|
+
# Create an event handler
|
|
28
|
+
event_handler = {
|
|
29
|
+
'name' => handler_name,
|
|
30
|
+
'event' => 'order:created',
|
|
31
|
+
'actions' => [
|
|
32
|
+
{
|
|
33
|
+
'action' => 'start_workflow',
|
|
34
|
+
'start_workflow' => {
|
|
35
|
+
'name' => 'process_order_workflow',
|
|
36
|
+
'version' => 1,
|
|
37
|
+
'input' => {
|
|
38
|
+
'orderId' => '${event.orderId}',
|
|
39
|
+
'customerId' => '${event.customerId}'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
'active' => true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
# Add event handler
|
|
49
|
+
puts "Creating event handler: #{handler_name}"
|
|
50
|
+
event_api.add_event_handler(event_handler)
|
|
51
|
+
puts 'Event handler created!'
|
|
52
|
+
|
|
53
|
+
# Get all event handlers
|
|
54
|
+
handlers = event_api.get_event_handlers
|
|
55
|
+
puts "\nAll event handlers (#{handlers.length} total):"
|
|
56
|
+
handlers.first(5).each do |h|
|
|
57
|
+
name = h.is_a?(Hash) ? h['name'] : h.name
|
|
58
|
+
puts " - #{name}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get handlers for specific event
|
|
62
|
+
puts "\nHandlers for 'order:created' event:"
|
|
63
|
+
begin
|
|
64
|
+
order_handlers = event_api.get_event_handlers_for_event('order:created', true)
|
|
65
|
+
order_handlers.each do |h|
|
|
66
|
+
name = h.is_a?(Hash) ? h['name'] : h.name
|
|
67
|
+
puts " - #{name}"
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
puts " Could not fetch: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
ensure
|
|
73
|
+
# Cleanup
|
|
74
|
+
puts "\nDeleting event handler..."
|
|
75
|
+
begin
|
|
76
|
+
event_api.remove_event_handler(handler_name)
|
|
77
|
+
puts 'Event handler deleted'
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
puts "Could not delete: #{e.message}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
puts "\nEvent handler example complete!"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if __FILE__ == $PROGRAM_NAME
|
|
87
|
+
begin
|
|
88
|
+
main
|
|
89
|
+
rescue Conductor::ApiError => e
|
|
90
|
+
puts "API Error: #{e.message}"
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
puts "Error: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Reusable event listener examples for TaskRunnerEventsListener.
|
|
5
|
+
#
|
|
6
|
+
# This module provides example event listener implementations that can be used
|
|
7
|
+
# in any application to monitor and track task execution.
|
|
8
|
+
#
|
|
9
|
+
# Available Listeners:
|
|
10
|
+
# - TaskExecutionLogger: Simple logging of all task lifecycle events
|
|
11
|
+
# - TaskTimingTracker: Statistical tracking of task execution times
|
|
12
|
+
# - DistributedTracingListener: Simulated distributed tracing integration
|
|
13
|
+
# - ErrorTrackingListener: Error aggregation and alerting
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# require_relative 'event_listener_examples'
|
|
17
|
+
#
|
|
18
|
+
# handler = Conductor::Worker::TaskHandler.new(
|
|
19
|
+
# configuration: config,
|
|
20
|
+
# event_listeners: [
|
|
21
|
+
# TaskExecutionLogger.new,
|
|
22
|
+
# TaskTimingTracker.new
|
|
23
|
+
# ]
|
|
24
|
+
# )
|
|
25
|
+
# handler.start
|
|
26
|
+
# handler.join
|
|
27
|
+
#
|
|
28
|
+
|
|
29
|
+
require 'logger'
|
|
30
|
+
require 'conductor'
|
|
31
|
+
|
|
32
|
+
# Simple listener that logs all task execution events.
|
|
33
|
+
#
|
|
34
|
+
# Demonstrates basic pre/post processing:
|
|
35
|
+
# - on_task_execution_started: Pre-processing before task executes
|
|
36
|
+
# - on_task_execution_completed: Post-processing after successful execution
|
|
37
|
+
# - on_task_execution_failure: Error handling after failed execution
|
|
38
|
+
#
|
|
39
|
+
class TaskExecutionLogger
|
|
40
|
+
def initialize(logger: nil)
|
|
41
|
+
@logger = logger || Logger.new($stdout)
|
|
42
|
+
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
43
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- #{msg}\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Called before task execution begins (pre-processing).
|
|
48
|
+
#
|
|
49
|
+
# Use this for:
|
|
50
|
+
# - Setting up context (tracing, logging context)
|
|
51
|
+
# - Validating preconditions
|
|
52
|
+
# - Starting timers
|
|
53
|
+
# - Recording audit events
|
|
54
|
+
#
|
|
55
|
+
def on_task_execution_started(event)
|
|
56
|
+
@logger.info("[PRE] Starting task '#{event.task_type}' " \
|
|
57
|
+
"(task_id=#{event.task_id}, worker=#{event.worker_id})")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Called after task execution completes successfully (post-processing).
|
|
61
|
+
#
|
|
62
|
+
# Use this for:
|
|
63
|
+
# - Logging results
|
|
64
|
+
# - Sending notifications
|
|
65
|
+
# - Updating external systems
|
|
66
|
+
# - Recording metrics
|
|
67
|
+
#
|
|
68
|
+
def on_task_execution_completed(event)
|
|
69
|
+
@logger.info("[POST] Completed task '#{event.task_type}' " \
|
|
70
|
+
"(task_id=#{event.task_id}, duration=#{event.duration_ms.round(2)}ms, " \
|
|
71
|
+
"output_size=#{event.output_size_bytes || 0} bytes)")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Called when task execution fails (error handling).
|
|
75
|
+
#
|
|
76
|
+
# Use this for:
|
|
77
|
+
# - Error logging
|
|
78
|
+
# - Alerting
|
|
79
|
+
# - Retry logic
|
|
80
|
+
# - Cleanup operations
|
|
81
|
+
#
|
|
82
|
+
def on_task_execution_failure(event)
|
|
83
|
+
@logger.error("[ERROR] Failed task '#{event.task_type}' " \
|
|
84
|
+
"(task_id=#{event.task_id}, duration=#{event.duration_ms.round(2)}ms, " \
|
|
85
|
+
"error=#{event.cause.class}: #{event.cause.message})")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Called when polling for tasks begins.
|
|
89
|
+
def on_poll_started(event)
|
|
90
|
+
@logger.debug("Polling for '#{event.task_type}' tasks (poll_count=#{event.poll_count})")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Called when polling completes successfully.
|
|
94
|
+
def on_poll_completed(event)
|
|
95
|
+
return unless event.tasks_received.positive?
|
|
96
|
+
|
|
97
|
+
@logger.debug("Received #{event.tasks_received} '#{event.task_type}' tasks " \
|
|
98
|
+
"in #{event.duration_ms.round(2)}ms")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Called when polling fails.
|
|
102
|
+
def on_poll_failure(event)
|
|
103
|
+
@logger.warn("Poll failed for '#{event.task_type}': #{event.cause.message}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Called when task update fails after all retries (CRITICAL).
|
|
107
|
+
def on_task_update_failure(event)
|
|
108
|
+
@logger.fatal("[CRITICAL] Task result LOST for '#{event.task_type}' " \
|
|
109
|
+
"(task_id=#{event.task_id}, retries=#{event.retry_count})")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Advanced listener that tracks task execution times and provides statistics.
|
|
114
|
+
#
|
|
115
|
+
# Demonstrates:
|
|
116
|
+
# - Stateful event processing
|
|
117
|
+
# - Aggregating data across multiple events
|
|
118
|
+
# - Custom business logic in listeners
|
|
119
|
+
#
|
|
120
|
+
class TaskTimingTracker
|
|
121
|
+
attr_reader :task_times, :task_errors
|
|
122
|
+
|
|
123
|
+
def initialize(logger: nil, report_interval: 10)
|
|
124
|
+
@logger = logger || Logger.new($stdout)
|
|
125
|
+
@task_times = Hash.new { |h, k| h[k] = [] }
|
|
126
|
+
@task_errors = Hash.new(0)
|
|
127
|
+
@report_interval = report_interval
|
|
128
|
+
@mutex = Mutex.new
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Track successful task execution times.
|
|
132
|
+
def on_task_execution_completed(event)
|
|
133
|
+
@mutex.synchronize do
|
|
134
|
+
@task_times[event.task_type] << event.duration_ms
|
|
135
|
+
|
|
136
|
+
# Print stats every N completions
|
|
137
|
+
count = @task_times[event.task_type].size
|
|
138
|
+
return unless (count % @report_interval).zero?
|
|
139
|
+
|
|
140
|
+
durations = @task_times[event.task_type]
|
|
141
|
+
avg = durations.sum / durations.size
|
|
142
|
+
min_time = durations.min
|
|
143
|
+
max_time = durations.max
|
|
144
|
+
|
|
145
|
+
@logger.info("Stats for '#{event.task_type}': " \
|
|
146
|
+
"count=#{count}, avg=#{avg.round(2)}ms, " \
|
|
147
|
+
"min=#{min_time.round(2)}ms, max=#{max_time.round(2)}ms")
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Track task failures.
|
|
152
|
+
def on_task_execution_failure(event)
|
|
153
|
+
@mutex.synchronize do
|
|
154
|
+
@task_errors[event.task_type] += 1
|
|
155
|
+
@logger.warn("Task '#{event.task_type}' has failed #{@task_errors[event.task_type]} times")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Get statistics for a task type.
|
|
160
|
+
# @param task_type [String] Task type name
|
|
161
|
+
# @return [Hash] Statistics including count, avg, min, max, error_count
|
|
162
|
+
def stats_for(task_type)
|
|
163
|
+
@mutex.synchronize do
|
|
164
|
+
durations = @task_times[task_type]
|
|
165
|
+
return nil if durations.empty?
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
count: durations.size,
|
|
169
|
+
avg_ms: durations.sum / durations.size,
|
|
170
|
+
min_ms: durations.min,
|
|
171
|
+
max_ms: durations.max,
|
|
172
|
+
error_count: @task_errors[task_type]
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Get all statistics.
|
|
178
|
+
# @return [Hash] Statistics for all task types
|
|
179
|
+
def all_stats
|
|
180
|
+
@mutex.synchronize do
|
|
181
|
+
@task_times.keys.each_with_object({}) do |task_type, result|
|
|
182
|
+
durations = @task_times[task_type]
|
|
183
|
+
result[task_type] = {
|
|
184
|
+
count: durations.size,
|
|
185
|
+
avg_ms: durations.sum / durations.size,
|
|
186
|
+
min_ms: durations.min,
|
|
187
|
+
max_ms: durations.max,
|
|
188
|
+
error_count: @task_errors[task_type]
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Example listener for distributed tracing integration.
|
|
196
|
+
#
|
|
197
|
+
# Demonstrates how to:
|
|
198
|
+
# - Generate trace IDs
|
|
199
|
+
# - Propagate trace context
|
|
200
|
+
# - Create spans for task execution
|
|
201
|
+
#
|
|
202
|
+
# In production, replace the logging with actual tracing library calls
|
|
203
|
+
# (OpenTelemetry, Jaeger, Zipkin, etc.)
|
|
204
|
+
#
|
|
205
|
+
class DistributedTracingListener
|
|
206
|
+
def initialize(logger: nil)
|
|
207
|
+
@logger = logger || Logger.new($stdout)
|
|
208
|
+
@active_traces = {}
|
|
209
|
+
@mutex = Mutex.new
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Start a trace span when task execution begins.
|
|
213
|
+
def on_task_execution_started(event)
|
|
214
|
+
trace_id = "trace-#{event.task_id[0..7]}"
|
|
215
|
+
span_id = "span-#{event.task_id[0..7]}"
|
|
216
|
+
|
|
217
|
+
@mutex.synchronize do
|
|
218
|
+
@active_traces[event.task_id] = {
|
|
219
|
+
trace_id: trace_id,
|
|
220
|
+
span_id: span_id,
|
|
221
|
+
start_time: Time.now,
|
|
222
|
+
task_type: event.task_type
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
@logger.info("[TRACE] Started span: trace_id=#{trace_id}, span_id=#{span_id}, " \
|
|
227
|
+
"task_type=#{event.task_type}")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# End the trace span when task execution completes.
|
|
231
|
+
def on_task_execution_completed(event)
|
|
232
|
+
trace_info = @mutex.synchronize { @active_traces.delete(event.task_id) }
|
|
233
|
+
return unless trace_info
|
|
234
|
+
|
|
235
|
+
duration = (Time.now - trace_info[:start_time]) * 1000
|
|
236
|
+
|
|
237
|
+
@logger.info("[TRACE] Completed span: trace_id=#{trace_info[:trace_id]}, " \
|
|
238
|
+
"span_id=#{trace_info[:span_id]}, duration=#{duration.round(2)}ms, status=SUCCESS")
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Mark the trace span as failed.
|
|
242
|
+
def on_task_execution_failure(event)
|
|
243
|
+
trace_info = @mutex.synchronize { @active_traces.delete(event.task_id) }
|
|
244
|
+
return unless trace_info
|
|
245
|
+
|
|
246
|
+
duration = (Time.now - trace_info[:start_time]) * 1000
|
|
247
|
+
|
|
248
|
+
@logger.info("[TRACE] Failed span: trace_id=#{trace_info[:trace_id]}, " \
|
|
249
|
+
"span_id=#{trace_info[:span_id]}, duration=#{duration.round(2)}ms, " \
|
|
250
|
+
"status=ERROR, error=#{event.cause.class}")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Error tracking listener for aggregating and alerting on errors.
|
|
255
|
+
#
|
|
256
|
+
# Demonstrates:
|
|
257
|
+
# - Error aggregation by type
|
|
258
|
+
# - Threshold-based alerting
|
|
259
|
+
# - Integration with external error tracking services
|
|
260
|
+
#
|
|
261
|
+
class ErrorTrackingListener
|
|
262
|
+
def initialize(logger: nil, alert_threshold: 5, alert_callback: nil)
|
|
263
|
+
@logger = logger || Logger.new($stdout)
|
|
264
|
+
@alert_threshold = alert_threshold
|
|
265
|
+
@alert_callback = alert_callback
|
|
266
|
+
@errors = Hash.new { |h, k| h[k] = [] }
|
|
267
|
+
@mutex = Mutex.new
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def on_task_execution_failure(event)
|
|
271
|
+
@mutex.synchronize do
|
|
272
|
+
error_key = "#{event.task_type}:#{event.cause.class}"
|
|
273
|
+
@errors[error_key] << {
|
|
274
|
+
task_id: event.task_id,
|
|
275
|
+
workflow_instance_id: event.workflow_instance_id,
|
|
276
|
+
message: event.cause.message,
|
|
277
|
+
is_retryable: event.is_retryable,
|
|
278
|
+
timestamp: event.timestamp
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# Check if threshold exceeded
|
|
282
|
+
recent_errors = @errors[error_key].select do |e|
|
|
283
|
+
e[:timestamp] > Time.now - 300 # Last 5 minutes
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
trigger_alert(event.task_type, event.cause.class.to_s, recent_errors.size) if recent_errors.size >= @alert_threshold
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def on_task_update_failure(event)
|
|
291
|
+
@logger.fatal("[ALERT] CRITICAL: Task result lost! task_id=#{event.task_id}, " \
|
|
292
|
+
"task_type=#{event.task_type}, retries=#{event.retry_count}")
|
|
293
|
+
|
|
294
|
+
# Always alert on task update failures
|
|
295
|
+
return unless @alert_callback
|
|
296
|
+
|
|
297
|
+
@alert_callback.call(
|
|
298
|
+
type: :task_update_failure,
|
|
299
|
+
severity: :critical,
|
|
300
|
+
task_id: event.task_id,
|
|
301
|
+
task_type: event.task_type,
|
|
302
|
+
retry_count: event.retry_count
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Get error summary.
|
|
307
|
+
# @return [Hash] Error counts by task_type:error_class
|
|
308
|
+
def error_summary
|
|
309
|
+
@mutex.synchronize do
|
|
310
|
+
@errors.transform_values(&:size)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
|
|
316
|
+
def trigger_alert(task_type, error_class, count)
|
|
317
|
+
@logger.warn("[ALERT] High error rate: #{task_type} has #{count} " \
|
|
318
|
+
"#{error_class} errors in last 5 minutes")
|
|
319
|
+
|
|
320
|
+
return unless @alert_callback
|
|
321
|
+
|
|
322
|
+
@alert_callback.call(
|
|
323
|
+
type: :high_error_rate,
|
|
324
|
+
severity: :warning,
|
|
325
|
+
task_type: task_type,
|
|
326
|
+
error_class: error_class,
|
|
327
|
+
count: count
|
|
328
|
+
)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# SLA monitoring listener that alerts when tasks exceed duration thresholds.
|
|
333
|
+
#
|
|
334
|
+
# Demonstrates:
|
|
335
|
+
# - Configuration-driven thresholds
|
|
336
|
+
# - Percentage-based alerting
|
|
337
|
+
# - P99 latency tracking
|
|
338
|
+
#
|
|
339
|
+
class SLAMonitorListener
|
|
340
|
+
def initialize(thresholds: {}, logger: nil, alert_callback: nil)
|
|
341
|
+
@thresholds = thresholds # { 'task_type' => max_duration_ms }
|
|
342
|
+
@logger = logger || Logger.new($stdout)
|
|
343
|
+
@alert_callback = alert_callback
|
|
344
|
+
@violations = Hash.new { |h, k| h[k] = [] }
|
|
345
|
+
@mutex = Mutex.new
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def on_task_execution_completed(event)
|
|
349
|
+
threshold = @thresholds[event.task_type]
|
|
350
|
+
return unless threshold && event.duration_ms > threshold
|
|
351
|
+
|
|
352
|
+
@mutex.synchronize do
|
|
353
|
+
@violations[event.task_type] << {
|
|
354
|
+
task_id: event.task_id,
|
|
355
|
+
duration_ms: event.duration_ms,
|
|
356
|
+
threshold_ms: threshold,
|
|
357
|
+
timestamp: event.timestamp
|
|
358
|
+
}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
@logger.warn("[SLA] Violation: '#{event.task_type}' took #{event.duration_ms.round(2)}ms " \
|
|
362
|
+
"(threshold: #{threshold}ms)")
|
|
363
|
+
|
|
364
|
+
return unless @alert_callback
|
|
365
|
+
|
|
366
|
+
@alert_callback.call(
|
|
367
|
+
type: :sla_violation,
|
|
368
|
+
task_type: event.task_type,
|
|
369
|
+
task_id: event.task_id,
|
|
370
|
+
duration_ms: event.duration_ms,
|
|
371
|
+
threshold_ms: threshold
|
|
372
|
+
)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Get SLA violation summary.
|
|
376
|
+
# @return [Hash] Violation counts by task type
|
|
377
|
+
def violation_summary
|
|
378
|
+
@mutex.synchronize do
|
|
379
|
+
@violations.transform_values(&:size)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Get recent violations for a task type.
|
|
384
|
+
# @param task_type [String] Task type name
|
|
385
|
+
# @param limit [Integer] Maximum number of violations to return
|
|
386
|
+
# @return [Array<Hash>] Recent violations
|
|
387
|
+
def recent_violations(task_type, limit: 10)
|
|
388
|
+
@mutex.synchronize do
|
|
389
|
+
@violations[task_type].last(limit)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Example showing how to run with event listeners (for documentation)
|
|
395
|
+
if __FILE__ == $PROGRAM_NAME
|
|
396
|
+
puts <<~USAGE
|
|
397
|
+
Event Listener Examples
|
|
398
|
+
|
|
399
|
+
This file provides reusable event listener implementations.
|
|
400
|
+
Import them in your application:
|
|
401
|
+
|
|
402
|
+
require_relative 'event_listener_examples'
|
|
403
|
+
|
|
404
|
+
# Create listeners
|
|
405
|
+
logger_listener = TaskExecutionLogger.new
|
|
406
|
+
timing_tracker = TaskTimingTracker.new
|
|
407
|
+
tracing_listener = DistributedTracingListener.new
|
|
408
|
+
|
|
409
|
+
# Use with TaskHandler
|
|
410
|
+
handler = Conductor::Worker::TaskHandler.new(
|
|
411
|
+
configuration: config,
|
|
412
|
+
event_listeners: [
|
|
413
|
+
logger_listener,
|
|
414
|
+
timing_tracker,
|
|
415
|
+
tracing_listener
|
|
416
|
+
]
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
handler.start
|
|
420
|
+
handler.join
|
|
421
|
+
|
|
422
|
+
Available Listeners:
|
|
423
|
+
- TaskExecutionLogger: Logs all task lifecycle events
|
|
424
|
+
- TaskTimingTracker: Tracks execution statistics
|
|
425
|
+
- DistributedTracingListener: Simulates distributed tracing
|
|
426
|
+
- ErrorTrackingListener: Aggregates errors and alerts
|
|
427
|
+
- SLAMonitorListener: Monitors SLA violations
|
|
428
|
+
|
|
429
|
+
USAGE
|
|
430
|
+
end
|