ruby_reactor 0.7.0 → 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/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
- data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +92 -15
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +8 -1
- data/CHANGELOG.md +151 -0
- data/CLAUDE.md +2 -2
- data/README.md +149 -40
- data/lib/ruby_reactor/context.rb +9 -2
- data/lib/ruby_reactor/context_serializer.rb +13 -0
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/lockable.rb +2 -2
- 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 +13 -5
- 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/compensation_manager.rb +58 -45
- data/lib/ruby_reactor/executor/ordered_lock_support.rb +9 -9
- data/lib/ruby_reactor/executor/result_handler.rb +44 -13
- data/lib/ruby_reactor/executor/retry_manager.rb +6 -2
- data/lib/ruby_reactor/executor/step_executor.rb +27 -14
- data/lib/ruby_reactor/executor.rb +20 -15
- data/lib/ruby_reactor/map/element_executor.rb +7 -1
- data/lib/ruby_reactor/map/helpers.rb +9 -7
- data/lib/ruby_reactor/map/result_enumerator.rb +2 -0
- data/lib/ruby_reactor/map/sweeper.rb +1 -1
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/open_telemetry.rb +7 -4
- data/lib/ruby_reactor/ordered_lock.rb +3 -3
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +64 -17
- data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
- 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 +178 -215
- data/lib/ruby_reactor/step.rb +119 -18
- data/lib/ruby_reactor/step_signals.rb +37 -0
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +1 -72
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
- 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/web/api.rb +23 -6
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
- data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor.rb +61 -9
- 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 +34 -11
- data/lib/ruby_reactor/web/public/assets/index-B46p-M6K.css +0 -1
- data/lib/ruby_reactor/web/public/assets/index-DPmP4yXT.js +0 -22
- data/specs/001-background-async-steps/checklists/requirements.md +0 -39
- data/specs/001-background-async-steps/contracts/public-dsl.md +0 -154
- data/specs/001-background-async-steps/data-model.md +0 -117
- data/specs/001-background-async-steps/plan.md +0 -168
- data/specs/001-background-async-steps/quickstart.md +0 -102
- data/specs/001-background-async-steps/research.md +0 -150
- data/specs/001-background-async-steps/spec.md +0 -146
- data/specs/001-background-async-steps/tasks.md +0 -271
|
@@ -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,238 +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)
|
|
68
|
-
|
|
69
|
-
process_results(results, arguments[:collect_block], arguments[:fail_fast])
|
|
70
|
-
end
|
|
70
|
+
private
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fail_fast = arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
72
|
+
def should_run_async?
|
|
73
|
+
return false if context.inline_async_execution
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
inputs[:async]
|
|
76
|
+
end
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
def run_inline
|
|
79
|
+
results = execute_inline_map
|
|
80
|
+
return results if results.is_a?(RubyReactor::Failure) || results.is_a?(RubyReactor::Halt)
|
|
82
81
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
end
|
|
82
|
+
process_results(results, inputs[:collect_block], inputs[:fail_fast])
|
|
83
|
+
end
|
|
86
84
|
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
def execute_inline_map
|
|
86
|
+
results = []
|
|
87
|
+
fail_fast = inputs[:fail_fast].nil? || inputs[:fail_fast]
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
child_context = RubyReactor::Context.new(mapped_inputs, arguments[:mapped_reactor_class])
|
|
93
|
-
|
|
94
|
-
link_contexts(child_context, context)
|
|
95
|
-
|
|
96
|
-
map_id = "#{context.context_id}:#{context.current_step}"
|
|
97
|
-
storage = RubyReactor.configuration.storage_adapter
|
|
98
|
-
storage.store_map_element_context_id(map_id, child_context.context_id, context.reactor_class.name)
|
|
99
|
-
|
|
100
|
-
# Set map metadata for failure handling
|
|
101
|
-
child_context.map_metadata = {
|
|
102
|
-
map_id: map_id,
|
|
103
|
-
parent_reactor_class_name: context.reactor_class.name,
|
|
104
|
-
index: nil # Inline map execution doesn't track index in metadata currently, but could
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# Store reference in composed_contexts so the UI knows where to find elements
|
|
108
|
-
context.composed_contexts[context.current_step] = {
|
|
109
|
-
name: context.current_step,
|
|
110
|
-
type: :map_ref,
|
|
111
|
-
map_id: map_id,
|
|
112
|
-
element_reactor_class: arguments[:mapped_reactor_class].name
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
executor = RubyReactor::Executor.new(arguments[:mapped_reactor_class], {}, child_context)
|
|
116
|
-
executor.execute
|
|
117
|
-
executor.result
|
|
118
|
-
end
|
|
89
|
+
inputs[:source].each do |element|
|
|
90
|
+
result = execute_single_element(element)
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
child_context.inline_async_execution = parent_context.inline_async_execution
|
|
124
|
-
end
|
|
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)
|
|
125
95
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
begin
|
|
129
|
-
# Collect block receives Result objects when fail_fast is false, values when true
|
|
130
|
-
return RubyReactor::Success(collect_block.call(results))
|
|
131
|
-
rescue StandardError => e
|
|
132
|
-
return RubyReactor::Failure(e)
|
|
133
|
-
end
|
|
96
|
+
if fail_fast && result.failure?
|
|
97
|
+
return result # Stop immediately on first failure
|
|
134
98
|
end
|
|
135
99
|
|
|
136
|
-
#
|
|
137
|
-
|
|
100
|
+
# When fail_fast is false, store Result objects; when true, store values
|
|
101
|
+
results << (fail_fast ? result.value : result)
|
|
138
102
|
end
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
104
|
+
results
|
|
105
|
+
end
|
|
106
|
+
|
|
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
|
|
136
|
+
|
|
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)
|
|
149
150
|
end
|
|
150
151
|
end
|
|
151
152
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
prepare_async_execution(context, map_id, arguments[:source].size)
|
|
153
|
+
# Simplified: both branches returned Success(results)
|
|
154
|
+
RubyReactor::Success(results)
|
|
155
|
+
end
|
|
156
156
|
|
|
157
|
-
|
|
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)
|
|
158
161
|
|
|
159
|
-
|
|
162
|
+
reactor_class_info = build_reactor_class_info(inputs[:mapped_reactor_class], step_name)
|
|
160
163
|
|
|
161
|
-
|
|
164
|
+
initialize_map_metadata(map_id, reactor_class_info)
|
|
162
165
|
|
|
163
|
-
|
|
164
|
-
context.composed_contexts[step_name.to_s] = {
|
|
165
|
-
name: step_name.to_s,
|
|
166
|
-
type: :map_ref,
|
|
167
|
-
map_id: map_id,
|
|
168
|
-
element_reactor_class: arguments[:mapped_reactor_class].name
|
|
169
|
-
}
|
|
166
|
+
job_id = dispatch_async_map(map_id, reactor_class_info, step_name)
|
|
170
167
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
}
|
|
177
175
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Recovery metadata for the map sweeper. When this map runs inside a map
|
|
188
|
-
# element (context.map_metadata present), it is a NESTED map: its parent
|
|
189
|
-
# holds the element's `map_element:` lock, not an `async:` lock (N1).
|
|
190
|
-
def map_recovery_metadata(context, step_name)
|
|
191
|
-
outer = context.map_metadata
|
|
192
|
-
{
|
|
193
|
-
parent_context_id: context.context_id,
|
|
194
|
-
step_name: step_name.to_s,
|
|
195
|
-
parent_is_map_element: !outer.nil?,
|
|
196
|
-
outer_map_id: outer && (outer[:map_id] || outer["map_id"]),
|
|
197
|
-
outer_index: outer && (outer[:index] || outer["index"])
|
|
198
|
-
}
|
|
199
|
-
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
|
|
200
182
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
RubyReactor::Map::Dispatcher.perform(
|
|
211
|
-
map_id: map_id,
|
|
212
|
-
parent_context_id: context.context_id,
|
|
213
|
-
parent_reactor_class_name: context.reactor_class.name,
|
|
214
|
-
source: arguments[:source],
|
|
215
|
-
batch_size: batch_size,
|
|
216
|
-
step_name: step_name,
|
|
217
|
-
argument_mappings: arguments[:argument_mappings],
|
|
218
|
-
strict_ordering: arguments[:strict_ordering],
|
|
219
|
-
mapped_reactor_class: arguments[:mapped_reactor_class],
|
|
220
|
-
fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
|
|
221
|
-
)
|
|
222
|
-
queue_collector(map_id, context, step_name, arguments[:strict_ordering])
|
|
223
|
-
"map:#{map_id}"
|
|
224
|
-
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
|
|
225
191
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
234
205
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
230
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
limit ||= arguments[:source].size
|
|
254
|
-
first_job_id = nil
|
|
255
|
-
arguments[:source].each_with_index do |element, index|
|
|
256
|
-
break if index >= limit
|
|
257
|
-
|
|
258
|
-
job_id = queue_map_element(
|
|
259
|
-
map_id: map_id, element: element, index: index, arguments: arguments,
|
|
260
|
-
context: context, reactor_class_info: reactor_class_info, step_name: step_name
|
|
261
|
-
)
|
|
262
|
-
first_job_id ||= job_id
|
|
263
|
-
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
|
|
264
239
|
|
|
265
|
-
|
|
266
|
-
|
|
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 }
|
|
267
245
|
end
|
|
246
|
+
end
|
|
268
247
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
map_id: map_id, element_id: "#{map_id}:#{index}", index: index,
|
|
276
|
-
serialized_inputs: serialized_inputs, reactor_class_info: reactor_class_info,
|
|
277
|
-
strict_ordering: arguments[:strict_ordering], parent_context_id: context.context_id,
|
|
278
|
-
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
279
|
-
batch_size: arguments[:batch_size]
|
|
280
|
-
)
|
|
281
|
-
end
|
|
282
|
-
# rubocop:enable Metrics/ParameterLists
|
|
283
|
-
|
|
284
|
-
def queue_collector(map_id, context, step_name, strict_ordering)
|
|
285
|
-
RubyReactor.configuration.async_router.perform_map_collection_async(
|
|
286
|
-
parent_context_id: context.context_id, map_id: map_id,
|
|
287
|
-
parent_reactor_class_name: context.reactor_class.name, step_name: step_name.to_s,
|
|
288
|
-
strict_ordering: strict_ordering, timeout: 3600
|
|
289
|
-
)
|
|
290
|
-
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
|
+
)
|
|
291
254
|
end
|
|
292
255
|
end
|
|
293
256
|
end
|