tasker-rb 0.1.3-x86_64-linux

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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +55 -0
  3. data/lib/tasker_core/batch_processing/batch_aggregation_scenario.rb +85 -0
  4. data/lib/tasker_core/batch_processing/batch_worker_context.rb +238 -0
  5. data/lib/tasker_core/bootstrap.rb +394 -0
  6. data/lib/tasker_core/client.rb +165 -0
  7. data/lib/tasker_core/domain_events/base_publisher.rb +220 -0
  8. data/lib/tasker_core/domain_events/base_subscriber.rb +178 -0
  9. data/lib/tasker_core/domain_events/publisher_registry.rb +253 -0
  10. data/lib/tasker_core/domain_events/subscriber_registry.rb +152 -0
  11. data/lib/tasker_core/domain_events.rb +43 -0
  12. data/lib/tasker_core/errors/CLAUDE.md +7 -0
  13. data/lib/tasker_core/errors/common.rb +305 -0
  14. data/lib/tasker_core/errors/error_classifier.rb +61 -0
  15. data/lib/tasker_core/errors.rb +4 -0
  16. data/lib/tasker_core/event_bridge.rb +330 -0
  17. data/lib/tasker_core/handlers.rb +159 -0
  18. data/lib/tasker_core/internal.rb +31 -0
  19. data/lib/tasker_core/logger.rb +234 -0
  20. data/lib/tasker_core/models.rb +337 -0
  21. data/lib/tasker_core/observability/types.rb +158 -0
  22. data/lib/tasker_core/observability.rb +292 -0
  23. data/lib/tasker_core/registry/handler_registry.rb +453 -0
  24. data/lib/tasker_core/registry/resolver_chain.rb +258 -0
  25. data/lib/tasker_core/registry/resolvers/base_resolver.rb +90 -0
  26. data/lib/tasker_core/registry/resolvers/class_constant_resolver.rb +156 -0
  27. data/lib/tasker_core/registry/resolvers/explicit_mapping_resolver.rb +146 -0
  28. data/lib/tasker_core/registry/resolvers/method_dispatch_wrapper.rb +144 -0
  29. data/lib/tasker_core/registry/resolvers/registry_resolver.rb +229 -0
  30. data/lib/tasker_core/registry/resolvers.rb +42 -0
  31. data/lib/tasker_core/registry.rb +12 -0
  32. data/lib/tasker_core/step_handler/api.rb +48 -0
  33. data/lib/tasker_core/step_handler/base.rb +354 -0
  34. data/lib/tasker_core/step_handler/batchable.rb +50 -0
  35. data/lib/tasker_core/step_handler/decision.rb +53 -0
  36. data/lib/tasker_core/step_handler/mixins/api.rb +452 -0
  37. data/lib/tasker_core/step_handler/mixins/batchable.rb +465 -0
  38. data/lib/tasker_core/step_handler/mixins/decision.rb +252 -0
  39. data/lib/tasker_core/step_handler/mixins.rb +66 -0
  40. data/lib/tasker_core/subscriber.rb +212 -0
  41. data/lib/tasker_core/task_handler/base.rb +254 -0
  42. data/lib/tasker_core/tasker_rb.so +0 -0
  43. data/lib/tasker_core/template_discovery.rb +181 -0
  44. data/lib/tasker_core/test_environment.rb +313 -0
  45. data/lib/tasker_core/tracing.rb +166 -0
  46. data/lib/tasker_core/types/batch_processing_outcome.rb +301 -0
  47. data/lib/tasker_core/types/client_types.rb +145 -0
  48. data/lib/tasker_core/types/decision_point_outcome.rb +177 -0
  49. data/lib/tasker_core/types/error_types.rb +72 -0
  50. data/lib/tasker_core/types/simple_message.rb +151 -0
  51. data/lib/tasker_core/types/step_context.rb +328 -0
  52. data/lib/tasker_core/types/step_handler_call_result.rb +307 -0
  53. data/lib/tasker_core/types/step_message.rb +112 -0
  54. data/lib/tasker_core/types/step_types.rb +207 -0
  55. data/lib/tasker_core/types/task_template.rb +240 -0
  56. data/lib/tasker_core/types/task_types.rb +148 -0
  57. data/lib/tasker_core/types.rb +132 -0
  58. data/lib/tasker_core/version.rb +13 -0
  59. data/lib/tasker_core/worker/CLAUDE.md +7 -0
  60. data/lib/tasker_core/worker/event_poller.rb +224 -0
  61. data/lib/tasker_core/worker/in_process_domain_event_poller.rb +271 -0
  62. data/lib/tasker_core.rb +161 -0
  63. metadata +292 -0
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module TestEnvironment
5
+ # Test environment conditional loading that only activates when TASKER_ENV=test
6
+ # Loads example handlers and configures test-specific paths for template discovery
7
+ class ConditionalLoader
8
+ include Singleton
9
+
10
+ attr_reader :logger, :loaded, :example_handlers_path, :template_fixtures_path
11
+
12
+ def initialize
13
+ @logger = nil # Will be set when logger is available
14
+ @loaded = false
15
+ @example_handlers_path = File.expand_path('../../spec/handlers/examples', __dir__)
16
+ @template_fixtures_path = File.expand_path('../../spec/fixtures/templates', __dir__)
17
+ end
18
+
19
+ # Check if we should load test environment components
20
+ def should_load_test_environment?
21
+ test_env = ENV['TASKER_ENV']&.downcase == 'test'
22
+ rails_test_env = ENV['RAILS_ENV']&.downcase == 'test'
23
+ force_examples = ENV['TASKER_FORCE_EXAMPLE_HANDLERS'] == 'true'
24
+
25
+ test_env || rails_test_env || force_examples
26
+ end
27
+
28
+ # Load test environment components if appropriate
29
+ def load_if_test_environment!
30
+ return false unless should_load_test_environment?
31
+ return true if @loaded # Already loaded
32
+
33
+ # Defer logger initialization until after TaskerCore loads
34
+ @logger = TaskerCore::Logger.instance
35
+
36
+ log_info('🧪 Test environment detected, loading example handlers and templates')
37
+
38
+ # Set template path override for test fixtures
39
+ setup_test_template_path!
40
+
41
+ # Pre-load all example handler files
42
+ load_example_handler_files!
43
+
44
+ # Verify example handlers are available
45
+ verify_test_setup!
46
+
47
+ @loaded = true
48
+ log_info("✅ Test environment setup complete: #{loaded_handler_count} example handlers loaded")
49
+ true
50
+ end
51
+
52
+ # Get count of loaded handler classes for verification
53
+ def loaded_handler_count
54
+ return 0 unless @loaded
55
+
56
+ count = 0
57
+ ObjectSpace.each_object(Class) do |klass|
58
+ if klass.name&.end_with?('Handler') &&
59
+ klass.ancestors.any? { |ancestor| ancestor.name&.include?('StepHandler') }
60
+ count += 1
61
+ end
62
+ rescue StandardError
63
+ # Skip classes that can't be introspected
64
+ next
65
+ end
66
+ count
67
+ end
68
+
69
+ # Get list of loaded example handler class names
70
+ def loaded_handler_names
71
+ return [] unless @loaded
72
+
73
+ names = []
74
+ ObjectSpace.each_object(Class) do |klass|
75
+ if klass.name&.end_with?('Handler') &&
76
+ klass.ancestors.any? { |ancestor| ancestor.name&.include?('StepHandler') }
77
+ names << klass.name
78
+ end
79
+ rescue StandardError
80
+ # Skip classes that can't be introspected
81
+ next
82
+ end
83
+ names.sort
84
+ end
85
+
86
+ # Get template discovery info for debugging
87
+ def test_template_info
88
+ return {} unless @loaded
89
+
90
+ {
91
+ template_path: ENV.fetch('TASKER_TEMPLATE_PATH', nil),
92
+ fixtures_path: @template_fixtures_path,
93
+ fixtures_exist: Dir.exist?(@template_fixtures_path),
94
+ template_files: Dir.exist?(@template_fixtures_path) ? Dir.glob("#{@template_fixtures_path}/*.yaml").count : 0,
95
+ example_handlers_path: @example_handlers_path,
96
+ examples_exist: Dir.exist?(@example_handlers_path),
97
+ handler_files: Dir.exist?(@example_handlers_path) ? Dir.glob("#{@example_handlers_path}/**/*_handler.rb").count : 0,
98
+ subscriber_files: Dir.exist?(@example_handlers_path) ? Dir.glob("#{@example_handlers_path}/**/*_subscriber.rb").count : 0,
99
+ registered_subscribers: TaskerCore::DomainEvents::SubscriberRegistry.instance.count
100
+ }
101
+ end
102
+
103
+ # TAS-65: Get count of loaded subscriber classes
104
+ def loaded_subscriber_count
105
+ return 0 unless @loaded
106
+
107
+ count = 0
108
+ ObjectSpace.each_object(Class) do |klass|
109
+ if klass < TaskerCore::DomainEvents::BaseSubscriber &&
110
+ klass != TaskerCore::DomainEvents::BaseSubscriber
111
+ count += 1
112
+ end
113
+ rescue StandardError
114
+ next
115
+ end
116
+ count
117
+ end
118
+
119
+ # TAS-65: Get list of loaded subscriber class names
120
+ def loaded_subscriber_names
121
+ return [] unless @loaded
122
+
123
+ names = []
124
+ ObjectSpace.each_object(Class) do |klass|
125
+ if klass < TaskerCore::DomainEvents::BaseSubscriber &&
126
+ klass != TaskerCore::DomainEvents::BaseSubscriber
127
+ names << klass.name
128
+ end
129
+ rescue StandardError
130
+ next
131
+ end
132
+ names.sort
133
+ end
134
+
135
+ private
136
+
137
+ def log_info(message)
138
+ if @logger
139
+ @logger.info(message)
140
+ else
141
+ puts message # Fallback if logger not available yet
142
+ end
143
+ end
144
+
145
+ def log_debug(message)
146
+ if @logger.respond_to?(:debug)
147
+ @logger.debug(message)
148
+ elsif ENV['LOG_LEVEL'] == 'debug'
149
+ puts "[DEBUG] #{message}"
150
+ end
151
+ end
152
+
153
+ def log_warn(message)
154
+ if @logger
155
+ @logger.warn(message)
156
+ else
157
+ puts "[WARN] #{message}"
158
+ end
159
+ end
160
+
161
+ def setup_test_template_path!
162
+ # Override template path to use test fixtures if not already set
163
+ return if ENV['TASKER_TEMPLATE_PATH']
164
+
165
+ if Dir.exist?(@template_fixtures_path)
166
+ ENV['TASKER_TEMPLATE_PATH'] = @template_fixtures_path
167
+ log_debug("📁 Set TASKER_TEMPLATE_PATH to: #{@template_fixtures_path}")
168
+ else
169
+ log_warn("⚠️ Test template fixtures directory not found: #{@template_fixtures_path}")
170
+ end
171
+ end
172
+
173
+ def load_example_handler_files!
174
+ return unless Dir.exist?(@example_handlers_path)
175
+
176
+ handler_files = Dir.glob("#{@example_handlers_path}/**/*_handler.rb")
177
+ log_debug("🔍 Found #{handler_files.count} example handler files")
178
+
179
+ loaded_count = 0
180
+ handler_files.each do |handler_file|
181
+ # Use require instead of require_relative to avoid duplicate loading
182
+ require handler_file
183
+ loaded_count += 1
184
+ log_debug("✅ Loaded handler file: #{File.basename(handler_file)}")
185
+ rescue LoadError => e
186
+ log_warn("❌ Failed to load handler file #{handler_file}: #{e.message}")
187
+ rescue StandardError => e
188
+ log_warn("❌ Error loading handler file #{handler_file}: #{e.class} - #{e.message}")
189
+ end
190
+
191
+ log_info("📚 Loaded #{loaded_count}/#{handler_files.count} example handler files")
192
+
193
+ # TAS-65: Also load subscriber files
194
+ load_example_subscriber_files!
195
+ end
196
+
197
+ # TAS-65: Load example domain event subscriber files
198
+ def load_example_subscriber_files!
199
+ return unless Dir.exist?(@example_handlers_path)
200
+
201
+ subscriber_files = Dir.glob("#{@example_handlers_path}/**/*_subscriber.rb")
202
+ return if subscriber_files.empty?
203
+
204
+ log_debug("🔍 Found #{subscriber_files.count} example subscriber files")
205
+
206
+ loaded_count = 0
207
+ subscriber_files.each do |subscriber_file|
208
+ require subscriber_file
209
+ loaded_count += 1
210
+ log_debug("✅ Loaded subscriber file: #{File.basename(subscriber_file)}")
211
+ rescue LoadError => e
212
+ log_warn("❌ Failed to load subscriber file #{subscriber_file}: #{e.message}")
213
+ rescue StandardError => e
214
+ log_warn("❌ Error loading subscriber file #{subscriber_file}: #{e.class} - #{e.message}")
215
+ end
216
+
217
+ log_info("📚 Loaded #{loaded_count}/#{subscriber_files.count} example subscriber files")
218
+
219
+ # Auto-register loaded subscribers with the registry
220
+ register_loaded_subscribers!
221
+ end
222
+
223
+ # TAS-65: Auto-register any loaded subscriber classes with SubscriberRegistry
224
+ def register_loaded_subscribers!
225
+ registry = TaskerCore::DomainEvents::SubscriberRegistry.instance
226
+ registered_count = 0
227
+
228
+ ObjectSpace.each_object(Class) do |klass|
229
+ # Skip if not a BaseSubscriber subclass
230
+ next unless klass < TaskerCore::DomainEvents::BaseSubscriber
231
+ # Skip the BaseSubscriber itself
232
+ next if klass == TaskerCore::DomainEvents::BaseSubscriber
233
+
234
+ registry.register(klass)
235
+ registered_count += 1
236
+ log_debug("✅ Registered subscriber: #{klass.name}")
237
+ rescue StandardError => e
238
+ log_warn("❌ Failed to register subscriber #{klass}: #{e.message}")
239
+ end
240
+
241
+ log_info("📚 Registered #{registered_count} domain event subscribers") if registered_count.positive?
242
+ end
243
+
244
+ def verify_test_setup!
245
+ # Verify template path is set correctly
246
+ template_path = ENV.fetch('TASKER_TEMPLATE_PATH', nil)
247
+ if template_path.nil?
248
+ log_warn('⚠️ TASKER_TEMPLATE_PATH not set, template discovery may fail')
249
+ elsif !Dir.exist?(template_path)
250
+ log_warn("⚠️ TASKER_TEMPLATE_PATH directory does not exist: #{template_path}")
251
+ else
252
+ yaml_files = Dir.glob("#{template_path}/*.yaml").count
253
+ log_debug("📄 Found #{yaml_files} YAML template files in #{template_path}")
254
+ end
255
+
256
+ # Verify example handlers directory exists
257
+ unless Dir.exist?(@example_handlers_path)
258
+ log_warn("⚠️ Example handlers directory not found: #{@example_handlers_path}")
259
+ return
260
+ end
261
+
262
+ # Count available handler classes
263
+ handler_count = loaded_handler_count
264
+ if handler_count.zero?
265
+ log_warn('⚠️ No example handler classes found after loading')
266
+ else
267
+ log_debug("🎯 #{handler_count} example handler classes are available")
268
+ end
269
+ end
270
+ end
271
+
272
+ # Main interface - call this to conditionally load test environment
273
+ def self.load_if_test!
274
+ ConditionalLoader.instance.load_if_test_environment!
275
+ end
276
+
277
+ # Check if test environment is loaded
278
+ def self.loaded?
279
+ ConditionalLoader.instance.loaded
280
+ end
281
+
282
+ # Get test environment info for debugging
283
+ def self.info
284
+ loader = ConditionalLoader.instance
285
+ base_info = {
286
+ should_load: loader.should_load_test_environment?,
287
+ loaded: loader.loaded,
288
+ handler_count: loader.loaded_handler_count
289
+ }
290
+
291
+ if loader.loaded
292
+ base_info.merge(loader.test_template_info)
293
+ else
294
+ base_info
295
+ end
296
+ end
297
+
298
+ # Get loaded handler names for debugging
299
+ def self.handler_names
300
+ ConditionalLoader.instance.loaded_handler_names
301
+ end
302
+
303
+ # TAS-65: Get loaded subscriber names for debugging
304
+ def self.subscriber_names
305
+ ConditionalLoader.instance.loaded_subscriber_names
306
+ end
307
+
308
+ # TAS-65: Get subscriber count
309
+ def self.subscriber_count
310
+ ConditionalLoader.instance.loaded_subscriber_count
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module TaskerCore
6
+ # Unified structured logging via Rust tracing FFI
7
+ #
8
+ # TAS-29 Phase 6: Replace Ruby's Logger with FFI calls to Rust's tracing
9
+ # infrastructure, enabling unified structured logging across Ruby and Rust.
10
+ #
11
+ # ## Architecture
12
+ #
13
+ # Ruby Handler → Tracing.info() → FFI Bridge → Rust tracing → OpenTelemetry
14
+ #
15
+ # ## Usage
16
+ #
17
+ # # Simple message
18
+ # Tracing.info("Task initialized")
19
+ #
20
+ # # With structured fields
21
+ # Tracing.info("Step completed", {
22
+ # correlation_id: correlation_id,
23
+ # task_uuid: task.uuid,
24
+ # step_uuid: step.uuid,
25
+ # namespace: "order_fulfillment",
26
+ # operation: "validate_inventory",
27
+ # duration_ms: elapsed_ms
28
+ # })
29
+ #
30
+ # # Error logging
31
+ # Tracing.error("Payment processing failed", {
32
+ # correlation_id: correlation_id,
33
+ # error_class: error.class.name,
34
+ # error_message: error.message
35
+ # })
36
+ #
37
+ # ## Structured Field Conventions (TAS-29 Phase 6.2)
38
+ #
39
+ # **Required fields** (when applicable):
40
+ # - correlation_id: Always include for distributed tracing
41
+ # - task_uuid: Include for task-level operations
42
+ # - step_uuid: Include for step-level operations
43
+ # - namespace: Include for namespace-specific operations
44
+ # - operation: Operation identifier (e.g., "validate_inventory")
45
+ #
46
+ # **Optional fields**:
47
+ # - duration_ms: For timed operations
48
+ # - error_class, error_message: For error context
49
+ # - entity_id: For domain entity operations
50
+ # - retry_count: For retryable operations
51
+ #
52
+ # ## Log Level Guidelines (TAS-29 Phase 6.1)
53
+ #
54
+ # - ERROR: Unrecoverable failures requiring intervention
55
+ # - WARN: Degraded operation, retryable failures
56
+ # - INFO: Lifecycle events, state transitions
57
+ # - DEBUG: Detailed diagnostic information
58
+ # - TRACE: Very verbose, hot-path entry/exit
59
+ #
60
+ class Tracing
61
+ include Singleton
62
+
63
+ # Log ERROR level message with structured fields
64
+ #
65
+ # @param message [String] Log message
66
+ # @param fields [Hash] Structured fields (correlation_id, task_uuid, etc.)
67
+ # @return [void]
68
+ def self.error(message, fields = {})
69
+ fields_hash = normalize_fields(fields)
70
+ TaskerCore::FFI.log_error(message.to_s, fields_hash)
71
+ rescue StandardError => e
72
+ # Fallback to stderr if FFI logging fails
73
+ warn "FFI logging failed: #{e.message}"
74
+ warn "#{message} | #{fields.inspect}"
75
+ end
76
+
77
+ # Log WARN level message with structured fields
78
+ #
79
+ # @param message [String] Log message
80
+ # @param fields [Hash] Structured fields
81
+ # @return [void]
82
+ def self.warn(message, fields = {})
83
+ fields_hash = normalize_fields(fields)
84
+ TaskerCore::FFI.log_warn(message.to_s, fields_hash)
85
+ rescue StandardError => e
86
+ warn "FFI logging failed: #{e.message}"
87
+ warn "#{message} | #{fields.inspect}"
88
+ end
89
+
90
+ # Log INFO level message with structured fields
91
+ #
92
+ # @param message [String] Log message
93
+ # @param fields [Hash] Structured fields
94
+ # @return [void]
95
+ def self.info(message, fields = {})
96
+ fields_hash = normalize_fields(fields)
97
+ TaskerCore::FFI.log_info(message.to_s, fields_hash)
98
+ rescue StandardError => e
99
+ warn "FFI logging failed: #{e.message}"
100
+ warn "#{message} | #{fields.inspect}"
101
+ end
102
+
103
+ # Log DEBUG level message with structured fields
104
+ #
105
+ # @param message [String] Log message
106
+ # @param fields [Hash] Structured fields
107
+ # @return [void]
108
+ def self.debug(message, fields = {})
109
+ fields_hash = normalize_fields(fields)
110
+ TaskerCore::FFI.log_debug(message.to_s, fields_hash)
111
+ rescue StandardError => e
112
+ warn "FFI logging failed: #{e.message}"
113
+ warn "#{message} | #{fields.inspect}"
114
+ end
115
+
116
+ # Log TRACE level message with structured fields
117
+ #
118
+ # @param message [String] Log message
119
+ # @param fields [Hash] Structured fields
120
+ # @return [void]
121
+ def self.trace(message, fields = {})
122
+ fields_hash = normalize_fields(fields)
123
+ TaskerCore::FFI.log_trace(message.to_s, fields_hash)
124
+ rescue StandardError => e
125
+ warn "FFI logging failed: #{e.message}"
126
+ warn "#{message} | #{fields.inspect}"
127
+ end
128
+
129
+ # Normalize fields to string keys and values for FFI
130
+ #
131
+ # @param fields [Hash] Input fields
132
+ # @return [Hash<String, String>] Normalized fields
133
+ # @api private
134
+ def self.normalize_fields(fields)
135
+ return {} if fields.nil? || fields.empty?
136
+
137
+ fields.transform_keys(&:to_s).transform_values { |v| normalize_value(v) }
138
+ end
139
+
140
+ # Normalize a value to string for FFI
141
+ #
142
+ # @param value [Object] Value to normalize
143
+ # @return [String] String representation
144
+ # @api private
145
+ def self.normalize_value(value)
146
+ case value
147
+ when nil
148
+ 'nil'
149
+ when String
150
+ value
151
+ when Symbol
152
+ value.to_s
153
+ when Numeric, TrueClass, FalseClass
154
+ value.to_s
155
+ when Exception
156
+ "#{value.class}: #{value.message}"
157
+ else
158
+ value.to_s
159
+ end
160
+ rescue StandardError
161
+ '<serialization_error>'
162
+ end
163
+
164
+ private_class_method :normalize_fields, :normalize_value
165
+ end
166
+ end