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.
- checksums.yaml +7 -0
- data/DEVELOPMENT.md +548 -0
- data/README.md +87 -0
- data/ext/tasker_core/Cargo.lock +4720 -0
- data/ext/tasker_core/Cargo.toml +76 -0
- data/ext/tasker_core/extconf.rb +38 -0
- data/ext/tasker_core/src/CLAUDE.md +7 -0
- data/ext/tasker_core/src/bootstrap.rs +320 -0
- data/ext/tasker_core/src/bridge.rs +400 -0
- data/ext/tasker_core/src/client_ffi.rs +173 -0
- data/ext/tasker_core/src/conversions.rs +131 -0
- data/ext/tasker_core/src/diagnostics.rs +57 -0
- data/ext/tasker_core/src/event_handler.rs +179 -0
- data/ext/tasker_core/src/event_publisher_ffi.rs +239 -0
- data/ext/tasker_core/src/ffi_logging.rs +245 -0
- data/ext/tasker_core/src/global_event_system.rs +16 -0
- data/ext/tasker_core/src/in_process_event_ffi.rs +319 -0
- data/ext/tasker_core/src/lib.rs +41 -0
- data/ext/tasker_core/src/observability_ffi.rs +339 -0
- data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
- data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
- data/lib/tasker_core/bootstrap.rb +394 -0
- data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
- data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
- data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
- data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
- data/lib/tasker_core/domain_events.rb +43 -0
- data/lib/tasker_core/errors/CLAUDE.md +7 -0
- data/lib/tasker_core/errors/common.rb +305 -0
- data/lib/tasker_core/errors/error_classifier.rb +61 -0
- data/lib/tasker_core/errors.rb +4 -0
- data/lib/tasker_core/event_bridge.rb +330 -0
- data/lib/tasker_core/handlers.rb +159 -0
- data/lib/tasker_core/internal.rb +31 -0
- data/lib/tasker_core/logger.rb +234 -0
- data/lib/tasker_core/models.rb +337 -0
- data/lib/tasker_core/observability/types.rb +158 -0
- data/lib/tasker_core/observability.rb +292 -0
- data/lib/tasker_core/registry/handler_registry.rb +453 -0
- data/lib/tasker_core/registry/resolver_chain.rb +258 -0
- data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
- data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
- data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
- data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
- data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
- data/lib/tasker_core/registry/resolvers.rb +42 -0
- data/lib/tasker_core/registry.rb +12 -0
- data/lib/tasker_core/step_handler/api.rb +48 -0
- data/lib/tasker_core/step_handler/base.rb +354 -0
- data/lib/tasker_core/step_handler/batchable.rb +50 -0
- data/lib/tasker_core/step_handler/decision.rb +53 -0
- data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
- data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
- data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
- data/lib/tasker_core/step_handler/mixins.rb +66 -0
- data/lib/tasker_core/subscriber.rb +212 -0
- data/lib/tasker_core/task_handler/base.rb +254 -0
- data/lib/tasker_core/tasker_rb.so +0 -0
- data/lib/tasker_core/template_discovery.rb +181 -0
- data/lib/tasker_core/tracing.rb +166 -0
- data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
- data/lib/tasker_core/types/client_types.rb +145 -0
- data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
- data/lib/tasker_core/types/error_types.rb +72 -0
- data/lib/tasker_core/types/simple_message.rb +151 -0
- data/lib/tasker_core/types/step_context.rb +328 -0
- data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
- data/lib/tasker_core/types/step_message.rb +112 -0
- data/lib/tasker_core/types/step_types.rb +207 -0
- data/lib/tasker_core/types/task_template.rb +240 -0
- data/lib/tasker_core/types/task_types.rb +148 -0
- data/lib/tasker_core/types.rb +132 -0
- data/lib/tasker_core/version.rb +13 -0
- data/lib/tasker_core/worker/CLAUDE.md +7 -0
- data/lib/tasker_core/worker/event_poller.rb +224 -0
- data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
- data/lib/tasker_core.rb +160 -0
- metadata +322 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module Worker
|
|
5
|
+
# Bootstrap orchestrator for Ruby worker
|
|
6
|
+
#
|
|
7
|
+
# Manages the complete initialization and lifecycle of the Ruby worker system,
|
|
8
|
+
# coordinating both Rust foundation layer and Ruby business logic components.
|
|
9
|
+
# This is the primary entry point for starting a TaskerCore worker process.
|
|
10
|
+
#
|
|
11
|
+
# The bootstrap process follows this sequence:
|
|
12
|
+
# 1. **Initialize Ruby Components**: EventBridge, HandlerRegistry, Subscribers
|
|
13
|
+
# 2. **Bootstrap Rust Foundation**: Start Rust worker via FFI
|
|
14
|
+
# 3. **Start Event Processing**: Begin EventPoller for step execution
|
|
15
|
+
# 4. **Register Shutdown Handlers**: Setup graceful termination on signals
|
|
16
|
+
#
|
|
17
|
+
# The Bootstrap class is a singleton, ensuring only one worker instance runs
|
|
18
|
+
# per process. It handles the complex coordination between Ruby and Rust,
|
|
19
|
+
# managing lifecycle transitions and providing health monitoring.
|
|
20
|
+
#
|
|
21
|
+
# @example Basic worker startup
|
|
22
|
+
# # Start with default configuration
|
|
23
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.start!
|
|
24
|
+
# # => Returns bootstrap instance in :running status
|
|
25
|
+
#
|
|
26
|
+
# # Check if worker is running
|
|
27
|
+
# bootstrap.running?
|
|
28
|
+
# # => true
|
|
29
|
+
#
|
|
30
|
+
# @example Custom configuration
|
|
31
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.start!(
|
|
32
|
+
# worker_id: "custom-worker-1",
|
|
33
|
+
# enable_web_api: false,
|
|
34
|
+
# event_driven_enabled: true,
|
|
35
|
+
# deployment_mode: "Hybrid", # PollingOnly, EventDrivenOnly, or Hybrid
|
|
36
|
+
# namespaces: ["payments", "fulfillment", "notifications"]
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# @example Health checking
|
|
40
|
+
# status = TaskerCore::Worker::Bootstrap.instance.health_check
|
|
41
|
+
# # => {
|
|
42
|
+
# # healthy: true,
|
|
43
|
+
# # status: :running,
|
|
44
|
+
# # rust: { running: true, worker_core_status: "processing" },
|
|
45
|
+
# # ruby: {
|
|
46
|
+
# # status: :running,
|
|
47
|
+
# # event_bridge_active: true,
|
|
48
|
+
# # handlers_registered: 12,
|
|
49
|
+
# # subscriber_active: true,
|
|
50
|
+
# # event_poller_active: true
|
|
51
|
+
# # }
|
|
52
|
+
# # }
|
|
53
|
+
#
|
|
54
|
+
# @example Graceful shutdown
|
|
55
|
+
# # Shutdown cleanly, completing in-flight work
|
|
56
|
+
# TaskerCore::Worker::Bootstrap.instance.shutdown!
|
|
57
|
+
# # => Stops Rust worker, Ruby components, runs shutdown handlers
|
|
58
|
+
#
|
|
59
|
+
# @example Custom shutdown handlers
|
|
60
|
+
# bootstrap = TaskerCore::Worker::Bootstrap.instance
|
|
61
|
+
# bootstrap.on_shutdown do
|
|
62
|
+
# puts "Cleaning up resources..."
|
|
63
|
+
# cleanup_database_connections
|
|
64
|
+
# flush_metrics
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# @example Getting comprehensive status
|
|
68
|
+
# status = bootstrap.status
|
|
69
|
+
# # => {
|
|
70
|
+
# # rust: { running: true, ... },
|
|
71
|
+
# # ruby: {
|
|
72
|
+
# # status: :running,
|
|
73
|
+
# # handle_stored: true,
|
|
74
|
+
# # handle_id: "550e8400-e29b-41d4-a716-446655440000",
|
|
75
|
+
# # worker_id: "ruby-worker-123",
|
|
76
|
+
# # event_bridge_active: true,
|
|
77
|
+
# # handler_registry_size: 12,
|
|
78
|
+
# # subscriber_active: true,
|
|
79
|
+
# # event_poller_active: true
|
|
80
|
+
# # }
|
|
81
|
+
# # }
|
|
82
|
+
#
|
|
83
|
+
# Deployment Modes:
|
|
84
|
+
# - **PollingOnly**: Traditional polling-based coordination (highest latency, most reliable)
|
|
85
|
+
# - **EventDrivenOnly**: Pure event-driven using PostgreSQL LISTEN/NOTIFY (lowest latency)
|
|
86
|
+
# - **Hybrid**: Event-driven with polling fallback (recommended for production)
|
|
87
|
+
#
|
|
88
|
+
# Worker States:
|
|
89
|
+
# - **:initialized**: Created but not started
|
|
90
|
+
# - **:running**: Fully operational and processing events
|
|
91
|
+
# - **:shutting_down**: Graceful shutdown in progress
|
|
92
|
+
# - **:stopped**: Completely shut down
|
|
93
|
+
#
|
|
94
|
+
# Signal Handling:
|
|
95
|
+
# The bootstrap automatically registers signal handlers for:
|
|
96
|
+
# - **SIGINT** (Ctrl+C): Graceful shutdown
|
|
97
|
+
# - **SIGTERM**: Graceful shutdown
|
|
98
|
+
# - **at_exit**: Cleanup on process exit
|
|
99
|
+
#
|
|
100
|
+
# @see TaskerCore::Worker::EventPoller For event polling details
|
|
101
|
+
# @see TaskerCore::EventBridge For event coordination
|
|
102
|
+
# @see TaskerCore::Registry::HandlerRegistry For handler management
|
|
103
|
+
# @see TaskerCore::FFI For Rust FFI operations
|
|
104
|
+
class Bootstrap
|
|
105
|
+
include Singleton
|
|
106
|
+
|
|
107
|
+
attr_reader :logger, :config, :rust_handle
|
|
108
|
+
|
|
109
|
+
def initialize
|
|
110
|
+
@logger = TaskerCore::Logger.instance
|
|
111
|
+
@status = :initialized
|
|
112
|
+
@shutdown_handlers = []
|
|
113
|
+
@rust_handle = nil
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Start the worker system with optional configuration
|
|
117
|
+
def self.start!
|
|
118
|
+
instance.start!
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Main boots
|
|
122
|
+
def start!
|
|
123
|
+
logger.info 'Starting Ruby worker bootstrap'
|
|
124
|
+
|
|
125
|
+
# Initialize Ruby components first
|
|
126
|
+
initialize_ruby_components!
|
|
127
|
+
|
|
128
|
+
# Bootstrap Rust foundation via FFI
|
|
129
|
+
bootstrap_rust_foundation!
|
|
130
|
+
|
|
131
|
+
# Start event processing
|
|
132
|
+
start_event_processing!
|
|
133
|
+
|
|
134
|
+
# Register shutdown handlers
|
|
135
|
+
register_shutdown_handlers!
|
|
136
|
+
|
|
137
|
+
@status = :running
|
|
138
|
+
logger.info 'Ruby worker system started successfully'
|
|
139
|
+
|
|
140
|
+
self
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
logger.error "Failed to start worker: #{e.message}"
|
|
143
|
+
logger.error e.backtrace.join("\n")
|
|
144
|
+
shutdown!
|
|
145
|
+
raise
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if worker is running
|
|
149
|
+
def running?
|
|
150
|
+
@status == :running && rust_worker_running?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if Rust handle is valid and running
|
|
154
|
+
def rust_handle_running?
|
|
155
|
+
return false unless @rust_handle.is_a?(Hash) &&
|
|
156
|
+
(@rust_handle[:handle_id] || @rust_handle['handle_id'])
|
|
157
|
+
|
|
158
|
+
rust_worker_running?
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Get comprehensive status
|
|
162
|
+
def status
|
|
163
|
+
rust_status = TaskerCore::FFI.worker_status
|
|
164
|
+
{
|
|
165
|
+
rust: rust_status,
|
|
166
|
+
ruby: {
|
|
167
|
+
status: @status,
|
|
168
|
+
handle_stored: !@rust_handle.nil?,
|
|
169
|
+
handle_id: @rust_handle&.dig('handle_id') || @rust_handle&.dig(:handle_id),
|
|
170
|
+
worker_id: @rust_handle&.dig('worker_id') || @rust_handle&.dig(:worker_id),
|
|
171
|
+
event_bridge_active: EventBridge.instance.active?,
|
|
172
|
+
handler_registry_size: Registry::HandlerRegistry.instance.handlers.size,
|
|
173
|
+
subscriber_active: @step_subscriber&.active? || false,
|
|
174
|
+
event_poller_active: EventPoller.instance.active?
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
logger.error "Failed to get status: #{e.message}"
|
|
179
|
+
{ error: e.message, status: @status }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Execute a block with the Rust handle, bootstrapping if necessary
|
|
183
|
+
def with_rust_handle(&block)
|
|
184
|
+
bootstrap_rust_foundation! unless rust_handle_running?
|
|
185
|
+
block.call
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Graceful shutdown
|
|
189
|
+
def shutdown!
|
|
190
|
+
return if @status == :stopped
|
|
191
|
+
|
|
192
|
+
logger.info 'Initiating graceful shutdown'
|
|
193
|
+
@status = :shutting_down
|
|
194
|
+
|
|
195
|
+
# Transition Rust to graceful shutdown first
|
|
196
|
+
if @rust_handle
|
|
197
|
+
begin
|
|
198
|
+
TaskerCore::FFI.transition_to_graceful_shutdown
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
logger.error "Failed to transition to graceful shutdown: #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Stop Ruby components
|
|
205
|
+
@step_subscriber&.stop!
|
|
206
|
+
EventBridge.instance.stop!
|
|
207
|
+
EventPoller.instance.stop!
|
|
208
|
+
|
|
209
|
+
# TAS-65: Stop domain event components
|
|
210
|
+
DomainEvents::SubscriberRegistry.instance.stop_all!
|
|
211
|
+
InProcessDomainEventPoller.instance.stop!
|
|
212
|
+
|
|
213
|
+
# Stop Rust worker and clear handle
|
|
214
|
+
if @rust_handle
|
|
215
|
+
begin
|
|
216
|
+
TaskerCore::FFI.stop_worker
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
logger.error "Failed to stop Rust worker: #{e.message}"
|
|
219
|
+
ensure
|
|
220
|
+
@rust_handle = nil
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Run custom shutdown handlers
|
|
225
|
+
@shutdown_handlers.each do |handler|
|
|
226
|
+
handler.call
|
|
227
|
+
rescue StandardError
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
@status = :stopped
|
|
232
|
+
logger.info 'Worker shutdown complete'
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Register custom shutdown handler
|
|
236
|
+
def on_shutdown(&block)
|
|
237
|
+
@shutdown_handlers << block if block_given?
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Perform health check on both Ruby and Rust components
|
|
241
|
+
def health_check
|
|
242
|
+
return { healthy: false, status: @status, error: 'not_running' } unless running?
|
|
243
|
+
|
|
244
|
+
begin
|
|
245
|
+
# Get Rust worker status
|
|
246
|
+
rust_status = TaskerCore::FFI.worker_status
|
|
247
|
+
rust_running = rust_status['running'] || rust_status[:running]
|
|
248
|
+
|
|
249
|
+
# Check Ruby components
|
|
250
|
+
ruby_healthy = @status == :running &&
|
|
251
|
+
EventBridge.instance.active? &&
|
|
252
|
+
EventPoller.instance.active? &&
|
|
253
|
+
Registry::HandlerRegistry.instance.handlers.any?
|
|
254
|
+
|
|
255
|
+
overall_healthy = rust_running && ruby_healthy
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
healthy: overall_healthy,
|
|
259
|
+
status: @status,
|
|
260
|
+
rust: {
|
|
261
|
+
running: rust_running,
|
|
262
|
+
worker_core_status: rust_status['worker_core_status'] || rust_status[:worker_core_status]
|
|
263
|
+
},
|
|
264
|
+
ruby: {
|
|
265
|
+
status: @status,
|
|
266
|
+
event_bridge_active: EventBridge.instance.active?,
|
|
267
|
+
handlers_registered: Registry::HandlerRegistry.instance.handlers.size,
|
|
268
|
+
subscriber_active: @step_subscriber&.active? || false,
|
|
269
|
+
event_poller_active: EventPoller.instance.active?,
|
|
270
|
+
# TAS-65: Domain event system status
|
|
271
|
+
domain_event_poller_active: InProcessDomainEventPoller.instance.active?,
|
|
272
|
+
domain_event_subscribers: DomainEvents::SubscriberRegistry.instance.stats
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
rescue StandardError => e
|
|
276
|
+
logger.error "Health check failed: #{e.message}"
|
|
277
|
+
{
|
|
278
|
+
healthy: false,
|
|
279
|
+
status: @status,
|
|
280
|
+
error: e.message
|
|
281
|
+
}
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def detect_namespaces
|
|
288
|
+
# Auto-detect from registered handlers
|
|
289
|
+
Registry::HandlerRegistry.instance.registered_handlers.map do |handler_class|
|
|
290
|
+
handler_class.namespace if handler_class.respond_to?(:namespace)
|
|
291
|
+
end.compact.uniq
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def initialize_ruby_components!
|
|
295
|
+
logger.info 'Initializing Ruby components...'
|
|
296
|
+
|
|
297
|
+
# Initialize event bridge
|
|
298
|
+
EventBridge.instance
|
|
299
|
+
|
|
300
|
+
# Initialize handler registry (bootstrap happens automatically)
|
|
301
|
+
Registry::HandlerRegistry.instance
|
|
302
|
+
|
|
303
|
+
# Initialize step execution subscriber
|
|
304
|
+
@step_subscriber = StepExecutionSubscriber.new
|
|
305
|
+
|
|
306
|
+
logger.info 'Ruby components initialized'
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def bootstrap_rust_foundation!
|
|
310
|
+
# Check if we already have a running handle
|
|
311
|
+
if rust_handle_running?
|
|
312
|
+
logger.debug 'Rust worker foundation already running, reusing handle'
|
|
313
|
+
return @rust_handle
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
logger.info 'Bootstrapping Rust worker foundation...'
|
|
317
|
+
|
|
318
|
+
# Bootstrap the worker and store the handle result
|
|
319
|
+
result = TaskerCore::FFI.bootstrap_worker
|
|
320
|
+
logger.info "Rust bootstrap result: #{result.inspect}"
|
|
321
|
+
|
|
322
|
+
# Check if it was already running or newly started
|
|
323
|
+
# Handle both string and symbol keys from Rust FFI
|
|
324
|
+
status = result['status'] || result[:status]
|
|
325
|
+
worker_id = result['worker_id'] || result[:worker_id]
|
|
326
|
+
|
|
327
|
+
if status == 'already_running'
|
|
328
|
+
logger.info 'Worker system was already running, reusing existing handle'
|
|
329
|
+
elsif status == 'started'
|
|
330
|
+
logger.info "New worker system started with ID: #{worker_id}"
|
|
331
|
+
else
|
|
332
|
+
raise "Unexpected bootstrap status: #{status}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Store the handle information
|
|
336
|
+
@rust_handle = result
|
|
337
|
+
|
|
338
|
+
# Verify it's running
|
|
339
|
+
status = TaskerCore::FFI.worker_status
|
|
340
|
+
# Handle both string and symbol keys from Rust FFI
|
|
341
|
+
running = status['running'] || status[:running]
|
|
342
|
+
unless running
|
|
343
|
+
@rust_handle = nil
|
|
344
|
+
raise "Rust worker failed to start: #{status.inspect}"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
handle_id = @rust_handle['handle_id'] || @rust_handle[:handle_id]
|
|
348
|
+
logger.info "Rust foundation bootstrapped with handle: #{handle_id}"
|
|
349
|
+
@rust_handle
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def start_event_processing!
|
|
353
|
+
logger.info 'Starting event processing...'
|
|
354
|
+
|
|
355
|
+
# Start the EventPoller to poll for step execution events from Rust
|
|
356
|
+
EventPoller.instance.start!
|
|
357
|
+
|
|
358
|
+
# NOTE: StepExecutionSubscriber already subscribes to step execution events
|
|
359
|
+
# in its initializer, so we don't need to subscribe again here.
|
|
360
|
+
# Duplicate subscriptions cause the same event to be processed twice,
|
|
361
|
+
# leading to double state transitions.
|
|
362
|
+
|
|
363
|
+
# TAS-65: Start in-process domain event poller for fast events
|
|
364
|
+
InProcessDomainEventPoller.instance.start!
|
|
365
|
+
|
|
366
|
+
# TAS-65: Start domain event subscribers
|
|
367
|
+
DomainEvents::SubscriberRegistry.instance.start_all!
|
|
368
|
+
|
|
369
|
+
logger.info 'Event processing started (step events + domain events)'
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def register_shutdown_handlers!
|
|
373
|
+
# Graceful shutdown on signals
|
|
374
|
+
%w[INT TERM].each do |signal|
|
|
375
|
+
Signal.trap(signal) do
|
|
376
|
+
Thread.new { shutdown! }
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Shutdown on exit
|
|
381
|
+
at_exit { shutdown! if running? }
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def rust_worker_running?
|
|
385
|
+
status = TaskerCore::FFI.worker_status
|
|
386
|
+
# Handle both string and symbol keys from Rust FFI
|
|
387
|
+
running = status['running'] || status[:running]
|
|
388
|
+
running == true
|
|
389
|
+
rescue StandardError
|
|
390
|
+
false
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TaskerCore
|
|
4
|
+
module DomainEvents
|
|
5
|
+
# TAS-65: Base class for custom domain event publishers
|
|
6
|
+
#
|
|
7
|
+
# Domain event publishers transform step execution results into business events.
|
|
8
|
+
# They are declared in task template YAML via the `publisher:` field and registered
|
|
9
|
+
# at bootstrap time.
|
|
10
|
+
#
|
|
11
|
+
# @example Creating a custom publisher
|
|
12
|
+
# class PaymentEventPublisher < TaskerCore::DomainEvents::BasePublisher
|
|
13
|
+
# def name
|
|
14
|
+
# 'PaymentEventPublisher'
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# def transform_payload(step_result, event_declaration)
|
|
18
|
+
# # Extract business-specific payload from step result
|
|
19
|
+
# {
|
|
20
|
+
# payment_id: step_result[:result][:payment_id],
|
|
21
|
+
# amount: step_result[:result][:amount],
|
|
22
|
+
# currency: step_result[:result][:currency],
|
|
23
|
+
# status: step_result[:success] ? 'succeeded' : 'failed'
|
|
24
|
+
# }
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# def should_publish?(step_result, event_declaration)
|
|
28
|
+
# # Only publish for successful payments
|
|
29
|
+
# step_result[:success] && step_result[:result][:payment_id].present?
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @example Registering a custom publisher
|
|
34
|
+
# TaskerCore::DomainEvents::PublisherRegistry.instance.register(
|
|
35
|
+
# PaymentEventPublisher.new
|
|
36
|
+
# )
|
|
37
|
+
#
|
|
38
|
+
# @example YAML declaration
|
|
39
|
+
# steps:
|
|
40
|
+
# - name: process_payment
|
|
41
|
+
# publishes_events:
|
|
42
|
+
# - name: payment.processed
|
|
43
|
+
# publisher: PaymentEventPublisher
|
|
44
|
+
# delivery_mode: durable
|
|
45
|
+
# condition: success
|
|
46
|
+
#
|
|
47
|
+
class BasePublisher
|
|
48
|
+
attr_reader :logger
|
|
49
|
+
|
|
50
|
+
def initialize
|
|
51
|
+
@logger = TaskerCore::Logger.instance
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The publisher name used for registry lookup
|
|
55
|
+
# Must match the `publisher:` field in YAML
|
|
56
|
+
#
|
|
57
|
+
# @return [String] The publisher name
|
|
58
|
+
def name
|
|
59
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Transform step result into business event payload
|
|
63
|
+
#
|
|
64
|
+
# Override this to customize the event payload for your domain.
|
|
65
|
+
# The default implementation returns the step result as-is.
|
|
66
|
+
#
|
|
67
|
+
# @param step_result [Hash] The step execution result
|
|
68
|
+
# - :success [Boolean] Whether the step succeeded
|
|
69
|
+
# - :result [Hash] The step handler's return value
|
|
70
|
+
# - :metadata [Hash] Execution metadata
|
|
71
|
+
# @param event_declaration [Hash] The event declaration from YAML
|
|
72
|
+
# - :name [String] The event name (e.g., "order.processed")
|
|
73
|
+
# - :delivery_mode [String] "durable" or "fast"
|
|
74
|
+
# - :condition [String] "success", "failure", or "always"
|
|
75
|
+
# @param step_context [Hash] The step execution context
|
|
76
|
+
# - :task [Hash] Task information
|
|
77
|
+
# - :workflow_step [Hash] Workflow step information
|
|
78
|
+
# - :step_definition [Hash] Step definition from YAML
|
|
79
|
+
#
|
|
80
|
+
# @return [Hash] The business event payload to publish
|
|
81
|
+
def transform_payload(step_result, _event_declaration, _step_context = nil)
|
|
82
|
+
step_result[:result] || {}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Determine if this event should be published
|
|
86
|
+
#
|
|
87
|
+
# Override this to add custom publishing conditions beyond the
|
|
88
|
+
# YAML `condition:` field. The YAML condition is evaluated first,
|
|
89
|
+
# then this method is called.
|
|
90
|
+
#
|
|
91
|
+
# @param step_result [Hash] The step execution result
|
|
92
|
+
# @param event_declaration [Hash] The event declaration from YAML
|
|
93
|
+
# @param step_context [Hash] The step execution context
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] true if the event should be published
|
|
96
|
+
def should_publish?(_step_result, _event_declaration, _step_context = nil)
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Add additional metadata to the event
|
|
101
|
+
#
|
|
102
|
+
# Override this to add custom metadata fields to the event.
|
|
103
|
+
# Default returns empty hash.
|
|
104
|
+
#
|
|
105
|
+
# @param step_result [Hash] The step execution result
|
|
106
|
+
# @param event_declaration [Hash] The event declaration from YAML
|
|
107
|
+
# @param step_context [Hash] The step execution context
|
|
108
|
+
#
|
|
109
|
+
# @return [Hash] Additional metadata to merge into event metadata
|
|
110
|
+
def additional_metadata(_step_result, _event_declaration, _step_context = nil)
|
|
111
|
+
{}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Hook called before publishing
|
|
115
|
+
#
|
|
116
|
+
# Override for pre-publish validation, logging, or metrics.
|
|
117
|
+
# Raise an exception to prevent publishing.
|
|
118
|
+
#
|
|
119
|
+
# @param event_name [String] The event name
|
|
120
|
+
# @param payload [Hash] The transformed payload
|
|
121
|
+
# @param metadata [Hash] The event metadata
|
|
122
|
+
def before_publish(event_name, _payload, _metadata)
|
|
123
|
+
logger.debug "Publishing event: #{event_name}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Hook called after successful publishing
|
|
127
|
+
#
|
|
128
|
+
# Override for post-publish logging, metrics, or cleanup.
|
|
129
|
+
#
|
|
130
|
+
# @param event_name [String] The event name
|
|
131
|
+
# @param payload [Hash] The transformed payload
|
|
132
|
+
# @param metadata [Hash] The event metadata
|
|
133
|
+
def after_publish(event_name, _payload, _metadata)
|
|
134
|
+
logger.debug "Event published: #{event_name}"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Hook called if publishing fails
|
|
138
|
+
#
|
|
139
|
+
# Override for error handling, logging, or fallback behavior.
|
|
140
|
+
# Default logs the error but does not re-raise.
|
|
141
|
+
#
|
|
142
|
+
# @param event_name [String] The event name
|
|
143
|
+
# @param error [Exception] The error that occurred
|
|
144
|
+
# @param payload [Hash] The transformed payload
|
|
145
|
+
def on_publish_error(event_name, error, _payload)
|
|
146
|
+
logger.error "Failed to publish event #{event_name}: #{error.message}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ========================================================================
|
|
150
|
+
# CROSS-LANGUAGE STANDARD API (TAS-96)
|
|
151
|
+
# ========================================================================
|
|
152
|
+
|
|
153
|
+
# Cross-language standard: Publish an event with unified context
|
|
154
|
+
#
|
|
155
|
+
# This method coordinates the existing hooks (should_publish?, transform_payload,
|
|
156
|
+
# additional_metadata, before_publish, after_publish) into a single publish call.
|
|
157
|
+
#
|
|
158
|
+
# @param ctx [Hash] Step event context containing:
|
|
159
|
+
# - :event_name [String] The event name (e.g., "payment.processed")
|
|
160
|
+
# - :step_result [Hash] The step execution result
|
|
161
|
+
# - :event_declaration [Hash] The event declaration from YAML
|
|
162
|
+
# - :step_context [Hash, TaskerCore::Types::StepContext] Step execution context
|
|
163
|
+
# @return [Boolean] true if event was published, false if skipped
|
|
164
|
+
#
|
|
165
|
+
# @example Publishing with context
|
|
166
|
+
# ctx = {
|
|
167
|
+
# event_name: 'payment.processed',
|
|
168
|
+
# step_result: { success: true, result: { payment_id: '123' } },
|
|
169
|
+
# event_declaration: { name: 'payment.processed', delivery_mode: 'durable' },
|
|
170
|
+
# step_context: step_context
|
|
171
|
+
# }
|
|
172
|
+
# publisher.publish(ctx)
|
|
173
|
+
def publish(ctx)
|
|
174
|
+
event_name = ctx[:event_name]
|
|
175
|
+
step_result = ctx[:step_result]
|
|
176
|
+
event_declaration = ctx[:event_declaration] || {}
|
|
177
|
+
step_context = ctx[:step_context]
|
|
178
|
+
|
|
179
|
+
# Check publishing conditions
|
|
180
|
+
return false unless should_publish?(step_result, event_declaration, step_context)
|
|
181
|
+
|
|
182
|
+
# Transform payload
|
|
183
|
+
payload = transform_payload(step_result, event_declaration, step_context)
|
|
184
|
+
|
|
185
|
+
# Build metadata
|
|
186
|
+
base_metadata = {
|
|
187
|
+
publisher: name,
|
|
188
|
+
published_at: Time.now.utc.iso8601
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add step context info if available
|
|
192
|
+
if step_context.respond_to?(:task_uuid)
|
|
193
|
+
base_metadata[:task_uuid] = step_context.task_uuid
|
|
194
|
+
base_metadata[:step_uuid] = step_context.step_uuid
|
|
195
|
+
base_metadata[:step_name] = step_context.step_name if step_context.respond_to?(:step_name)
|
|
196
|
+
base_metadata[:namespace] = step_context.namespace_name if step_context.respond_to?(:namespace_name)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
metadata = base_metadata.merge(additional_metadata(step_result, event_declaration, step_context))
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
# Pre-publish hook
|
|
203
|
+
before_publish(event_name, payload, metadata)
|
|
204
|
+
|
|
205
|
+
# Actual publishing is handled by the event router/bridge
|
|
206
|
+
# This method just prepares and validates the event
|
|
207
|
+
# Subclasses can override to perform actual publishing
|
|
208
|
+
|
|
209
|
+
# Post-publish hook
|
|
210
|
+
after_publish(event_name, payload, metadata)
|
|
211
|
+
|
|
212
|
+
true
|
|
213
|
+
rescue StandardError => e
|
|
214
|
+
on_publish_error(event_name, e, payload)
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|