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.
- checksums.yaml +7 -0
- data/README.md +55 -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/client.rb +165 -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/test_environment.rb +313 -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 +161 -0
- 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
|