tasker-rb 0.1.1

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/DEVELOPMENT.md +548 -0
  3. data/README.md +87 -0
  4. data/ext/tasker_core/Cargo.lock +4720 -0
  5. data/ext/tasker_core/Cargo.toml +76 -0
  6. data/ext/tasker_core/extconf.rb +38 -0
  7. data/ext/tasker_core/src/CLAUDE.md +7 -0
  8. data/ext/tasker_core/src/bootstrap.rs +320 -0
  9. data/ext/tasker_core/src/bridge.rs +400 -0
  10. data/ext/tasker_core/src/client_ffi.rs +173 -0
  11. data/ext/tasker_core/src/conversions.rs +131 -0
  12. data/ext/tasker_core/src/diagnostics.rs +57 -0
  13. data/ext/tasker_core/src/event_handler.rs +179 -0
  14. data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
  15. data/ext/tasker_core/src/ffi_logging.rs +245 -0
  16. data/ext/tasker_core/src/global_event_system.rs +16 -0
  17. data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
  18. data/ext/tasker_core/src/lib.rs +41 -0
  19. data/ext/tasker_core/src/observability_ffi.rs +339 -0
  20. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  21. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  22. data/lib/tasker_core/bootstrap.rb +394 -0
  23. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  24. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  25. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  26. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  27. data/lib/tasker_core/domain_events.rb +43 -0
  28. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  29. data/lib/tasker_core/errors/common.rb +305 -0
  30. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  31. data/lib/tasker_core/errors.rb +4 -0
  32. data/lib/tasker_core/event_bridge.rb +330 -0
  33. data/lib/tasker_core/handlers.rb +159 -0
  34. data/lib/tasker_core/internal.rb +31 -0
  35. data/lib/tasker_core/logger.rb +234 -0
  36. data/lib/tasker_core/models.rb +337 -0
  37. data/lib/tasker_core/observability/types.rb +158 -0
  38. data/lib/tasker_core/observability.rb +292 -0
  39. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  40. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  41. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  42. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  43. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  44. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  45. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  46. data/lib/tasker_core/registry/resolvers.rb +42 -0
  47. data/lib/tasker_core/registry.rb +12 -0
  48. data/lib/tasker_core/step_handler/api.rb +48 -0
  49. data/lib/tasker_core/step_handler/base.rb +354 -0
  50. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  51. data/lib/tasker_core/step_handler/decision.rb +53 -0
  52. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  53. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  54. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  55. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  56. data/lib/tasker_core/subscriber.rb +212 -0
  57. data/lib/tasker_core/task_handler/base.rb +254 -0
  58. data/lib/tasker_core/tasker_rb.so +0 -0
  59. data/lib/tasker_core/template_discovery.rb +181 -0
  60. data/lib/tasker_core/tracing.rb +166 -0
  61. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  62. data/lib/tasker_core/types/client_types.rb +145 -0
  63. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  64. data/lib/tasker_core/types/error_types.rb +72 -0
  65. data/lib/tasker_core/types/simple_message.rb +151 -0
  66. data/lib/tasker_core/types/step_context.rb +328 -0
  67. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  68. data/lib/tasker_core/types/step_message.rb +112 -0
  69. data/lib/tasker_core/types/step_types.rb +207 -0
  70. data/lib/tasker_core/types/task_template.rb +240 -0
  71. data/lib/tasker_core/types/task_types.rb +148 -0
  72. data/lib/tasker_core/types.rb +132 -0
  73. data/lib/tasker_core/version.rb +13 -0
  74. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  75. data/lib/tasker_core/worker/event_poller.rb +224 -0
  76. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  77. data/lib/tasker_core.rb +160 -0
  78. metadata +322 -0
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module Worker
5
+ # EventPoller polls for step execution events from Rust via FFI
6
+ #
7
+ # Solves the cross-thread communication issue between Rust and Ruby by
8
+ # actively polling the Rust worker for pending events. Events are forwarded
9
+ # to EventBridge for normal processing.
10
+ #
11
+ # The poller runs in a dedicated background thread, continuously checking
12
+ # for new events from the Rust worker at a configurable interval (default 10ms).
13
+ # This approach allows Ruby to control the thread context and safely process
14
+ # events without running into Ruby's Global Interpreter Lock (GIL) limitations.
15
+ #
16
+ # @example Starting the poller
17
+ # poller = TaskerCore::Worker::EventPoller.instance
18
+ # poller.start!
19
+ # # => Spawns polling thread with 10ms interval
20
+ #
21
+ # # Check if poller started successfully
22
+ # if poller.active?
23
+ # puts "Event poller is running"
24
+ # end
25
+ #
26
+ # @example Checking poller status
27
+ # poller.active?
28
+ # # => true/false
29
+ #
30
+ # if poller.active? && poller.polling_thread&.alive?
31
+ # puts "Poller thread is healthy"
32
+ # end
33
+ #
34
+ # @example Stopping the poller
35
+ # poller.stop!
36
+ # # => Gracefully stops polling thread (max 5 second wait)
37
+ #
38
+ # # Poller will wait for current poll to complete
39
+ # # and then cleanly shutdown
40
+ #
41
+ # Threading Model:
42
+ # - **Main Thread**: Ruby application, handler execution
43
+ # - **Polling Thread**: Dedicated background thread for event polling
44
+ # - **Rust Threads**: Rust worker runtime (separate from Ruby)
45
+ #
46
+ # The polling thread:
47
+ # 1. Calls `TaskerCore::FFI.poll_step_events` to check for events
48
+ # 2. If event found, forwards to EventBridge for processing
49
+ # 3. If no event, sleeps for POLL_INTERVAL
50
+ # 4. On error, logs and sleeps for 10x POLL_INTERVAL
51
+ #
52
+ # Why Polling is Necessary:
53
+ #
54
+ # Ruby's GIL prevents Rust from directly calling Ruby methods from Rust threads.
55
+ # Direct FFI callbacks from Rust to Ruby would either:
56
+ # - Cause segfaults (unsafe thread access)
57
+ # - Block indefinitely (GIL contention)
58
+ # - Require complex unsafe code
59
+ #
60
+ # Polling allows:
61
+ # - Ruby to control thread context
62
+ # - Safe event processing in Ruby thread
63
+ # - Simple, reliable cross-language communication
64
+ # - Predictable performance characteristics
65
+ #
66
+ # Performance Characteristics:
67
+ # - **Poll Interval**: 10ms (0.01 seconds) - configurable via POLL_INTERVAL
68
+ # - **Max Latency**: ~10ms from event generation to processing start
69
+ # - **CPU Usage**: Minimal (yields during sleep)
70
+ # - **Error Recovery**: Automatic with exponential backoff (10x interval)
71
+ #
72
+ # Event Flow:
73
+ # 1. Rust worker detects step ready for execution
74
+ # 2. Rust queues event in internal event queue
75
+ # 3. EventPoller calls poll_step_events via FFI
76
+ # 4. Rust returns next event (or nil if queue empty)
77
+ # 5. EventPoller forwards event to EventBridge
78
+ # 6. EventBridge publishes to Ruby subscribers
79
+ # 7. StepExecutionSubscriber executes handler
80
+ #
81
+ # @see TaskerCore::Worker::EventBridge For event processing
82
+ # @see TaskerCore::FFI For Rust FFI operations
83
+ # @see POLL_INTERVAL For polling frequency configuration
84
+ class EventPoller
85
+ include Singleton
86
+
87
+ attr_reader :logger, :active, :polling_thread
88
+
89
+ # Polling interval in seconds (10ms)
90
+ # Lower values reduce latency but increase CPU usage
91
+ # Higher values reduce CPU usage but increase latency
92
+ POLL_INTERVAL = 0.01
93
+
94
+ # TAS-67 Phase 2: Check for starvation warnings every N poll iterations
95
+ # At 10ms poll interval, 100 iterations = 1 second between starvation checks
96
+ STARVATION_CHECK_INTERVAL = 100
97
+
98
+ def initialize
99
+ @logger = TaskerCore::Logger.instance
100
+ @active = false
101
+ @polling_thread = nil
102
+ @poll_count = 0 # TAS-67 Phase 2: Counter for starvation check interval
103
+ end
104
+
105
+ # Start polling for events from Rust
106
+ def start!
107
+ return if @active
108
+
109
+ @active = true
110
+ logger.info 'Starting EventPoller - polling for step execution events from Rust'
111
+
112
+ @polling_thread = Thread.new do
113
+ poll_events_loop
114
+ end
115
+
116
+ logger.info '✅ EventPoller started successfully'
117
+ end
118
+
119
+ # Stop polling
120
+ def stop!
121
+ return unless @active
122
+
123
+ logger.info 'Stopping EventPoller...'
124
+ @active = false
125
+
126
+ if @polling_thread&.alive?
127
+ @polling_thread.join(5.0) # Wait up to 5 seconds for thread to finish
128
+ @polling_thread.kill if @polling_thread.alive? # Force kill if still running
129
+ end
130
+
131
+ @polling_thread = nil
132
+ logger.info '✅ EventPoller stopped'
133
+ end
134
+
135
+ # Check if poller is active
136
+ def active?
137
+ @active
138
+ end
139
+
140
+ # TAS-67 Phase 2: Get FFI dispatch channel metrics for monitoring
141
+ #
142
+ # Returns a hash with the following keys:
143
+ # - :pending_count - Number of events waiting for completion
144
+ # - :oldest_pending_age_ms - Age of the oldest pending event in milliseconds
145
+ # - :newest_pending_age_ms - Age of the newest pending event in milliseconds
146
+ # - :oldest_event_id - UUID of the oldest pending event (for debugging)
147
+ # - :starvation_detected - Boolean indicating if any events exceed starvation threshold
148
+ # - :starving_event_count - Number of events exceeding starvation threshold
149
+ #
150
+ # @return [Hash] FFI dispatch metrics, or empty hash if worker not running
151
+ # @example Check for starvation
152
+ # metrics = EventPoller.instance.ffi_dispatch_metrics
153
+ # if metrics[:starvation_detected]
154
+ # logger.warn "Starvation detected: #{metrics[:starving_event_count]} events waiting"
155
+ # end
156
+ def ffi_dispatch_metrics
157
+ TaskerCore::FFI.get_ffi_dispatch_metrics
158
+ rescue StandardError => e
159
+ logger.debug "Failed to get FFI dispatch metrics: #{e.message}"
160
+ {}
161
+ end
162
+
163
+ private
164
+
165
+ # Main polling loop - runs in dedicated thread
166
+ def poll_events_loop
167
+ logger.debug 'EventPoller: Starting poll loop'
168
+
169
+ while @active
170
+ begin
171
+ # TAS-67 Phase 2: Periodically check for starvation warnings
172
+ @poll_count += 1
173
+ check_starvation_periodically
174
+
175
+ # Poll for next event from Rust via FFI
176
+ event_data = TaskerCore::FFI.poll_step_events
177
+
178
+ if event_data.nil?
179
+ # No events available, sleep briefly
180
+ sleep(POLL_INTERVAL)
181
+ next
182
+ end
183
+
184
+ # Process the event through EventBridge
185
+ process_event(event_data)
186
+ rescue StandardError => e
187
+ logger.error "EventPoller error: #{e.message}"
188
+ logger.error e.backtrace.join("\n")
189
+
190
+ # Sleep longer on error to avoid tight error loops
191
+ sleep(POLL_INTERVAL * 10) if @active
192
+ end
193
+ end
194
+
195
+ logger.debug 'EventPoller: Poll loop terminated'
196
+ end
197
+
198
+ # TAS-67 Phase 2: Check for starvation warnings periodically
199
+ # This triggers Rust-side logging of any events exceeding the starvation threshold
200
+ def check_starvation_periodically
201
+ return unless (@poll_count % STARVATION_CHECK_INTERVAL).zero?
202
+
203
+ TaskerCore::FFI.check_starvation_warnings
204
+ rescue StandardError => e
205
+ # Don't let starvation check errors affect polling
206
+ logger.debug "Starvation check error (non-fatal): #{e.message}"
207
+ end
208
+
209
+ # Process a polled event through the EventBridge
210
+ def process_event(event_data)
211
+ logger.debug "EventPoller: Processing event from Rust: #{event_data}"
212
+
213
+ # Forward to EventBridge for normal processing
214
+ EventBridge.instance.publish_step_execution(event_data)
215
+
216
+ logger.debug 'EventPoller: Event forwarded to EventBridge successfully'
217
+ rescue StandardError => e
218
+ logger.error "EventPoller: Failed to process event: #{e.message}"
219
+ logger.error e.backtrace.join("\n")
220
+ raise
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,271 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module TaskerCore
6
+ module Worker
7
+ # TAS-65 Phase 4.2: In-Process Domain Event Poller
8
+ #
9
+ # Polls for fast domain events (delivery_mode: fast) from the Rust in-process
10
+ # event bus via FFI. Ruby handlers can subscribe to specific event patterns
11
+ # for integration purposes (Sentry, DataDog, Slack, etc.).
12
+ #
13
+ # This follows the same polling pattern as EventPoller but for domain events
14
+ # rather than step execution events. Events flow through a broadcast channel
15
+ # from Rust to Ruby.
16
+ #
17
+ # Event Flow:
18
+ # 1. Step handler (Rust or Ruby) publishes domain event with delivery_mode: fast
19
+ # 2. EventRouter routes to InProcessEventBus
20
+ # 3. InProcessEventBus broadcasts to all subscribers (including FFI channel)
21
+ # 4. This poller calls poll_in_process_events via FFI
22
+ # 5. Events are dispatched to registered Ruby handlers based on pattern matching
23
+ #
24
+ # @example Starting the poller
25
+ # poller = TaskerCore::Worker::InProcessDomainEventPoller.instance
26
+ # poller.start!
27
+ #
28
+ # @example Subscribing to specific event patterns
29
+ # poller.subscribe('payment.*') do |event|
30
+ # puts "Payment event: #{event[:event_name]}"
31
+ # # Forward to Sentry, DataDog, etc.
32
+ # end
33
+ #
34
+ # @example Subscribing to all events
35
+ # poller.subscribe('*') do |event|
36
+ # MyMetricsCollector.record(event)
37
+ # end
38
+ #
39
+ # @example Stopping the poller
40
+ # poller.stop!
41
+ class InProcessDomainEventPoller
42
+ include Singleton
43
+
44
+ attr_reader :logger, :active, :polling_thread
45
+
46
+ # Polling interval in seconds (50ms - longer than step events since these are async notifications)
47
+ POLL_INTERVAL = 0.05
48
+
49
+ # Maximum events to poll per iteration
50
+ MAX_EVENTS_PER_POLL = 10
51
+
52
+ def initialize
53
+ @logger = TaskerCore::Logger.instance
54
+ @active = false
55
+ @polling_thread = nil
56
+ @subscribers = {} # pattern => array of handlers
57
+ @mutex = Mutex.new
58
+ end
59
+
60
+ # Subscribe a handler to an event pattern
61
+ #
62
+ # @param pattern [String] Event pattern (exact, wildcard 'payment.*', or global '*')
63
+ # @yield [event] Block called when matching event is received
64
+ # @yieldparam event [Hash] Domain event data
65
+ # @return [void]
66
+ #
67
+ # @example
68
+ # poller.subscribe('payment.processed') { |event| puts event[:event_name] }
69
+ # poller.subscribe('order.*') { |event| notify_slack(event) }
70
+ # poller.subscribe('*') { |event| log_all_events(event) }
71
+ def subscribe(pattern, &handler)
72
+ raise ArgumentError, 'Block required for subscription' unless block_given?
73
+ raise ArgumentError, 'Pattern cannot be empty' if pattern.nil? || pattern.empty?
74
+
75
+ @mutex.synchronize do
76
+ @subscribers[pattern] ||= []
77
+ @subscribers[pattern] << handler
78
+ end
79
+
80
+ logger.info "InProcessDomainEventPoller: Subscribed to pattern '#{pattern}'"
81
+ end
82
+
83
+ # Unsubscribe all handlers for a pattern
84
+ #
85
+ # @param pattern [String] Event pattern to unsubscribe
86
+ # @return [void]
87
+ def unsubscribe(pattern)
88
+ @mutex.synchronize do
89
+ @subscribers.delete(pattern)
90
+ end
91
+
92
+ logger.info "InProcessDomainEventPoller: Unsubscribed from pattern '#{pattern}'"
93
+ end
94
+
95
+ # Clear all subscriptions
96
+ def clear_subscriptions!
97
+ @mutex.synchronize do
98
+ @subscribers.clear
99
+ end
100
+
101
+ logger.info 'InProcessDomainEventPoller: Cleared all subscriptions'
102
+ end
103
+
104
+ # Get count of registered subscribers
105
+ def subscriber_count
106
+ @mutex.synchronize do
107
+ @subscribers.values.flatten.size
108
+ end
109
+ end
110
+
111
+ # Start polling for in-process domain events
112
+ def start!
113
+ return if @active
114
+
115
+ @active = true
116
+ logger.info 'Starting InProcessDomainEventPoller - polling for fast domain events from Rust'
117
+
118
+ @polling_thread = Thread.new do
119
+ poll_events_loop
120
+ end
121
+
122
+ logger.info '✅ InProcessDomainEventPoller started successfully'
123
+ end
124
+
125
+ # Stop polling
126
+ def stop!
127
+ return unless @active
128
+
129
+ logger.info 'Stopping InProcessDomainEventPoller...'
130
+ @active = false
131
+
132
+ if @polling_thread&.alive?
133
+ @polling_thread.join(5.0) # Wait up to 5 seconds for thread to finish
134
+ @polling_thread.kill if @polling_thread.alive? # Force kill if still running
135
+ end
136
+
137
+ @polling_thread = nil
138
+ logger.info '✅ InProcessDomainEventPoller stopped'
139
+ end
140
+
141
+ # Check if poller is active
142
+ def active?
143
+ @active
144
+ end
145
+
146
+ # Get statistics about the poller
147
+ def stats
148
+ ffi_stats = begin
149
+ TaskerCore::FFI.in_process_event_stats
150
+ rescue StandardError => e
151
+ logger.warn "Failed to get FFI stats: #{e.message}"
152
+ { enabled: false, status: 'unknown' }
153
+ end
154
+
155
+ {
156
+ active: @active,
157
+ subscriber_count: subscriber_count,
158
+ patterns: @subscribers.keys,
159
+ ffi: ffi_stats
160
+ }
161
+ end
162
+
163
+ private
164
+
165
+ # Main polling loop - runs in dedicated thread
166
+ def poll_events_loop
167
+ logger.debug 'InProcessDomainEventPoller: Starting poll loop'
168
+
169
+ while @active
170
+ begin
171
+ # Poll for events from Rust via FFI
172
+ events = TaskerCore::FFI.poll_in_process_events(MAX_EVENTS_PER_POLL)
173
+
174
+ if events.nil? || events.empty?
175
+ # No events available, sleep briefly
176
+ sleep(POLL_INTERVAL)
177
+ next
178
+ end
179
+
180
+ # Process each event through pattern matching
181
+ events.each do |event_data|
182
+ dispatch_event(event_data)
183
+ end
184
+ rescue StandardError => e
185
+ logger.error "InProcessDomainEventPoller error: #{e.message}"
186
+ logger.error e.backtrace.first(5).join("\n")
187
+
188
+ # Sleep longer on error to avoid tight error loops
189
+ sleep(POLL_INTERVAL * 10) if @active
190
+ end
191
+ end
192
+
193
+ logger.debug 'InProcessDomainEventPoller: Poll loop terminated'
194
+ end
195
+
196
+ # Dispatch event to matching subscribers
197
+ def dispatch_event(event_data)
198
+ event_name = event_data[:event_name] || event_data['event_name']
199
+ return unless event_name
200
+
201
+ logger.debug "InProcessDomainEventPoller: Dispatching event '#{event_name}'"
202
+
203
+ # Parse business_payload if it's a JSON string
204
+ if event_data[:business_payload].is_a?(String)
205
+ begin
206
+ event_data[:business_payload] = JSON.parse(event_data[:business_payload])
207
+ rescue JSON::ParserError => e
208
+ logger.warn "Failed to parse business_payload JSON: #{e.message}"
209
+ end
210
+ end
211
+
212
+ # Parse error if it's a JSON string
213
+ if event_data.dig(:execution_result, :error).is_a?(String)
214
+ begin
215
+ event_data[:execution_result][:error] = JSON.parse(event_data[:execution_result][:error])
216
+ rescue JSON::ParserError => e
217
+ logger.warn "Failed to parse error JSON: #{e.message}"
218
+ end
219
+ end
220
+
221
+ # Find and invoke matching handlers
222
+ handlers = find_matching_handlers(event_name)
223
+
224
+ if handlers.empty?
225
+ logger.debug "InProcessDomainEventPoller: No handlers for event '#{event_name}'"
226
+ return
227
+ end
228
+
229
+ handlers.each do |handler|
230
+ invoke_handler_safely(handler, event_data, event_name)
231
+ end
232
+ end
233
+
234
+ # Find handlers that match the event name
235
+ def find_matching_handlers(event_name)
236
+ handlers = []
237
+
238
+ @mutex.synchronize do
239
+ @subscribers.each do |pattern, pattern_handlers|
240
+ handlers.concat(pattern_handlers) if matches_pattern?(event_name, pattern)
241
+ end
242
+ end
243
+
244
+ handlers
245
+ end
246
+
247
+ # Check if event name matches pattern
248
+ def matches_pattern?(event_name, pattern)
249
+ return true if pattern == '*' # Global wildcard
250
+
251
+ if pattern.end_with?('.*')
252
+ # Wildcard pattern: 'payment.*' matches 'payment.processed', 'payment.failed', etc.
253
+ prefix = pattern[0..-3] # Remove '.*'
254
+ event_name.start_with?("#{prefix}.")
255
+ else
256
+ # Exact match
257
+ event_name == pattern
258
+ end
259
+ end
260
+
261
+ # Safely invoke a handler (fire-and-forget semantics)
262
+ def invoke_handler_safely(handler, event_data, event_name)
263
+ handler.call(event_data)
264
+ rescue StandardError => e
265
+ # Fire-and-forget: log errors but don't propagate
266
+ logger.warn "InProcessDomainEventPoller: Handler failed for '#{event_name}': #{e.message}"
267
+ logger.warn e.backtrace.first(3).join("\n")
268
+ end
269
+ end
270
+ end
271
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'tasker_core/version'
4
+ require 'json'
5
+ require 'faraday'
6
+ require 'dry-events'
7
+ require 'dry-struct'
8
+ require 'dry-types'
9
+ require 'dry-validation'
10
+ require 'concurrent-ruby'
11
+ require 'timeout'
12
+ require 'dotenv'
13
+ require 'active_support'
14
+ require 'active_support/core_ext'
15
+
16
+ # TaskerCore - High-performance workflow orchestration for Ruby
17
+ #
18
+ # This library provides a Ruby interface to the Rust-powered tasker-core orchestration
19
+ # system, enabling developers to build complex, distributed workflows with automatic
20
+ # retry logic, dependency management, and event-driven execution.
21
+ #
22
+ # The system consists of two main layers:
23
+ # - **Rust Foundation**: High-performance orchestration, state management, and messaging
24
+ # - **Ruby Business Logic**: Step handlers, task handlers, and custom workflow logic
25
+ #
26
+ # These layers communicate via FFI (Foreign Function Interface) and an event-driven
27
+ # architecture, providing the performance of Rust with the expressiveness of Ruby.
28
+ #
29
+ # @example Getting started with TaskerCore
30
+ # # 1. Compile the Rust extension (first time only)
31
+ # bundle exec rake compile
32
+ #
33
+ # # 2. Define a step handler for your business logic
34
+ # class ProcessPaymentHandler < TaskerCore::StepHandler::Base
35
+ # def call(context)
36
+ # # Access task context data
37
+ # amount = context.get_task_field('amount')
38
+ # currency = context.get_task_field('currency')
39
+ #
40
+ # # Your business logic here
41
+ # payment_result = charge_payment(amount, currency)
42
+ #
43
+ # # Return results
44
+ # success(result: { payment_id: payment_result.id, status: 'succeeded' })
45
+ # end
46
+ # end
47
+ #
48
+ # # 3. Start the worker to process tasks
49
+ # TaskerCore::Worker::Bootstrap.start!(
50
+ # namespaces: ['payments']
51
+ # )
52
+ #
53
+ # @example Creating and submitting a task
54
+ # # Build a task request
55
+ # request = TaskerCore::Types::TaskRequest.build_test(
56
+ # namespace: "payments",
57
+ # name: "process_payment",
58
+ # context: { amount: 100.00, currency: "USD" }
59
+ # )
60
+ #
61
+ # # Submit to orchestration system
62
+ # TaskerCore::FFI.submit_task(request)
63
+ #
64
+ # @example Using the API step handler for HTTP calls
65
+ # class FetchUserHandler < TaskerCore::StepHandler::Api
66
+ # def call(context)
67
+ # user_id = context.get_task_field('user_id')
68
+ #
69
+ # # Automatic error classification and retry logic
70
+ # response = get("/users/#{user_id}")
71
+ #
72
+ # success(result: response.body)
73
+ # end
74
+ # end
75
+ #
76
+ # Architecture Overview:
77
+ #
78
+ # 1. **Task State Machine**: 12 states managing overall workflow orchestration
79
+ # - Initial: Pending, Initializing
80
+ # - Active: EnqueuingSteps, StepsInProcess, EvaluatingResults
81
+ # - Waiting: WaitingForDependencies, WaitingForRetry, BlockedByFailures
82
+ # - Terminal: Complete, Error, Cancelled, ResolvedManually
83
+ #
84
+ # 2. **Step State Machine**: 9 states managing individual step execution
85
+ # - Pipeline: Pending, Enqueued, InProgress, EnqueuedForOrchestration
86
+ # - Terminal: Complete, Error, Cancelled, ResolvedManually, WaitingForRetry
87
+ #
88
+ # 3. **Event-Driven Communication**:
89
+ # - Rust → Ruby: Step execution events via EventPoller
90
+ # - Ruby → Rust: Step completion events via EventBridge
91
+ # - PostgreSQL LISTEN/NOTIFY for real-time coordination
92
+ #
93
+ # Load Order (Important):
94
+ # 1. Rust native extension (tasker_rb) - provides FFI and base classes
95
+ # 2. Ruby error classes - error hierarchy for classification
96
+ # 3. Ruby models - data wrappers for FFI types
97
+ # 4. Ruby handlers - step and task handler base classes
98
+ # 5. Event system - EventBridge, EventPoller, Subscriber
99
+ # 6. Bootstrap - worker initialization and lifecycle
100
+ #
101
+ # @see TaskerCore::StepHandler::Base For creating custom step handlers
102
+ # @see TaskerCore::StepHandler::Api For HTTP-based step handlers
103
+ # @see TaskerCore::Worker::Bootstrap For worker initialization
104
+ # @see TaskerCore::Handlers For the public API namespace
105
+ # @see TaskerCore::Errors For error classification and retry logic
106
+ # @see https://github.com/tasker-systems/tasker-core Documentation and examples
107
+ module TaskerCore
108
+ end
109
+
110
+ begin
111
+ Dotenv.load
112
+ # Load the compiled Rust extension first (provides base classes)
113
+ require_relative 'tasker_core/tasker_rb'
114
+ rescue LoadError => e
115
+ raise LoadError, <<~MSG
116
+
117
+ ❌ Failed to load tasker-rb native extension!
118
+
119
+ This usually means the Rust extension hasn't been compiled yet.
120
+
121
+ To compile the extension:
122
+ cd #{File.dirname(__FILE__)}/../..
123
+ rake compile
124
+
125
+ Or if you're using this gem in a Rails application:
126
+ bundle exec rake tasker_core:compile
127
+
128
+ Original error: #{e.message}
129
+
130
+ MSG
131
+ end
132
+
133
+ # Load Ruby modules after Rust extension (they depend on Rust base classes)
134
+ require_relative 'tasker_core/errors'
135
+ require_relative 'tasker_core/logger'
136
+ require_relative 'tasker_core/tracing' # TAS-29 Phase 6: Unified structured logging via FFI
137
+ require_relative 'tasker_core/internal'
138
+ require_relative 'tasker_core/template_discovery'
139
+ require_relative 'tasker_core/test_environment'
140
+ require_relative 'tasker_core/types'
141
+ require_relative 'tasker_core/models'
142
+ require_relative 'tasker_core/handlers'
143
+ require_relative 'tasker_core/batch_processing/batch_worker_context'
144
+ require_relative 'tasker_core/batch_processing/batch_aggregation_scenario'
145
+ require_relative 'tasker_core/registry'
146
+ require_relative 'tasker_core/subscriber'
147
+ require_relative 'tasker_core/event_bridge'
148
+ require_relative 'tasker_core/domain_events' # TAS-65: Domain event publisher infrastructure
149
+ require_relative 'tasker_core/observability' # TAS-77: Observability via FFI
150
+ require_relative 'tasker_core/worker/event_poller'
151
+ require_relative 'tasker_core/worker/in_process_domain_event_poller' # TAS-65: Fast domain events
152
+ require_relative 'tasker_core/bootstrap'
153
+
154
+ module TaskerCore
155
+ module Worker
156
+ end
157
+ end
158
+
159
+ # Load test environment components if appropriate (before auto-boot)
160
+ TaskerCore::TestEnvironment.load_if_test!