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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8ed4ebd9912ca9a7b20f052991f07c260be1809876e1727b7a0e36ec5d8d6c52
4
+ data.tar.gz: b1f6d776dbb3e03ca2c488d7d611ecba12c689e265036edcbe206a6718c9feb1
5
+ SHA512:
6
+ metadata.gz: 7253ebf0a02c9888fe2b0f23a380fba34d3ec43145496df714b390ec95050f0fede2b01220e72972d4329f270f213aa8bedb8f1df38506a81274c3f973ce1ae2
7
+ data.tar.gz: 8ee494e98a9a38c12aa51aae18bdfc7f1b7287cce9f6eaaf7674ead230a2fbec44401b091e702172737658c9e1799a819ae34ce2814311a59eb1347fdb2c8185
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # Tasker Core Ruby Bindings
2
+
3
+ Ruby FFI bindings for the high-performance Tasker Core Rust orchestration engine.
4
+
5
+ ## Status
6
+
7
+ Production ready. Ruby FFI bindings provide full step handler execution via Magnus.
8
+
9
+ ## Development Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ bundle install
14
+
15
+ # Compile the Rust extension (requires Ruby dev environment)
16
+ rake compile
17
+
18
+ # Run tests
19
+ rake spec
20
+
21
+ # Full development setup
22
+ rake setup
23
+ ```
24
+
25
+ ## Architecture
26
+
27
+ This gem follows the **delegation-based architecture**:
28
+
29
+ ```
30
+ Rails Engine ↔ tasker-core-rb (FFI) ↔ tasker-core (Performance Core)
31
+ ```
32
+
33
+ - **Rails**: Business logic and step execution
34
+ - **Rust**: High-performance orchestration and dependency resolution
35
+ - **Ruby Bindings**: Safe FFI bridge between the two
36
+
37
+ ## Performance Targets
38
+
39
+ - **10-100x faster** dependency resolution vs PostgreSQL functions
40
+ - **<1ms FFI overhead** per orchestration call
41
+ - **>10k events/sec** cross-language event processing
42
+
43
+ ## Requirements
44
+
45
+ - **Ruby**: 3.0+ with development headers
46
+ - **Rust**: 1.70+ with magnus dependencies
47
+ - **PostgreSQL**: 12+ for database operations
48
+
49
+ ## Contributing
50
+
51
+ This is part of the larger tasker-systems monorepo. See the main project documentation for development guidelines and contribution instructions.
52
+
53
+ ---
54
+
55
+ 🦀 **Built with Rust + Magnus for maximum performance and safety**
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module BatchProcessing
5
+ # Ruby equivalent of Rust's BatchAggregationScenario
6
+ #
7
+ # Detects whether a batchable step created batch workers or returned NoBatches outcome.
8
+ # Uses DependencyResultsWrapper to access parent step results.
9
+ #
10
+ # This helper distinguishes two scenarios:
11
+ # - **NoBatches**: Batchable step returned NoBatches outcome, only placeholder worker exists
12
+ # - **WithBatches**: Batchable step created real batch workers for parallel processing
13
+ #
14
+ # @example Detecting aggregation scenario
15
+ # scenario = BatchAggregationScenario.detect(
16
+ # sequence_step,
17
+ # 'analyze_dataset',
18
+ # 'process_batch_'
19
+ # )
20
+ #
21
+ # if scenario.no_batches?
22
+ # # Handle NoBatches scenario - use batchable result directly
23
+ # return scenario.batchable_result
24
+ # else
25
+ # # Handle WithBatches scenario - aggregate worker results
26
+ # total = scenario.batch_results.values.sum
27
+ # return { total: total, worker_count: scenario.worker_count }
28
+ # end
29
+ class BatchAggregationScenario
30
+ attr_reader :type, :batchable_result, :batch_results, :worker_count
31
+
32
+ # Detect aggregation scenario from step dependencies
33
+ #
34
+ # @param dependency_results [DependencyResultsWrapper] Dependency results wrapper
35
+ # @param batchable_step_name [String] Name of the batchable step
36
+ # @param batch_worker_prefix [String] Prefix for batch worker step names
37
+ # @return [BatchAggregationScenario] Detected scenario
38
+ def self.detect(dependency_results, batchable_step_name, batch_worker_prefix)
39
+ # dependency_results is already a DependencyResultsWrapper - pass it directly
40
+ new(dependency_results, batchable_step_name, batch_worker_prefix)
41
+ end
42
+
43
+ # Initialize aggregation scenario
44
+ #
45
+ # @param dependency_results [DependencyResultsWrapper] Dependency results wrapper
46
+ # @param batchable_step_name [String] Name of the batchable step
47
+ # @param batch_worker_prefix [String] Prefix for batch worker step names
48
+ def initialize(dependency_results, batchable_step_name, batch_worker_prefix)
49
+ # Get the batchable step result (extract the 'result' field)
50
+ @batchable_result = dependency_results.get_results(batchable_step_name)
51
+
52
+ # Find all batch worker results by prefix matching (extract 'result' field from each)
53
+ @batch_results = {}
54
+ dependency_results.each_key do |step_name|
55
+ if step_name.start_with?(batch_worker_prefix)
56
+ @batch_results[step_name] = dependency_results.get_results(step_name)
57
+ end
58
+ end
59
+
60
+ # Determine scenario type based on batch worker count
61
+ if @batch_results.empty?
62
+ @type = :no_batches
63
+ @worker_count = 0
64
+ else
65
+ @type = :with_batches
66
+ @worker_count = @batch_results.size
67
+ end
68
+ end
69
+
70
+ # Check if scenario is NoBatches
71
+ #
72
+ # @return [Boolean] True if batchable step returned NoBatches outcome
73
+ def no_batches?
74
+ @type == :no_batches
75
+ end
76
+
77
+ # Check if scenario is WithBatches
78
+ #
79
+ # @return [Boolean] True if batch workers were created
80
+ def with_batches?
81
+ @type == :with_batches
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaskerCore
4
+ module BatchProcessing
5
+ # Ruby equivalent of Rust's BatchWorkerContext
6
+ #
7
+ # Extracts cursor config from workflow_step.inputs via WorkflowStepWrapper.
8
+ # This mirrors the Rust implementation which deserializes BatchWorkerInputs
9
+ # from the workflow_step.inputs field.
10
+ #
11
+ # @example Checking if worker is no-op
12
+ # context = BatchWorkerContext.from_step_data(sequence_step)
13
+ # if context.no_op?
14
+ # # Handle placeholder worker for NoBatches scenario
15
+ # return success_result(message: 'No-op worker completed')
16
+ # end
17
+ #
18
+ # @example Accessing cursor configuration
19
+ # context = BatchWorkerContext.from_step_data(sequence_step)
20
+ # start = context.start_cursor # => 0
21
+ # end_pos = context.end_cursor # => 1000
22
+ # batch_id = context.batch_id # => "001"
23
+ #
24
+ # == Cursor Flexibility
25
+ #
26
+ # Cursors are intentionally flexible to support diverse business logic scenarios.
27
+ # The system validates numeric cursors when both values are integers, but cursors
28
+ # can also be alphanumeric strings, timestamps, UUIDs, or any comparable type that
29
+ # makes sense for your data partitioning strategy.
30
+ #
31
+ # With great power comes great responsibility: ensure your cursor implementation
32
+ # matches your data source's capabilities and that your handler logic correctly
33
+ # interprets the cursor boundaries.
34
+ #
35
+ # @example Numeric cursors (validated for ordering)
36
+ # cursor: {
37
+ # batch_id: '001',
38
+ # start_cursor: 0, # Integer cursors are validated
39
+ # end_cursor: 1000
40
+ # }
41
+ #
42
+ # @example Alphanumeric cursors (developer responsibility)
43
+ # cursor: {
44
+ # batch_id: '002',
45
+ # start_cursor: 'A', # String cursors for alphabetical ranges
46
+ # end_cursor: 'M'
47
+ # }
48
+ #
49
+ # @example UUID-based cursors (developer responsibility)
50
+ # cursor: {
51
+ # batch_id: '003',
52
+ # start_cursor: '00000000-0000-0000-0000-000000000000',
53
+ # end_cursor: '88888888-8888-8888-8888-888888888888'
54
+ # }
55
+ #
56
+ # @example Timestamp cursors (developer responsibility)
57
+ # cursor: {
58
+ # batch_id: '004',
59
+ # start_cursor: '2024-01-01T00:00:00Z',
60
+ # end_cursor: '2024-01-31T23:59:59Z'
61
+ # }
62
+ class BatchWorkerContext
63
+ attr_reader :cursor, :batch_metadata, :is_no_op, :checkpoint
64
+
65
+ # Extract context from WorkflowStepWrapper
66
+ #
67
+ # @param workflow_step [WorkflowStepWrapper] Workflow step wrapper
68
+ # @return [BatchWorkerContext] Extracted context
69
+ def self.from_step_data(workflow_step)
70
+ new(workflow_step)
71
+ end
72
+
73
+ # Initialize batch worker context from workflow step
74
+ #
75
+ # Reads BatchWorkerInputs from workflow_step.inputs (instance data)
76
+ # not from step_definition.handler.initialization (template data).
77
+ #
78
+ # @param workflow_step [WorkflowStepWrapper] Workflow step wrapper
79
+ def initialize(workflow_step)
80
+ # Access inputs via WorkflowStepWrapper
81
+ # Use ActiveSupport deep_symbolize_keys for clean key access
82
+ inputs = (workflow_step.inputs || {}).deep_symbolize_keys
83
+
84
+ @is_no_op = inputs[:is_no_op] == true
85
+
86
+ # TAS-125: Extract checkpoint data from workflow step
87
+ # Checkpoint contains persisted progress from previous yields
88
+ # Use with_indifferent_access for flexible string/symbol key access
89
+ @checkpoint = (workflow_step.checkpoint || {}).with_indifferent_access
90
+
91
+ if @is_no_op
92
+ # Placeholder worker - minimal context
93
+ @cursor = {}
94
+ @batch_metadata = {}
95
+ else
96
+ @cursor = inputs[:cursor] || {}
97
+ @batch_metadata = inputs[:batch_metadata] || {}
98
+
99
+ validate_cursor!
100
+ end
101
+ end
102
+
103
+ # Get the starting cursor in the dataset (inclusive)
104
+ #
105
+ # @return [Integer] Starting cursor position
106
+ def start_cursor
107
+ cursor[:start_cursor].to_i
108
+ end
109
+
110
+ # Get the ending cursor in the dataset (exclusive)
111
+ #
112
+ # @return [Integer] Ending cursor position
113
+ def end_cursor
114
+ cursor[:end_cursor].to_i
115
+ end
116
+
117
+ # Get the batch identifier
118
+ #
119
+ # @return [String] Batch ID (e.g., "001", "002", "000" for no-op)
120
+ def batch_id
121
+ cursor[:batch_id] || 'unknown'
122
+ end
123
+
124
+ # Check if this is a no-op/placeholder worker
125
+ #
126
+ # @return [Boolean] True if this worker should skip processing
127
+ def no_op?
128
+ @is_no_op
129
+ end
130
+
131
+ # TAS-125: Get checkpoint cursor from previous yield
132
+ #
133
+ # When a handler yields a checkpoint, the cursor position is persisted.
134
+ # On re-dispatch, this returns that cursor position to resume from.
135
+ #
136
+ # @return [Integer, String, Hash, nil] Last persisted cursor position, or nil if no checkpoint
137
+ #
138
+ # @example Resume from checkpoint
139
+ # start = batch_ctx.checkpoint_cursor || batch_ctx.start_cursor
140
+ def checkpoint_cursor
141
+ checkpoint[:cursor]
142
+ end
143
+
144
+ # TAS-125: Get accumulated results from previous checkpoint yield
145
+ #
146
+ # When a handler yields a checkpoint with accumulated_results, those
147
+ # partial aggregations are persisted. On re-dispatch, this returns
148
+ # them so the handler can continue accumulating.
149
+ #
150
+ # @return [Hash, nil] Partial aggregations from previous yields
151
+ #
152
+ # @example Continue accumulating totals
153
+ # accumulated = batch_ctx.accumulated_results || { 'total' => 0 }
154
+ # accumulated['total'] += item.value
155
+ def accumulated_results
156
+ checkpoint[:accumulated_results]
157
+ end
158
+
159
+ # TAS-125: Check if checkpoint exists
160
+ #
161
+ # Use this to determine if this is a fresh execution or a resumption
162
+ # from a previous checkpoint yield.
163
+ #
164
+ # @return [Boolean] True if checkpoint data exists
165
+ #
166
+ # @example Conditional initialization
167
+ # if batch_ctx.has_checkpoint?
168
+ # # Resuming from previous checkpoint
169
+ # start = batch_ctx.checkpoint_cursor
170
+ # else
171
+ # # Fresh start
172
+ # start = batch_ctx.start_cursor
173
+ # end
174
+ def has_checkpoint?
175
+ checkpoint.present? && checkpoint[:cursor].present?
176
+ end
177
+
178
+ # TAS-125: Get items processed count from checkpoint
179
+ #
180
+ # Returns the cumulative count of items processed across all yields.
181
+ #
182
+ # @return [Integer] Items processed so far, or 0 if no checkpoint
183
+ def checkpoint_items_processed
184
+ checkpoint[:items_processed] || 0
185
+ end
186
+
187
+ private
188
+
189
+ # Validate cursor configuration for real workers
190
+ #
191
+ # @raise [ArgumentError] If cursor configuration is invalid
192
+ def validate_cursor!
193
+ return if @is_no_op
194
+
195
+ raise ArgumentError, 'Missing cursor configuration' if cursor.empty?
196
+ raise ArgumentError, 'Missing batch_id' unless cursor[:batch_id]
197
+ raise ArgumentError, 'Missing start_cursor' unless cursor.key?(:start_cursor)
198
+ raise ArgumentError, 'Missing end_cursor' unless cursor.key?(:end_cursor)
199
+
200
+ # Extract cursor values for validation
201
+ start_val = cursor[:start_cursor]
202
+ end_val = cursor[:end_cursor]
203
+
204
+ # Validate numeric cursors if they're integers
205
+ # (supports both integer and other types like timestamps/UUIDs)
206
+ return unless start_val.is_a?(Integer) || end_val.is_a?(Integer)
207
+
208
+ # If one is integer, both should be integers
209
+ unless start_val.is_a?(Integer) && end_val.is_a?(Integer)
210
+ raise ArgumentError,
211
+ "Mixed cursor types not allowed: start=#{start_val.class}, end=#{end_val.class}"
212
+ end
213
+
214
+ # Validate non-negative values
215
+ if start_val.negative?
216
+ raise ArgumentError,
217
+ "start_cursor must be non-negative, got #{start_val}"
218
+ end
219
+ if end_val.negative?
220
+ raise ArgumentError,
221
+ "end_cursor must be non-negative, got #{end_val}"
222
+ end
223
+
224
+ # Validate logical ordering
225
+ if start_val > end_val
226
+ raise ArgumentError,
227
+ "start_cursor (#{start_val}) must be <= end_cursor (#{end_val})"
228
+ end
229
+
230
+ # Warn about zero-length ranges (likely a bug)
231
+ return unless start_val == end_val
232
+
233
+ warn "WARNING: Zero-length cursor range (#{start_val} == #{end_val}) " \
234
+ "- worker will process no data. batch_id=#{cursor[:batch_id]}"
235
+ end
236
+ end
237
+ end
238
+ end