ruby_reactor 0.2.0 → 0.3.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 +4 -4
- data/README.md +132 -0
- data/Rakefile +2 -2
- data/documentation/data_pipelines.md +90 -84
- data/documentation/testing.md +812 -0
- data/lib/ruby_reactor/configuration.rb +1 -1
- data/lib/ruby_reactor/context.rb +13 -5
- data/lib/ruby_reactor/context_serializer.rb +70 -4
- data/lib/ruby_reactor/dsl/map_builder.rb +6 -2
- data/lib/ruby_reactor/dsl/reactor.rb +3 -2
- data/lib/ruby_reactor/error/step_failure_error.rb +5 -2
- data/lib/ruby_reactor/executor/result_handler.rb +9 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +26 -8
- data/lib/ruby_reactor/executor/step_executor.rb +24 -99
- data/lib/ruby_reactor/executor.rb +3 -13
- data/lib/ruby_reactor/map/collector.rb +72 -33
- data/lib/ruby_reactor/map/dispatcher.rb +162 -0
- data/lib/ruby_reactor/map/element_executor.rb +103 -114
- data/lib/ruby_reactor/map/execution.rb +18 -4
- data/lib/ruby_reactor/map/helpers.rb +4 -3
- data/lib/ruby_reactor/map/result_enumerator.rb +105 -0
- data/lib/ruby_reactor/reactor.rb +174 -16
- data/lib/ruby_reactor/rspec/helpers.rb +17 -0
- data/lib/ruby_reactor/rspec/matchers.rb +256 -0
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +85 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +625 -0
- data/lib/ruby_reactor/rspec.rb +18 -0
- data/lib/ruby_reactor/{async_router.rb → sidekiq_adapter.rb} +15 -10
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +1 -3
- data/lib/ruby_reactor/step/compose_step.rb +0 -1
- data/lib/ruby_reactor/step/map_step.rb +52 -27
- data/lib/ruby_reactor/storage/redis_adapter.rb +59 -0
- data/lib/ruby_reactor/template/dynamic_source.rb +32 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +32 -24
- data/lib/ruby_reactor.rb +70 -10
- metadata +12 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Map
|
|
5
|
+
class Dispatcher
|
|
6
|
+
extend Helpers
|
|
7
|
+
|
|
8
|
+
def self.perform(arguments)
|
|
9
|
+
arguments = arguments.transform_keys(&:to_sym)
|
|
10
|
+
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
11
|
+
|
|
12
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
13
|
+
|
|
14
|
+
# Load parent context to resolve source
|
|
15
|
+
parent_context = load_parent_context_from_storage(
|
|
16
|
+
arguments[:parent_context_id],
|
|
17
|
+
parent_reactor_class_name,
|
|
18
|
+
storage
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Initialize metadata if first run
|
|
22
|
+
initialize_map_metadata(arguments, storage) unless arguments[:continuation]
|
|
23
|
+
|
|
24
|
+
# Resolve Source
|
|
25
|
+
# We need to resolve the source to know what we are iterating.
|
|
26
|
+
# Strict "Array Only" rule means we expect an Array-like object or we handle the
|
|
27
|
+
# "Query Builder" result if user used it.
|
|
28
|
+
source = resolve_source(arguments, parent_context)
|
|
29
|
+
|
|
30
|
+
# Dispatch next batch
|
|
31
|
+
dispatch_batch(source, arguments, parent_context, storage)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.initialize_map_metadata(arguments, storage)
|
|
35
|
+
map_id = arguments[:map_id]
|
|
36
|
+
reactor_class_name = arguments[:parent_reactor_class_name]
|
|
37
|
+
|
|
38
|
+
# Reset or set initial offset. Use NX to act as a mutex/guard against duplicate initialization.
|
|
39
|
+
storage.set_map_offset_if_not_exists(map_id, 0, reactor_class_name)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.resolve_source(arguments, context)
|
|
43
|
+
# Arguments has :source which is a Template::Input or similar.
|
|
44
|
+
# We need to resolve it against the context.
|
|
45
|
+
source_template = arguments[:source]
|
|
46
|
+
|
|
47
|
+
# Fallback: look up from step config if missing (e.g. called from ElementExecutor)
|
|
48
|
+
if source_template.nil? && context
|
|
49
|
+
step_name = arguments[:step_name]
|
|
50
|
+
step_config = context.reactor_class.steps[step_name.to_sym]
|
|
51
|
+
source_template = step_config.arguments[:source][:source]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# If source is packaged in arguments as a value (deserialized)
|
|
55
|
+
return source_template if source_template.is_a?(Array)
|
|
56
|
+
|
|
57
|
+
# Resolve template
|
|
58
|
+
return source_template.resolve(context) if source_template.respond_to?(:resolve)
|
|
59
|
+
|
|
60
|
+
source_template
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.dispatch_batch(source, arguments, parent_context, storage)
|
|
64
|
+
map_id = arguments[:map_id]
|
|
65
|
+
reactor_class_name = arguments[:parent_reactor_class_name]
|
|
66
|
+
|
|
67
|
+
# Fail Fast Check
|
|
68
|
+
if arguments[:fail_fast]
|
|
69
|
+
failed_context_id = storage.retrieve_map_failed_context_id(map_id, reactor_class_name)
|
|
70
|
+
return if failed_context_id
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
batch_size = arguments[:batch_size] || source.size # Default to all if no batch_size (async=true only)
|
|
74
|
+
|
|
75
|
+
# Atomically reserve a batch
|
|
76
|
+
new_offset = storage.increment_map_offset(map_id, batch_size, reactor_class_name)
|
|
77
|
+
current_offset = new_offset - batch_size
|
|
78
|
+
|
|
79
|
+
batch_elements = if source.is_a?(Array)
|
|
80
|
+
source.slice(current_offset, batch_size) || []
|
|
81
|
+
elsif source.respond_to?(:offset) && source.respond_to?(:limit)
|
|
82
|
+
# Optimized for ActiveRecord and similar query builders
|
|
83
|
+
source.offset(current_offset).limit(batch_size).to_a
|
|
84
|
+
else
|
|
85
|
+
# Fallback for generic Enumerable
|
|
86
|
+
# This is inefficient for huge sets if not Array, but compliant
|
|
87
|
+
source.drop(current_offset).take(batch_size)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
return if batch_elements.empty?
|
|
91
|
+
|
|
92
|
+
# Queue Jobs
|
|
93
|
+
queue_options = {
|
|
94
|
+
map_id: map_id,
|
|
95
|
+
arguments: arguments,
|
|
96
|
+
context: parent_context,
|
|
97
|
+
reactor_class_info: resolve_reactor_class_info(arguments, parent_context),
|
|
98
|
+
step_name: arguments[:step_name]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
batch_elements.each_with_index do |element, i|
|
|
102
|
+
absolute_index = current_offset + i
|
|
103
|
+
queue_element_job(element, absolute_index, queue_options)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.queue_element_job(element, index, options)
|
|
108
|
+
arguments = options[:arguments]
|
|
109
|
+
context = options[:context]
|
|
110
|
+
|
|
111
|
+
# Resolve mappings
|
|
112
|
+
mappings_template = arguments[:argument_mappings]
|
|
113
|
+
|
|
114
|
+
# Fallback: look up from step config if missing (e.g. called from ElementExecutor)
|
|
115
|
+
if mappings_template.nil? && context
|
|
116
|
+
step_name = options[:step_name] || arguments[:step_name]
|
|
117
|
+
step_config = context.reactor_class.steps[step_name.to_sym]
|
|
118
|
+
mappings_template = step_config.arguments[:argument_mappings]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
mappings = if mappings_template.respond_to?(:resolve)
|
|
122
|
+
mappings_template.resolve(context)
|
|
123
|
+
else
|
|
124
|
+
mappings_template || {}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fix for weird structure observed in fallback (wrapped in :source -> Template::Value)
|
|
128
|
+
if mappings.key?(:source) && mappings[:source].respond_to?(:value) && mappings[:source].value.is_a?(Hash)
|
|
129
|
+
mappings = mappings[:source].value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
mapped_inputs = build_element_inputs(mappings, context, element)
|
|
133
|
+
serialized_inputs = ContextSerializer.serialize_value(mapped_inputs)
|
|
134
|
+
|
|
135
|
+
RubyReactor.configuration.async_router.perform_map_element_async(
|
|
136
|
+
map_id: options[:map_id],
|
|
137
|
+
element_id: "#{options[:map_id]}:#{index}",
|
|
138
|
+
index: index,
|
|
139
|
+
serialized_inputs: serialized_inputs,
|
|
140
|
+
reactor_class_info: options[:reactor_class_info],
|
|
141
|
+
strict_ordering: arguments[:strict_ordering],
|
|
142
|
+
parent_context_id: context.context_id,
|
|
143
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
144
|
+
step_name: options[:step_name].to_s,
|
|
145
|
+
batch_size: arguments[:batch_size], # Passed to worker so it knows to trigger next batch?
|
|
146
|
+
fail_fast: arguments[:fail_fast]
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.resolve_reactor_class_info(arguments, context)
|
|
151
|
+
mapped_reactor_class = arguments[:mapped_reactor_class]
|
|
152
|
+
step_name = arguments[:step_name]
|
|
153
|
+
|
|
154
|
+
if mapped_reactor_class.respond_to?(:name)
|
|
155
|
+
{ "type" => "class", "name" => mapped_reactor_class.name }
|
|
156
|
+
else
|
|
157
|
+
{ "type" => "inline", "parent" => context.reactor_class.name, "step" => step_name.to_s }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -5,153 +5,142 @@ module RubyReactor
|
|
|
5
5
|
class ElementExecutor
|
|
6
6
|
extend Helpers
|
|
7
7
|
|
|
8
|
-
# rubocop:disable Metrics/MethodLength
|
|
9
8
|
def self.perform(arguments)
|
|
10
9
|
arguments = arguments.transform_keys(&:to_sym)
|
|
11
|
-
map_id = arguments[:map_id]
|
|
12
|
-
_element_id = arguments[:element_id]
|
|
13
|
-
index = arguments[:index]
|
|
14
|
-
serialized_inputs = arguments[:serialized_inputs]
|
|
15
|
-
reactor_class_info = arguments[:reactor_class_info]
|
|
16
|
-
strict_ordering = arguments[:strict_ordering]
|
|
17
|
-
parent_context_id = arguments[:parent_context_id]
|
|
18
|
-
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
19
|
-
step_name = arguments[:step_name]
|
|
20
|
-
batch_size = arguments[:batch_size]
|
|
21
|
-
# rubocop:enable Metrics/MethodLength
|
|
22
|
-
serialized_context = arguments[:serialized_context]
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
11
|
+
context = hydrate_or_create_context(arguments)
|
|
12
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
13
|
+
storage.store_map_element_context_id(arguments[:map_id], context.context_id,
|
|
14
|
+
arguments[:parent_reactor_class_name])
|
|
15
|
+
|
|
16
|
+
return if check_fail_fast?(arguments, storage)
|
|
17
|
+
|
|
18
|
+
executor = Executor.new(context.reactor_class, {}, context)
|
|
19
|
+
arguments[:serialized_context] ? executor.resume_execution : executor.execute
|
|
20
|
+
|
|
21
|
+
handle_result(executor.result, arguments, context, storage, executor)
|
|
22
|
+
finalize_execution(arguments, storage)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.load_parent_context(arguments, reactor_class_name, storage)
|
|
26
|
+
parent_context_data = storage.retrieve_context(arguments[:parent_context_id], reactor_class_name)
|
|
27
|
+
parent_reactor_class = Object.const_get(reactor_class_name)
|
|
28
|
+
parent_context = Context.new(
|
|
29
|
+
ContextSerializer.deserialize_value(parent_context_data["inputs"]),
|
|
30
|
+
parent_reactor_class
|
|
31
|
+
)
|
|
32
|
+
parent_context.context_id = arguments[:parent_context_id]
|
|
33
|
+
parent_context
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Legacy helpers resolved_next_element, build_serialized_inputs, queue_element_job
|
|
37
|
+
# are REMOVED as they are no longer used for self-queuing.
|
|
38
|
+
|
|
39
|
+
# Basic helper to build inputs for the CURRENT element (still needed for perform)
|
|
40
|
+
# Wait, perform uses `serialized_inputs` passed to it.
|
|
41
|
+
# We don't need `build_element_inputs` here?
|
|
42
|
+
# `perform` uses `params[:serialized_inputs]`.
|
|
43
|
+
# So we can remove input building helpers too?
|
|
44
|
+
# Let's check if they are used elsewhere.
|
|
45
|
+
# `resolve_reactor_class` is used in `perform`.
|
|
46
|
+
# `build_element_inputs` is likely in Helpers or mixed in?
|
|
47
|
+
|
|
48
|
+
# rubocop:disable Style/IdenticalConditionalBranches
|
|
49
|
+
def self.hydrate_or_create_context(arguments)
|
|
50
|
+
if arguments[:serialized_context]
|
|
51
|
+
context = ContextSerializer.deserialize(arguments[:serialized_context])
|
|
26
52
|
context.map_metadata = arguments
|
|
27
|
-
reactor_class = context.reactor_class
|
|
28
|
-
else
|
|
29
|
-
# Deserialize inputs
|
|
30
|
-
inputs = ContextSerializer.deserialize_value(serialized_inputs)
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
if context.inputs.empty? && arguments[:serialized_inputs]
|
|
55
|
+
context.inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
|
|
56
|
+
end
|
|
57
|
+
context
|
|
58
|
+
else
|
|
59
|
+
inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
|
|
60
|
+
reactor_class = resolve_reactor_class(arguments[:reactor_class_info])
|
|
34
61
|
|
|
35
|
-
# Create context
|
|
36
62
|
context = Context.new(inputs, reactor_class)
|
|
37
|
-
context.parent_context_id = parent_context_id
|
|
63
|
+
context.parent_context_id = arguments[:parent_context_id]
|
|
38
64
|
context.map_metadata = arguments
|
|
65
|
+
context
|
|
39
66
|
end
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Style/IdenticalConditionalBranches
|
|
40
69
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Execute
|
|
45
|
-
executor = Executor.new(reactor_class, {}, context)
|
|
70
|
+
def self.check_fail_fast?(arguments, storage)
|
|
71
|
+
return false unless arguments[:fail_fast]
|
|
46
72
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
else
|
|
50
|
-
executor.execute
|
|
51
|
-
end
|
|
73
|
+
map_id = arguments[:map_id]
|
|
74
|
+
parent_reactor_class_name = arguments[:parent_reactor_class_name]
|
|
52
75
|
|
|
53
|
-
|
|
76
|
+
failed_context_id = storage.retrieve_map_failed_context_id(map_id, parent_reactor_class_name)
|
|
77
|
+
return false unless failed_context_id
|
|
54
78
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
# Skip execution
|
|
80
|
+
finalize_execution(arguments, storage)
|
|
81
|
+
true
|
|
82
|
+
end
|
|
59
83
|
|
|
60
|
-
|
|
84
|
+
def self.handle_result(result, arguments, context, storage, executor)
|
|
85
|
+
return if result.is_a?(RetryQueuedResult)
|
|
61
86
|
|
|
62
|
-
|
|
87
|
+
map_id = arguments[:map_id]
|
|
88
|
+
index = arguments[:index]
|
|
89
|
+
parent_class = arguments[:parent_reactor_class_name] # Using short name for variable
|
|
63
90
|
|
|
64
91
|
if result.success?
|
|
65
|
-
storage.store_map_result(map_id, index, result.value,
|
|
66
|
-
strict_ordering: strict_ordering)
|
|
92
|
+
storage.store_map_result(map_id, index, ContextSerializer.serialize_value(result.value),
|
|
93
|
+
parent_class, strict_ordering: arguments[:strict_ordering])
|
|
67
94
|
else
|
|
68
|
-
|
|
69
|
-
storage.store_map_result(map_id, index, { _error: result.error },
|
|
70
|
-
strict_ordering: strict_ordering)
|
|
95
|
+
executor.undo_all
|
|
96
|
+
storage.store_map_result(map_id, index, { _error: result.error }, parent_class,
|
|
97
|
+
strict_ordering: arguments[:strict_ordering])
|
|
98
|
+
|
|
99
|
+
if arguments[:fail_fast]
|
|
100
|
+
storage.store_map_failed_context_id(map_id, context.context_id, parent_class)
|
|
101
|
+
# FAST FAIL: Trigger Collector immediately to cancel/fail the map execution
|
|
102
|
+
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
103
|
+
parent_context_id: arguments[:parent_context_id],
|
|
104
|
+
map_id: map_id,
|
|
105
|
+
parent_reactor_class_name: parent_class,
|
|
106
|
+
step_name: arguments[:step_name],
|
|
107
|
+
strict_ordering: arguments[:strict_ordering],
|
|
108
|
+
timeout: 3600
|
|
109
|
+
)
|
|
110
|
+
end
|
|
71
111
|
end
|
|
112
|
+
end
|
|
72
113
|
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
def self.finalize_execution(arguments, storage)
|
|
115
|
+
map_id = arguments[:map_id]
|
|
116
|
+
parent_class = arguments[:parent_reactor_class_name]
|
|
75
117
|
|
|
76
|
-
|
|
118
|
+
new_count = storage.decrement_map_counter(map_id, parent_class)
|
|
119
|
+
trigger_next_batch_if_needed(arguments, arguments[:index], arguments[:batch_size])
|
|
77
120
|
|
|
78
121
|
return unless new_count.zero?
|
|
79
122
|
|
|
80
|
-
# Trigger collection
|
|
81
123
|
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
82
|
-
parent_context_id: parent_context_id,
|
|
124
|
+
parent_context_id: arguments[:parent_context_id],
|
|
83
125
|
map_id: map_id,
|
|
84
|
-
parent_reactor_class_name:
|
|
85
|
-
step_name: step_name,
|
|
86
|
-
strict_ordering: strict_ordering,
|
|
126
|
+
parent_reactor_class_name: parent_class,
|
|
127
|
+
step_name: arguments[:step_name],
|
|
128
|
+
strict_ordering: arguments[:strict_ordering],
|
|
87
129
|
timeout: 3600
|
|
88
130
|
)
|
|
89
131
|
end
|
|
90
132
|
|
|
91
|
-
def self.
|
|
92
|
-
|
|
93
|
-
map_id = arguments[:map_id]
|
|
94
|
-
reactor_class_name = arguments[:parent_reactor_class_name]
|
|
95
|
-
|
|
96
|
-
next_index = storage.increment_last_queued_index(map_id, reactor_class_name)
|
|
97
|
-
total_count = storage.retrieve_map_metadata(map_id, reactor_class_name)["count"]
|
|
98
|
-
|
|
99
|
-
return unless next_index < total_count
|
|
100
|
-
|
|
101
|
-
parent_context = load_parent_context(arguments, reactor_class_name, storage)
|
|
102
|
-
element = resolve_next_element(arguments, parent_context, next_index)
|
|
103
|
-
serialized_inputs = build_serialized_inputs(arguments, parent_context, element)
|
|
133
|
+
def self.trigger_next_batch_if_needed(arguments, index, batch_size)
|
|
134
|
+
return unless batch_size && ((index + 1) % batch_size).zero?
|
|
104
135
|
|
|
105
|
-
|
|
136
|
+
# Trigger Dispatcher for next batch
|
|
137
|
+
next_batch_args = arguments.dup
|
|
138
|
+
# Ensure we don't carry over temporary execution flags if any
|
|
139
|
+
next_batch_args[:continuation] = true
|
|
140
|
+
RubyReactor::Map::Dispatcher.perform(next_batch_args)
|
|
106
141
|
end
|
|
107
142
|
|
|
108
|
-
|
|
109
|
-
parent_context_data = storage.retrieve_context(arguments[:parent_context_id], reactor_class_name)
|
|
110
|
-
parent_reactor_class = Object.const_get(reactor_class_name)
|
|
111
|
-
parent_context = Context.new(
|
|
112
|
-
ContextSerializer.deserialize_value(parent_context_data["inputs"]),
|
|
113
|
-
parent_reactor_class
|
|
114
|
-
)
|
|
115
|
-
parent_context.context_id = arguments[:parent_context_id]
|
|
116
|
-
parent_context
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def self.resolve_next_element(arguments, parent_context, next_index)
|
|
120
|
-
parent_reactor_class = parent_context.reactor_class
|
|
121
|
-
step_config = parent_reactor_class.steps[arguments[:step_name].to_sym]
|
|
122
|
-
|
|
123
|
-
source_template = step_config.arguments[:source][:source]
|
|
124
|
-
source = source_template.resolve(parent_context)
|
|
125
|
-
source[next_index]
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def self.build_serialized_inputs(arguments, parent_context, element)
|
|
129
|
-
parent_reactor_class = parent_context.reactor_class
|
|
130
|
-
step_config = parent_reactor_class.steps[arguments[:step_name].to_sym]
|
|
131
|
-
|
|
132
|
-
mappings_template = step_config.arguments[:argument_mappings][:source]
|
|
133
|
-
mappings = mappings_template.resolve(parent_context) || {}
|
|
134
|
-
|
|
135
|
-
mapped_inputs = build_element_inputs(mappings, parent_context, element)
|
|
136
|
-
ContextSerializer.serialize_value(mapped_inputs)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def self.queue_element_job(arguments, map_id, next_index, serialized_inputs, reactor_class_name)
|
|
140
|
-
RubyReactor.configuration.async_router.perform_map_element_async(
|
|
141
|
-
map_id: map_id,
|
|
142
|
-
element_id: "#{map_id}:#{next_index}",
|
|
143
|
-
index: next_index,
|
|
144
|
-
serialized_inputs: serialized_inputs,
|
|
145
|
-
reactor_class_info: arguments[:reactor_class_info],
|
|
146
|
-
strict_ordering: arguments[:strict_ordering],
|
|
147
|
-
parent_context_id: arguments[:parent_context_id],
|
|
148
|
-
parent_reactor_class_name: reactor_class_name,
|
|
149
|
-
step_name: arguments[:step_name],
|
|
150
|
-
batch_size: arguments[:batch_size]
|
|
151
|
-
)
|
|
152
|
-
end
|
|
153
|
-
private_class_method :queue_next_batch, :load_parent_context,
|
|
154
|
-
:resolve_next_element, :build_serialized_inputs, :queue_element_job
|
|
143
|
+
private_class_method :load_parent_context, :trigger_next_batch_if_needed
|
|
155
144
|
end
|
|
156
145
|
end
|
|
157
146
|
end
|
|
@@ -21,7 +21,8 @@ module RubyReactor
|
|
|
21
21
|
storage_options: {
|
|
22
22
|
map_id: arguments[:map_id], storage: storage,
|
|
23
23
|
parent_reactor_class_name: arguments[:parent_reactor_class_name],
|
|
24
|
-
strict_ordering: arguments[:strict_ordering]
|
|
24
|
+
strict_ordering: arguments[:strict_ordering],
|
|
25
|
+
fail_fast: arguments[:fail_fast]
|
|
25
26
|
}
|
|
26
27
|
)
|
|
27
28
|
|
|
@@ -30,7 +31,14 @@ module RubyReactor
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def self.execute_all_elements(source:, mappings:, reactor_class:, parent_context:, storage_options:)
|
|
34
|
+
# rubocop:disable Metrics/BlockLength
|
|
33
35
|
source.map.with_index do |element, index|
|
|
36
|
+
if storage_options[:fail_fast]
|
|
37
|
+
failed_context_id = storage_options[:storage].retrieve_map_failed_context_id(
|
|
38
|
+
storage_options[:map_id], storage_options[:parent_reactor_class_name]
|
|
39
|
+
)
|
|
40
|
+
next if failed_context_id
|
|
41
|
+
end
|
|
34
42
|
element_inputs = build_element_inputs(mappings, parent_context, element)
|
|
35
43
|
|
|
36
44
|
# Manually create and link context to ensure parent_context_id is set
|
|
@@ -56,21 +64,27 @@ module RubyReactor
|
|
|
56
64
|
|
|
57
65
|
store_result(result, index, storage_options)
|
|
58
66
|
|
|
67
|
+
if result.failure? && storage_options[:fail_fast]
|
|
68
|
+
storage_options[:storage].store_map_failed_context_id(
|
|
69
|
+
storage_options[:map_id], child_context.context_id, storage_options[:parent_reactor_class_name]
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
59
73
|
result
|
|
60
|
-
end
|
|
74
|
+
end.compact
|
|
75
|
+
# rubocop:enable Metrics/BlockLength
|
|
61
76
|
end
|
|
62
77
|
|
|
63
78
|
def self.link_contexts(child_context, parent_context)
|
|
64
79
|
child_context.parent_context = parent_context
|
|
65
80
|
child_context.root_context = parent_context.root_context || parent_context
|
|
66
|
-
child_context.test_mode = parent_context.test_mode
|
|
67
81
|
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
def self.store_result(result, index, options)
|
|
71
85
|
value = result.success? ? result.value : { _error: result.error }
|
|
72
86
|
options[:storage].store_map_result(
|
|
73
|
-
options[:map_id], index, value, options[:parent_reactor_class_name],
|
|
87
|
+
options[:map_id], index, ContextSerializer.serialize_value(value), options[:parent_reactor_class_name],
|
|
74
88
|
strict_ordering: options[:strict_ordering]
|
|
75
89
|
)
|
|
76
90
|
end
|
|
@@ -54,16 +54,17 @@ module RubyReactor
|
|
|
54
54
|
# Resumes parent reactor execution after map completion
|
|
55
55
|
def resume_parent_execution(parent_context, step_name, final_result, storage)
|
|
56
56
|
executor = RubyReactor::Executor.new(parent_context.reactor_class, {}, parent_context)
|
|
57
|
+
step_name_sym = step_name.to_sym
|
|
57
58
|
|
|
58
59
|
if final_result.failure?
|
|
59
|
-
step_name_sym = step_name.to_sym
|
|
60
60
|
parent_context.current_step = step_name_sym
|
|
61
61
|
|
|
62
62
|
error = RubyReactor::Error::StepFailureError.new(
|
|
63
63
|
final_result.error,
|
|
64
64
|
step: step_name_sym,
|
|
65
65
|
context: parent_context,
|
|
66
|
-
original_error: final_result.error.is_a?(Exception) ? final_result.error : nil
|
|
66
|
+
original_error: final_result.error.is_a?(Exception) ? final_result.error : nil,
|
|
67
|
+
exception_class: final_result.respond_to?(:exception_class) ? final_result.exception_class : nil
|
|
67
68
|
)
|
|
68
69
|
|
|
69
70
|
# Pass backtrace if available
|
|
@@ -77,7 +78,7 @@ module RubyReactor
|
|
|
77
78
|
# Manually update context status since we're not running executor loop
|
|
78
79
|
executor.send(:update_context_status, failure_response)
|
|
79
80
|
else
|
|
80
|
-
parent_context.set_result(
|
|
81
|
+
parent_context.set_result(step_name_sym, final_result.value)
|
|
81
82
|
|
|
82
83
|
# Manually update execution trace to reflect completion
|
|
83
84
|
# This is necessary because resume_execution continues from the NEXT step
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Map
|
|
5
|
+
class ResultEnumerator
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
DEFAULT_BATCH_SIZE = 1000
|
|
9
|
+
|
|
10
|
+
attr_reader :map_id, :reactor_class_name, :strict_ordering, :batch_size
|
|
11
|
+
|
|
12
|
+
def initialize(map_id, reactor_class_name, strict_ordering: true, batch_size: DEFAULT_BATCH_SIZE)
|
|
13
|
+
@map_id = map_id
|
|
14
|
+
@reactor_class_name = reactor_class_name
|
|
15
|
+
@strict_ordering = strict_ordering
|
|
16
|
+
@batch_size = batch_size
|
|
17
|
+
@storage = RubyReactor.configuration.storage_adapter
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each
|
|
21
|
+
return enum_for(:each) unless block_given?
|
|
22
|
+
|
|
23
|
+
if @strict_ordering
|
|
24
|
+
count.times do |i|
|
|
25
|
+
yield self[i]
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
offset = 0
|
|
29
|
+
loop do
|
|
30
|
+
results = @storage.retrieve_map_results_batch(
|
|
31
|
+
@map_id,
|
|
32
|
+
@reactor_class_name,
|
|
33
|
+
offset: offset,
|
|
34
|
+
limit: @batch_size,
|
|
35
|
+
strict_ordering: @strict_ordering
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
break if results.empty?
|
|
39
|
+
|
|
40
|
+
results.each { |result| yield wrap_result(result) }
|
|
41
|
+
|
|
42
|
+
offset += results.size
|
|
43
|
+
break if results.size < @batch_size
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def count
|
|
49
|
+
@count ||= @storage.count_map_results(@map_id, @reactor_class_name)
|
|
50
|
+
end
|
|
51
|
+
alias size count
|
|
52
|
+
alias length count
|
|
53
|
+
|
|
54
|
+
def empty?
|
|
55
|
+
count.zero?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def any?
|
|
59
|
+
!empty?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def [](index)
|
|
63
|
+
return nil if index.negative? || index >= count
|
|
64
|
+
|
|
65
|
+
results = @storage.retrieve_map_results_batch(
|
|
66
|
+
@map_id,
|
|
67
|
+
@reactor_class_name,
|
|
68
|
+
offset: index,
|
|
69
|
+
limit: 1,
|
|
70
|
+
strict_ordering: @strict_ordering
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return nil if results.empty?
|
|
74
|
+
|
|
75
|
+
wrap_result(results.first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def first
|
|
79
|
+
self[0]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def last
|
|
83
|
+
self[count - 1]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def successes
|
|
87
|
+
lazy.select { |result| result.is_a?(RubyReactor::Success) }.map(&:value)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def failures
|
|
91
|
+
lazy.select { |result| result.is_a?(RubyReactor::Failure) }.map(&:error)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def wrap_result(result)
|
|
97
|
+
if result.is_a?(Hash) && result.key?("_error")
|
|
98
|
+
RubyReactor::Failure.new(result["_error"])
|
|
99
|
+
else
|
|
100
|
+
RubyReactor::Success.new(ContextSerializer.deserialize_value(result))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|