ruby_reactor 0.7.1 → 0.8.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/.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 +150 -3
- data/CLAUDE.md +1 -1
- data/README.md +171 -63
- data/lib/ruby_reactor/adapters/active_job/router.rb +2 -2
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +2 -2
- data/lib/ruby_reactor/context.rb +2 -2
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/map_builder.rb +31 -5
- 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/map/helpers.rb +9 -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 +182 -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
|
|
15
|
-
run_async(
|
|
12
|
+
if fan_out?
|
|
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,208 @@ 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
|
+
# Fans out anywhere except inside a map element: an element's result and
|
|
73
|
+
# the map's completion counter are tracked by its own ElementExecutor job,
|
|
74
|
+
# so a nested hand-off there would escape that tracking. A reactor worker
|
|
75
|
+
# (`background`, collector resume, `async_reactor` child) fans out as the
|
|
76
|
+
# caller would — the collector resumes the reactor in a worker either way.
|
|
77
|
+
def fan_out?
|
|
78
|
+
return false if context.map_metadata || context.root_context&.map_metadata
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fail_fast = arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
80
|
+
inputs[:fan_out]
|
|
81
|
+
end
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
def run_inline
|
|
84
|
+
results = execute_inline_map
|
|
85
|
+
return results if results.is_a?(RubyReactor::Failure) || results.is_a?(RubyReactor::Halt)
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return result if result.is_a?(RubyReactor::Halt)
|
|
87
|
+
process_results(results, inputs[:collect_block], inputs[:fail_fast])
|
|
88
|
+
end
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
def execute_inline_map
|
|
91
|
+
results = []
|
|
92
|
+
fail_fast = inputs[:fail_fast].nil? || inputs[:fail_fast]
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
end
|
|
94
|
+
inputs[:source].each do |element|
|
|
95
|
+
result = execute_single_element(element)
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
# An element-level Halt propagates as a run halt: stop immediately
|
|
98
|
+
# rather than being collected as a (nil) value.
|
|
99
|
+
return result if result.is_a?(RubyReactor::Halt)
|
|
93
100
|
|
|
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
|
|
101
|
+
if fail_fast && result.failure?
|
|
102
|
+
return result # Stop immediately on first failure
|
|
103
|
+
end
|
|
123
104
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
child_context.root_context = parent_context.root_context || parent_context
|
|
127
|
-
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
105
|
+
# When fail_fast is false, store Result objects; when true, store values
|
|
106
|
+
results << (fail_fast ? result.value : result)
|
|
128
107
|
end
|
|
129
108
|
|
|
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
|
|
109
|
+
results
|
|
110
|
+
end
|
|
139
111
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
112
|
+
def execute_single_element(element)
|
|
113
|
+
mapped_inputs = self.class.build_mapped_inputs(inputs[:argument_mappings] || {}, context, element)
|
|
114
|
+
child_context = RubyReactor::Context.new(mapped_inputs, inputs[:mapped_reactor_class])
|
|
115
|
+
|
|
116
|
+
link_contexts(child_context, context)
|
|
117
|
+
|
|
118
|
+
map_id = "#{context.context_id}:#{context.current_step}"
|
|
119
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
120
|
+
storage.store_map_element_context_id(map_id, child_context.context_id, context.reactor_class.name)
|
|
121
|
+
|
|
122
|
+
# Set map metadata for failure handling
|
|
123
|
+
child_context.map_metadata = {
|
|
124
|
+
map_id: map_id,
|
|
125
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
126
|
+
index: nil # Inline map execution doesn't track index in metadata currently, but could
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Store reference in composed_contexts so the UI knows where to find elements
|
|
130
|
+
context.composed_contexts[context.current_step] = {
|
|
131
|
+
name: context.current_step,
|
|
132
|
+
type: :map_ref,
|
|
133
|
+
map_id: map_id,
|
|
134
|
+
element_reactor_class: inputs[:mapped_reactor_class].name
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
executor = RubyReactor::Executor.new(inputs[:mapped_reactor_class], {}, child_context)
|
|
138
|
+
executor.execute
|
|
139
|
+
executor.result
|
|
140
|
+
end
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
def link_contexts(child_context, parent_context)
|
|
143
|
+
child_context.parent_context = parent_context
|
|
144
|
+
child_context.root_context = parent_context.root_context || parent_context
|
|
145
|
+
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def process_results(results, collect_block, _fail_fast = true)
|
|
149
|
+
if collect_block
|
|
150
|
+
begin
|
|
151
|
+
# Collect block receives Result objects when fail_fast is false, values when true
|
|
152
|
+
return RubyReactor::Success(collect_block.call(results))
|
|
153
|
+
rescue StandardError => e
|
|
154
|
+
return RubyReactor::Failure(e)
|
|
153
155
|
end
|
|
154
156
|
end
|
|
155
157
|
|
|
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)
|
|
158
|
+
# Simplified: both branches returned Success(results)
|
|
159
|
+
RubyReactor::Success(results)
|
|
160
|
+
end
|
|
162
161
|
|
|
163
|
-
|
|
162
|
+
def run_async(step_name)
|
|
163
|
+
map_id = "#{context.context_id}:#{step_name}"
|
|
164
|
+
context.map_operations[step_name.to_s] = map_id
|
|
165
|
+
prepare_async_execution(map_id, inputs[:source].size)
|
|
164
166
|
|
|
165
|
-
|
|
167
|
+
reactor_class_info = build_reactor_class_info(inputs[:mapped_reactor_class], step_name)
|
|
166
168
|
|
|
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
|
-
}
|
|
169
|
+
initialize_map_metadata(map_id, reactor_class_info)
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
job_id: job_id,
|
|
177
|
-
intermediate_results: context.intermediate_results,
|
|
178
|
-
execution_id: context.context_id
|
|
179
|
-
)
|
|
180
|
-
end
|
|
171
|
+
job_id = dispatch_async_map(map_id, reactor_class_info, step_name)
|
|
181
172
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
end
|
|
173
|
+
# Store reference in composed_contexts so the UI knows where to find elements
|
|
174
|
+
context.composed_contexts[step_name.to_s] = {
|
|
175
|
+
name: step_name.to_s,
|
|
176
|
+
type: :map_ref,
|
|
177
|
+
map_id: map_id,
|
|
178
|
+
element_reactor_class: inputs[:mapped_reactor_class].name
|
|
179
|
+
}
|
|
190
180
|
|
|
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
|
|
181
|
+
RubyReactor::DispatchResult.new(
|
|
182
|
+
job_id: job_id,
|
|
183
|
+
intermediate_results: context.intermediate_results,
|
|
184
|
+
execution_id: context.context_id
|
|
185
|
+
)
|
|
186
|
+
end
|
|
204
187
|
|
|
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
|
|
188
|
+
def initialize_map_metadata(map_id, reactor_class_info)
|
|
189
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
190
|
+
storage.initialize_map_operation(
|
|
191
|
+
map_id, inputs[:source].size, context.reactor_class.name,
|
|
192
|
+
strict_ordering: inputs[:strict_ordering], reactor_class_info: reactor_class_info,
|
|
193
|
+
**map_recovery_metadata(inputs[:step_name] || context.current_step)
|
|
194
|
+
)
|
|
195
|
+
end
|
|
229
196
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
197
|
+
# Recovery metadata for the map sweeper. When this map runs inside a map
|
|
198
|
+
# element (context.map_metadata present), it is a NESTED map: its parent
|
|
199
|
+
# holds the element's `map_element:` lock, not an `async:` lock (N1).
|
|
200
|
+
def map_recovery_metadata(step_name)
|
|
201
|
+
outer = context.map_metadata
|
|
202
|
+
{
|
|
203
|
+
parent_context_id: context.context_id,
|
|
204
|
+
step_name: step_name.to_s,
|
|
205
|
+
parent_is_map_element: !outer.nil?,
|
|
206
|
+
outer_map_id: outer && (outer[:map_id] || outer["map_id"]),
|
|
207
|
+
outer_index: outer && (outer[:index] || outer["index"])
|
|
208
|
+
}
|
|
209
|
+
end
|
|
238
210
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
211
|
+
def dispatch_async_map(map_id, _reactor_class_info, step_name)
|
|
212
|
+
# Every async map runs through the per-element Dispatcher path. When no
|
|
213
|
+
# batch_size is given we default to the full source size (one fan-out
|
|
214
|
+
# batch), so there is a single execution path: each element runs in its
|
|
215
|
+
# own worker, with the map counter/collector tracking completion. This
|
|
216
|
+
# lets elements with async steps or async retries hand off correctly
|
|
217
|
+
# instead of being forced to run synchronously in a single worker.
|
|
218
|
+
batch_size = inputs[:batch_size] || inputs[:source].size
|
|
219
|
+
|
|
220
|
+
RubyReactor::Map::Dispatcher.perform(
|
|
221
|
+
map_id: map_id,
|
|
222
|
+
parent_context_id: context.context_id,
|
|
223
|
+
parent_reactor_class_name: context.reactor_class.name,
|
|
224
|
+
source: inputs[:source],
|
|
225
|
+
batch_size: batch_size,
|
|
226
|
+
step_name: step_name,
|
|
227
|
+
argument_mappings: inputs[:argument_mappings],
|
|
228
|
+
strict_ordering: inputs[:strict_ordering],
|
|
229
|
+
mapped_reactor_class: inputs[:mapped_reactor_class],
|
|
230
|
+
fail_fast: inputs[:fail_fast].nil? || inputs[:fail_fast]
|
|
231
|
+
)
|
|
232
|
+
queue_collector(map_id, step_name, inputs[:strict_ordering])
|
|
233
|
+
"map:#{map_id}"
|
|
234
|
+
end
|
|
246
235
|
|
|
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
|
|
236
|
+
def prepare_async_execution(map_id, count)
|
|
237
|
+
storage = RubyReactor.configuration.storage_adapter
|
|
238
|
+
middlewares = context.middlewares || Executor.middlewares_for(context.reactor_class)
|
|
239
|
+
middlewares.on(:before_async_enqueue, context)
|
|
240
|
+
serialized_context = ContextSerializer.serialize(context)
|
|
241
|
+
storage.store_context(context.context_id, serialized_context, context.reactor_class.name)
|
|
242
|
+
storage.set_map_counter(map_id, count, context.reactor_class.name)
|
|
243
|
+
end
|
|
268
244
|
|
|
269
|
-
|
|
270
|
-
|
|
245
|
+
def build_reactor_class_info(mapped_reactor_class, step_name)
|
|
246
|
+
if mapped_reactor_class.respond_to?(:name)
|
|
247
|
+
{ "type" => "class", "name" => mapped_reactor_class.name }
|
|
248
|
+
else
|
|
249
|
+
{ "type" => "inline", "parent" => context.reactor_class.name, "step" => step_name.to_s }
|
|
271
250
|
end
|
|
251
|
+
end
|
|
272
252
|
|
|
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
|
|
253
|
+
def queue_collector(map_id, step_name, strict_ordering)
|
|
254
|
+
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
255
|
+
parent_context_id: context.context_id, map_id: map_id,
|
|
256
|
+
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
257
|
+
strict_ordering: strict_ordering, timeout: 3600
|
|
258
|
+
)
|
|
295
259
|
end
|
|
296
260
|
end
|
|
297
261
|
end
|