ruby_reactor 0.7.1 → 0.8.0
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/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +26 -16
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +1 -1
- data/CHANGELOG.md +140 -0
- data/CLAUDE.md +1 -1
- data/README.md +125 -31
- data/lib/ruby_reactor/context.rb +2 -2
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/reactor.rb +36 -18
- data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
- data/lib/ruby_reactor/dsl/template_helpers.rb +2 -2
- data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
- data/lib/ruby_reactor/executor/result_handler.rb +9 -3
- data/lib/ruby_reactor/executor/retry_manager.rb +2 -1
- data/lib/ruby_reactor/executor/step_executor.rb +9 -2
- data/lib/ruby_reactor/executor.rb +3 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +3 -6
- data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
- data/lib/ruby_reactor/step/compose_step.rb +56 -75
- data/lib/ruby_reactor/step/input_contract.rb +128 -0
- data/lib/ruby_reactor/step/map_step.rb +177 -218
- data/lib/ruby_reactor/step.rb +116 -21
- data/lib/ruby_reactor/step_signals.rb +6 -2
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/template/result.rb +9 -2
- data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +5 -2
- data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
- data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
- data/specs/002-step-input-contracts/data-model.md +115 -0
- data/specs/002-step-input-contracts/plan.md +165 -0
- data/specs/002-step-input-contracts/quickstart.md +170 -0
- data/specs/002-step-input-contracts/research.md +233 -0
- data/specs/002-step-input-contracts/spec.md +359 -0
- data/specs/002-step-input-contracts/tasks.md +367 -0
- data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
- data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
- data/specs/004-inheritable-step-class/data-model.md +116 -0
- data/specs/004-inheritable-step-class/plan.md +174 -0
- data/specs/004-inheritable-step-class/quickstart.md +112 -0
- data/specs/004-inheritable-step-class/research.md +308 -0
- data/specs/004-inheritable-step-class/spec.md +316 -0
- data/specs/004-inheritable-step-class/tasks.md +258 -0
- data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
- data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
- data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
- data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
- data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
- data/specs/deferred-003-step-lock-declarations/research.md +196 -0
- data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
- data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
- data/specs/possible_feature.md +22 -0
- metadata +28 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
class Step
|
|
5
|
+
# The inputs one unit of work accepts: declared with `input` on a step
|
|
6
|
+
# class, or inside an inline step's `inputs do ... end` block. Both forms
|
|
7
|
+
# are enforced by the same `#enforce!`, so they cannot drift apart.
|
|
8
|
+
class InputContract
|
|
9
|
+
include RubyReactor::Dsl::ValidationHelpers
|
|
10
|
+
|
|
11
|
+
Declaration = Struct.new(:name, :type, :optional, :default, :redact, :predicates, :macro_block, :schema,
|
|
12
|
+
:validator, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
REDACTED = "[REDACTED]"
|
|
15
|
+
|
|
16
|
+
attr_reader :owner, :declarations, :cross_field_validators
|
|
17
|
+
|
|
18
|
+
def initialize(owner: nil, declarations: {}, cross_field_validators: [])
|
|
19
|
+
@owner = owner
|
|
20
|
+
@declarations = declarations
|
|
21
|
+
@cross_field_validators = cross_field_validators
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Metrics/ParameterLists
|
|
25
|
+
def input(name, type = nil, optional: false, default: nil, redact: false, validate: nil, **predicates, &block)
|
|
26
|
+
# rubocop:enable Metrics/ParameterLists
|
|
27
|
+
check_dry_validation_available!
|
|
28
|
+
unless default.nil? || optional
|
|
29
|
+
raise Error::ValidationError,
|
|
30
|
+
"input :#{name} declares `default:` but is required; a default only applies to an " \
|
|
31
|
+
"optional input, so add `optional: true`."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@declarations[name] = Declaration.new(
|
|
35
|
+
name: name, type: type, optional: optional, default: default, redact: redact,
|
|
36
|
+
predicates: predicates, macro_block: block, schema: validate,
|
|
37
|
+
validator: build_declaration_validator(name, type, optional, validate, predicates, &block)
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Cross-field rules over the whole argument hash, applied after the
|
|
42
|
+
# per-input rules. A block, a dry-schema, or a dry-validation contract.
|
|
43
|
+
def validate_inputs(schema = nil, &block)
|
|
44
|
+
@cross_field_validators << create_input_validator(block || Validation::SchemaBuilder.schema_for(schema))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def declares?(name)
|
|
48
|
+
@declarations.key?(name.to_sym)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def required_names
|
|
52
|
+
@declarations.values.reject(&:optional).map(&:name)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def optional_names
|
|
56
|
+
@declarations.values.select(&:optional).map(&:name)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def defaults
|
|
60
|
+
@declarations.values.reject { |d| d.default.nil? }.to_h { |d| [d.name, d.default] }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def redacted_names
|
|
64
|
+
@declarations.values.select(&:redact).map(&:name)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def empty?
|
|
68
|
+
@declarations.empty? && @cross_field_validators.empty?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A child's contract: the parent's declarations plus the child's, where a
|
|
72
|
+
# same-named child input replaces the parent's. Neither side is mutated.
|
|
73
|
+
def merge(child)
|
|
74
|
+
self.class.new(
|
|
75
|
+
owner: child.owner,
|
|
76
|
+
declarations: @declarations.merge(child.declarations),
|
|
77
|
+
cross_field_validators: @cross_field_validators + child.cross_field_validators
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def redact(args)
|
|
82
|
+
names = redacted_names
|
|
83
|
+
return args if names.empty?
|
|
84
|
+
|
|
85
|
+
args.to_h { |key, value| [key, names.include?(key.to_sym) ? REDACTED : value] }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns `args` with defaults applied — the resolved values, never the
|
|
89
|
+
# schema's coerced output — or raises InputValidationError. `step_name`
|
|
90
|
+
# is left for the caller, which knows it.
|
|
91
|
+
def enforce!(args)
|
|
92
|
+
args = apply_defaults(args)
|
|
93
|
+
errors = {}
|
|
94
|
+
|
|
95
|
+
@declarations.each_value do |declaration|
|
|
96
|
+
next unless declaration.validator
|
|
97
|
+
next if declaration.optional && !args.key?(declaration.name)
|
|
98
|
+
|
|
99
|
+
# `slice`, so an absent key reads "is missing" and nil "must be filled".
|
|
100
|
+
collect_errors(errors, declaration.validator.call(args.slice(declaration.name)))
|
|
101
|
+
end
|
|
102
|
+
@cross_field_validators.each { |validator| collect_errors(errors, validator.call(args)) }
|
|
103
|
+
|
|
104
|
+
return args if errors.empty?
|
|
105
|
+
|
|
106
|
+
error = Error::InputValidationError.new(errors)
|
|
107
|
+
error.step_arguments = redact(args)
|
|
108
|
+
raise error
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Applied when the key is absent or the value is nil — never for `false`.
|
|
112
|
+
# Public so a step's undo/compensate resolve the same `inputs` as `run`
|
|
113
|
+
# without validating them.
|
|
114
|
+
def apply_defaults(args)
|
|
115
|
+
defaults = self.defaults
|
|
116
|
+
return args if defaults.empty?
|
|
117
|
+
|
|
118
|
+
args.merge(defaults) { |_name, supplied, default| supplied.nil? ? default : supplied }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def collect_errors(errors, result)
|
|
124
|
+
errors.merge!(result.error.field_errors) if result.failure?
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyReactor
|
|
4
|
-
|
|
5
|
-
class MapStep
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def self.run(arguments, context)
|
|
9
|
-
return RubyReactor::Failure("Map source cannot be nil") if arguments[:source].nil?
|
|
4
|
+
class Step
|
|
5
|
+
class MapStep < RubyReactor::Step
|
|
6
|
+
def run
|
|
7
|
+
return RubyReactor::Failure("Map source cannot be nil") if inputs[:source].nil?
|
|
10
8
|
|
|
11
9
|
# Initialize map state in context if not present
|
|
12
10
|
context.map_operations ||= {}
|
|
13
11
|
|
|
14
|
-
if should_run_async?
|
|
15
|
-
run_async(
|
|
12
|
+
if should_run_async?
|
|
13
|
+
run_async(context.current_step)
|
|
16
14
|
else
|
|
17
|
-
run_inline
|
|
15
|
+
run_inline
|
|
18
16
|
end
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
def
|
|
19
|
+
def compensate
|
|
22
20
|
# TODO: Implement compensation for map steps
|
|
23
21
|
RubyReactor.Success()
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
class << self
|
|
27
25
|
def build_mapped_inputs(mappings, context, element)
|
|
28
|
-
|
|
26
|
+
built = {}
|
|
29
27
|
|
|
30
28
|
mappings.each do |mapped_input_name, source|
|
|
31
29
|
# Handle serialized template objects (Hashes from Sidekiq)
|
|
@@ -39,10 +37,10 @@ module RubyReactor
|
|
|
39
37
|
else
|
|
40
38
|
source.resolve(context)
|
|
41
39
|
end
|
|
42
|
-
|
|
40
|
+
built[mapped_input_name] = value
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
built
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
def resolve_element(template_element, current_element)
|
|
@@ -56,242 +54,203 @@ module RubyReactor
|
|
|
56
54
|
|
|
57
55
|
private
|
|
58
56
|
|
|
59
|
-
def
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
def extract_path(value, path)
|
|
58
|
+
if path.is_a?(Symbol) && value.respond_to?(:[])
|
|
59
|
+
value[path]
|
|
60
|
+
elsif path.is_a?(String)
|
|
61
|
+
path.split(".").reduce(value) { |v, key| v&.send(:[], key) }
|
|
62
|
+
elsif path.is_a?(Array)
|
|
63
|
+
path.reduce(value) { |v, key| v&.send(:[], key) }
|
|
64
|
+
elsif value.respond_to?(path)
|
|
65
|
+
value.send(path)
|
|
66
|
+
end
|
|
63
67
|
end
|
|
68
|
+
end
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
results = execute_inline_map(arguments, context)
|
|
67
|
-
return results if results.is_a?(RubyReactor::Failure) || results.is_a?(RubyReactor::Halt)
|
|
70
|
+
private
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
def should_run_async?
|
|
73
|
+
return false if context.inline_async_execution
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fail_fast = arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
75
|
+
inputs[:async]
|
|
76
|
+
end
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
def run_inline
|
|
79
|
+
results = execute_inline_map
|
|
80
|
+
return results if results.is_a?(RubyReactor::Failure) || results.is_a?(RubyReactor::Halt)
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return result if result.is_a?(RubyReactor::Halt)
|
|
82
|
+
process_results(results, inputs[:collect_block], inputs[:fail_fast])
|
|
83
|
+
end
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
def execute_inline_map
|
|
86
|
+
results = []
|
|
87
|
+
fail_fast = inputs[:fail_fast].nil? || inputs[:fail_fast]
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
end
|
|
89
|
+
inputs[:source].each do |element|
|
|
90
|
+
result = execute_single_element(element)
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
# An element-level Halt propagates as a run halt: stop immediately
|
|
93
|
+
# rather than being collected as a (nil) value.
|
|
94
|
+
return result if result.is_a?(RubyReactor::Halt)
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
link_contexts(child_context, context)
|
|
99
|
-
|
|
100
|
-
map_id = "#{context.context_id}:#{context.current_step}"
|
|
101
|
-
storage = RubyReactor.configuration.storage_adapter
|
|
102
|
-
storage.store_map_element_context_id(map_id, child_context.context_id, context.reactor_class.name)
|
|
103
|
-
|
|
104
|
-
# Set map metadata for failure handling
|
|
105
|
-
child_context.map_metadata = {
|
|
106
|
-
map_id: map_id,
|
|
107
|
-
parent_reactor_class_name: context.reactor_class.name,
|
|
108
|
-
index: nil # Inline map execution doesn't track index in metadata currently, but could
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
# Store reference in composed_contexts so the UI knows where to find elements
|
|
112
|
-
context.composed_contexts[context.current_step] = {
|
|
113
|
-
name: context.current_step,
|
|
114
|
-
type: :map_ref,
|
|
115
|
-
map_id: map_id,
|
|
116
|
-
element_reactor_class: arguments[:mapped_reactor_class].name
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
executor = RubyReactor::Executor.new(arguments[:mapped_reactor_class], {}, child_context)
|
|
120
|
-
executor.execute
|
|
121
|
-
executor.result
|
|
122
|
-
end
|
|
96
|
+
if fail_fast && result.failure?
|
|
97
|
+
return result # Stop immediately on first failure
|
|
98
|
+
end
|
|
123
99
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
child_context.root_context = parent_context.root_context || parent_context
|
|
127
|
-
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
100
|
+
# When fail_fast is false, store Result objects; when true, store values
|
|
101
|
+
results << (fail_fast ? result.value : result)
|
|
128
102
|
end
|
|
129
103
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
begin
|
|
133
|
-
# Collect block receives Result objects when fail_fast is false, values when true
|
|
134
|
-
return RubyReactor::Success(collect_block.call(results))
|
|
135
|
-
rescue StandardError => e
|
|
136
|
-
return RubyReactor::Failure(e)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
104
|
+
results
|
|
105
|
+
end
|
|
139
106
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
107
|
+
def execute_single_element(element)
|
|
108
|
+
mapped_inputs = self.class.build_mapped_inputs(inputs[:argument_mappings] || {}, context, element)
|
|
109
|
+
child_context = RubyReactor::Context.new(mapped_inputs, inputs[:mapped_reactor_class])
|
|
110
|
+
|
|
111
|
+
link_contexts(child_context, context)
|
|
112
|
+
|
|
113
|
+
map_id = "#{context.context_id}:#{context.current_step}"
|
|
114
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
115
|
+
storage.store_map_element_context_id(map_id, child_context.context_id, context.reactor_class.name)
|
|
116
|
+
|
|
117
|
+
# Set map metadata for failure handling
|
|
118
|
+
child_context.map_metadata = {
|
|
119
|
+
map_id: map_id,
|
|
120
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
121
|
+
index: nil # Inline map execution doesn't track index in metadata currently, but could
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Store reference in composed_contexts so the UI knows where to find elements
|
|
125
|
+
context.composed_contexts[context.current_step] = {
|
|
126
|
+
name: context.current_step,
|
|
127
|
+
type: :map_ref,
|
|
128
|
+
map_id: map_id,
|
|
129
|
+
element_reactor_class: inputs[:mapped_reactor_class].name
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
executor = RubyReactor::Executor.new(inputs[:mapped_reactor_class], {}, child_context)
|
|
133
|
+
executor.execute
|
|
134
|
+
executor.result
|
|
135
|
+
end
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
def link_contexts(child_context, parent_context)
|
|
138
|
+
child_context.parent_context = parent_context
|
|
139
|
+
child_context.root_context = parent_context.root_context || parent_context
|
|
140
|
+
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def process_results(results, collect_block, _fail_fast = true)
|
|
144
|
+
if collect_block
|
|
145
|
+
begin
|
|
146
|
+
# Collect block receives Result objects when fail_fast is false, values when true
|
|
147
|
+
return RubyReactor::Success(collect_block.call(results))
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
return RubyReactor::Failure(e)
|
|
153
150
|
end
|
|
154
151
|
end
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
prepare_async_execution(context, map_id, arguments[:source].size)
|
|
160
|
-
|
|
161
|
-
reactor_class_info = build_reactor_class_info(arguments[:mapped_reactor_class], context, step_name)
|
|
153
|
+
# Simplified: both branches returned Success(results)
|
|
154
|
+
RubyReactor::Success(results)
|
|
155
|
+
end
|
|
162
156
|
|
|
163
|
-
|
|
157
|
+
def run_async(step_name)
|
|
158
|
+
map_id = "#{context.context_id}:#{step_name}"
|
|
159
|
+
context.map_operations[step_name.to_s] = map_id
|
|
160
|
+
prepare_async_execution(map_id, inputs[:source].size)
|
|
164
161
|
|
|
165
|
-
|
|
162
|
+
reactor_class_info = build_reactor_class_info(inputs[:mapped_reactor_class], step_name)
|
|
166
163
|
|
|
167
|
-
|
|
168
|
-
context.composed_contexts[step_name.to_s] = {
|
|
169
|
-
name: step_name.to_s,
|
|
170
|
-
type: :map_ref,
|
|
171
|
-
map_id: map_id,
|
|
172
|
-
element_reactor_class: arguments[:mapped_reactor_class].name
|
|
173
|
-
}
|
|
164
|
+
initialize_map_metadata(map_id, reactor_class_info)
|
|
174
165
|
|
|
175
|
-
|
|
176
|
-
job_id: job_id,
|
|
177
|
-
intermediate_results: context.intermediate_results,
|
|
178
|
-
execution_id: context.context_id
|
|
179
|
-
)
|
|
180
|
-
end
|
|
166
|
+
job_id = dispatch_async_map(map_id, reactor_class_info, step_name)
|
|
181
167
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
end
|
|
168
|
+
# Store reference in composed_contexts so the UI knows where to find elements
|
|
169
|
+
context.composed_contexts[step_name.to_s] = {
|
|
170
|
+
name: step_name.to_s,
|
|
171
|
+
type: :map_ref,
|
|
172
|
+
map_id: map_id,
|
|
173
|
+
element_reactor_class: inputs[:mapped_reactor_class].name
|
|
174
|
+
}
|
|
190
175
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
parent_context_id: context.context_id,
|
|
198
|
-
step_name: step_name.to_s,
|
|
199
|
-
parent_is_map_element: !outer.nil?,
|
|
200
|
-
outer_map_id: outer && (outer[:map_id] || outer["map_id"]),
|
|
201
|
-
outer_index: outer && (outer[:index] || outer["index"])
|
|
202
|
-
}
|
|
203
|
-
end
|
|
176
|
+
RubyReactor::DispatchResult.new(
|
|
177
|
+
job_id: job_id,
|
|
178
|
+
intermediate_results: context.intermediate_results,
|
|
179
|
+
execution_id: context.context_id
|
|
180
|
+
)
|
|
181
|
+
end
|
|
204
182
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
RubyReactor::Map::Dispatcher.perform(
|
|
215
|
-
map_id: map_id,
|
|
216
|
-
parent_context_id: context.context_id,
|
|
217
|
-
parent_reactor_class_name: context.reactor_class.name,
|
|
218
|
-
source: arguments[:source],
|
|
219
|
-
batch_size: batch_size,
|
|
220
|
-
step_name: step_name,
|
|
221
|
-
argument_mappings: arguments[:argument_mappings],
|
|
222
|
-
strict_ordering: arguments[:strict_ordering],
|
|
223
|
-
mapped_reactor_class: arguments[:mapped_reactor_class],
|
|
224
|
-
fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
225
|
-
)
|
|
226
|
-
queue_collector(map_id, context, step_name, arguments[:strict_ordering])
|
|
227
|
-
"map:#{map_id}"
|
|
228
|
-
end
|
|
183
|
+
def initialize_map_metadata(map_id, reactor_class_info)
|
|
184
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
185
|
+
storage.initialize_map_operation(
|
|
186
|
+
map_id, inputs[:source].size, context.reactor_class.name,
|
|
187
|
+
strict_ordering: inputs[:strict_ordering], reactor_class_info: reactor_class_info,
|
|
188
|
+
**map_recovery_metadata(inputs[:step_name] || context.current_step)
|
|
189
|
+
)
|
|
190
|
+
end
|
|
229
191
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
192
|
+
# Recovery metadata for the map sweeper. When this map runs inside a map
|
|
193
|
+
# element (context.map_metadata present), it is a NESTED map: its parent
|
|
194
|
+
# holds the element's `map_element:` lock, not an `async:` lock (N1).
|
|
195
|
+
def map_recovery_metadata(step_name)
|
|
196
|
+
outer = context.map_metadata
|
|
197
|
+
{
|
|
198
|
+
parent_context_id: context.context_id,
|
|
199
|
+
step_name: step_name.to_s,
|
|
200
|
+
parent_is_map_element: !outer.nil?,
|
|
201
|
+
outer_map_id: outer && (outer[:map_id] || outer["map_id"]),
|
|
202
|
+
outer_index: outer && (outer[:index] || outer["index"])
|
|
203
|
+
}
|
|
204
|
+
end
|
|
238
205
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
206
|
+
def dispatch_async_map(map_id, _reactor_class_info, step_name)
|
|
207
|
+
# Every async map runs through the per-element Dispatcher path. When no
|
|
208
|
+
# batch_size is given we default to the full source size (one fan-out
|
|
209
|
+
# batch), so there is a single execution path: each element runs in its
|
|
210
|
+
# own worker, with the map counter/collector tracking completion. This
|
|
211
|
+
# lets elements with async steps or async retries hand off correctly
|
|
212
|
+
# instead of being forced to run synchronously in a single worker.
|
|
213
|
+
batch_size = inputs[:batch_size] || inputs[:source].size
|
|
214
|
+
|
|
215
|
+
RubyReactor::Map::Dispatcher.perform(
|
|
216
|
+
map_id: map_id,
|
|
217
|
+
parent_context_id: context.context_id,
|
|
218
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
219
|
+
source: inputs[:source],
|
|
220
|
+
batch_size: batch_size,
|
|
221
|
+
step_name: step_name,
|
|
222
|
+
argument_mappings: inputs[:argument_mappings],
|
|
223
|
+
strict_ordering: inputs[:strict_ordering],
|
|
224
|
+
mapped_reactor_class: inputs[:mapped_reactor_class],
|
|
225
|
+
fail_fast: inputs[:fail_fast].nil? || inputs[:fail_fast]
|
|
226
|
+
)
|
|
227
|
+
queue_collector(map_id, step_name, inputs[:strict_ordering])
|
|
228
|
+
"map:#{map_id}"
|
|
229
|
+
end
|
|
246
230
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
limit ||= arguments[:source].size
|
|
258
|
-
first_job_id = nil
|
|
259
|
-
arguments[:source].each_with_index do |element, index|
|
|
260
|
-
break if index >= limit
|
|
261
|
-
|
|
262
|
-
job_id = queue_map_element(
|
|
263
|
-
map_id: map_id, element: element, index: index, arguments: arguments,
|
|
264
|
-
context: context, reactor_class_info: reactor_class_info, step_name: step_name
|
|
265
|
-
)
|
|
266
|
-
first_job_id ||= job_id
|
|
267
|
-
end
|
|
231
|
+
def prepare_async_execution(map_id, count)
|
|
232
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
233
|
+
middlewares = context.middlewares || Executor.middlewares_for(context.reactor_class)
|
|
234
|
+
middlewares.on(:before_async_enqueue, context)
|
|
235
|
+
serialized_context = ContextSerializer.serialize(context)
|
|
236
|
+
storage.store_context(context.context_id, serialized_context, context.reactor_class.name)
|
|
237
|
+
storage.set_map_counter(map_id, count, context.reactor_class.name)
|
|
238
|
+
end
|
|
268
239
|
|
|
269
|
-
|
|
270
|
-
|
|
240
|
+
def build_reactor_class_info(mapped_reactor_class, step_name)
|
|
241
|
+
if mapped_reactor_class.respond_to?(:name)
|
|
242
|
+
{ "type" => "class", "name" => mapped_reactor_class.name }
|
|
243
|
+
else
|
|
244
|
+
{ "type" => "inline", "parent" => context.reactor_class.name, "step" => step_name.to_s }
|
|
271
245
|
end
|
|
246
|
+
end
|
|
272
247
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
map_id: map_id, element_id: "#{map_id}:#{index}", index: index,
|
|
280
|
-
serialized_inputs: serialized_inputs, reactor_class_info: reactor_class_info,
|
|
281
|
-
strict_ordering: arguments[:strict_ordering], parent_context_id: context.context_id,
|
|
282
|
-
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
283
|
-
batch_size: arguments[:batch_size]
|
|
284
|
-
)
|
|
285
|
-
end
|
|
286
|
-
# rubocop:enable Metrics/ParameterLists
|
|
287
|
-
|
|
288
|
-
def queue_collector(map_id, context, step_name, strict_ordering)
|
|
289
|
-
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
290
|
-
parent_context_id: context.context_id, map_id: map_id,
|
|
291
|
-
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
292
|
-
strict_ordering: strict_ordering, timeout: 3600
|
|
293
|
-
)
|
|
294
|
-
end
|
|
248
|
+
def queue_collector(map_id, step_name, strict_ordering)
|
|
249
|
+
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
250
|
+
parent_context_id: context.context_id, map_id: map_id,
|
|
251
|
+
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
252
|
+
strict_ordering: strict_ordering, timeout: 3600
|
|
253
|
+
)
|
|
295
254
|
end
|
|
296
255
|
end
|
|
297
256
|
end
|