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,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