ruby_reactor 0.4.1 → 0.5.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.rubocop.yml +1 -0
  4. data/CHANGELOG.md +14 -0
  5. data/README.md +153 -18
  6. data/lib/ruby_reactor/configuration.rb +13 -1
  7. data/lib/ruby_reactor/context.rb +2 -1
  8. data/lib/ruby_reactor/context_serializer.rb +1 -3
  9. data/lib/ruby_reactor/dsl/interrupt_builder.rb +18 -2
  10. data/lib/ruby_reactor/dsl/lockable.rb +19 -28
  11. data/lib/ruby_reactor/dsl/reactor.rb +44 -9
  12. data/lib/ruby_reactor/dsl/step_builder.rb +25 -39
  13. data/lib/ruby_reactor/dsl/validation_helpers.rb +34 -0
  14. data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
  15. data/lib/ruby_reactor/executor/compensation_manager.rb +75 -47
  16. data/lib/ruby_reactor/executor/result_handler.rb +35 -8
  17. data/lib/ruby_reactor/executor/retry_manager.rb +15 -5
  18. data/lib/ruby_reactor/executor/step_executor.rb +46 -23
  19. data/lib/ruby_reactor/executor.rb +188 -49
  20. data/lib/ruby_reactor/map/collector.rb +4 -4
  21. data/lib/ruby_reactor/map/element_executor.rb +15 -1
  22. data/lib/ruby_reactor/map/helpers.rb +17 -4
  23. data/lib/ruby_reactor/middleware.rb +13 -0
  24. data/lib/ruby_reactor/middleware_runner.rb +29 -0
  25. data/lib/ruby_reactor/open_telemetry.rb +647 -0
  26. data/lib/ruby_reactor/rate_limit.rb +28 -0
  27. data/lib/ruby_reactor/rate_limit_registry.rb +51 -0
  28. data/lib/ruby_reactor/reactor.rb +1 -0
  29. data/lib/ruby_reactor/rspec/test_subject.rb +0 -1
  30. data/lib/ruby_reactor/sidekiq_adapter.rb +7 -21
  31. data/lib/ruby_reactor/sidekiq_workers/worker.rb +4 -0
  32. data/lib/ruby_reactor/step/map_step.rb +25 -33
  33. data/lib/ruby_reactor/validation/base.rb +4 -1
  34. data/lib/ruby_reactor/validation/input_validator.rb +4 -2
  35. data/lib/ruby_reactor/validation/schema_builder.rb +82 -0
  36. data/lib/ruby_reactor/version.rb +1 -1
  37. data/lib/ruby_reactor/web/coordination_serializer.rb +12 -18
  38. data/teley/Dockerfile +60 -0
  39. metadata +6 -3
  40. data/lib/ruby_reactor/map/execution.rb +0 -101
  41. data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +0 -15
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ # Global registry of named rate limits, configured once via
5
+ # `RubyReactor.configure`. Lets multiple reactors share a single quota for an
6
+ # external service (e.g. all Stripe-calling reactors throttle against one
7
+ # `:stripe` bucket).
8
+ #
9
+ # @example
10
+ # RubyReactor.configure do |config|
11
+ # config.rate_limits.register(:stripe, limit: 3, period: :second)
12
+ # config.rate_limits.register(:twilio, limits: { second: 10, minute: 100 })
13
+ # end
14
+ #
15
+ # class ChargeReactor < RubyReactor::Reactor
16
+ # with_rate_limit(:stripe)
17
+ # end
18
+ class RateLimitRegistry
19
+ # Raised when a reactor references a rate-limit name that was never
20
+ # registered. This is a configuration error, so it propagates out of
21
+ # `execute` rather than being swallowed into a step failure result.
22
+ class UnknownLimitError < StandardError; end
23
+
24
+ def initialize
25
+ @limits = {}
26
+ end
27
+
28
+ # Register a named rate limit. Same window args as the inline DSL form:
29
+ # a single window (`limit:` + `period:`) or layered windows (`limits:`).
30
+ def register(name, limit: nil, period: nil, limits: nil)
31
+ @limits[name.to_sym] = RubyReactor::RateLimit.normalize_specs(
32
+ limit: limit, period: period, limits: limits
33
+ )
34
+ self
35
+ end
36
+
37
+ # Return the normalized spec array for a registered name, or raise if the
38
+ # name was never registered (resolved lazily at execute time, so the error
39
+ # surfaces with a clear message instead of a nil dereference).
40
+ def fetch(name)
41
+ @limits.fetch(name.to_sym) do
42
+ raise UnknownLimitError, "Unknown rate limit #{name.inspect}. " \
43
+ "Register it with config.rate_limits.register(#{name.inspect}, ...)."
44
+ end
45
+ end
46
+
47
+ def registered?(name)
48
+ @limits.key?(name.to_sym)
49
+ end
50
+ end
51
+ end
@@ -105,6 +105,7 @@ module RubyReactor
105
105
  if self.class.async? && !@context.inline_async_execution
106
106
  # For async reactors, queue a job for the whole reactor
107
107
  @context.status = :running
108
+ Executor.middlewares_for(self.class).on(:before_async_enqueue, @context)
108
109
  save_context
109
110
 
110
111
  serialized_context = ContextSerializer.serialize(@context)
@@ -432,7 +432,6 @@ module RubyReactor
432
432
  worker_classes = [
433
433
  RubyReactor::SidekiqWorkers::Worker,
434
434
  RubyReactor::SidekiqWorkers::MapElementWorker,
435
- RubyReactor::SidekiqWorkers::MapExecutionWorker,
436
435
  RubyReactor::SidekiqWorkers::MapCollectorWorker
437
436
  ]
438
437
 
@@ -41,8 +41,8 @@ module RubyReactor
41
41
 
42
42
  def self.perform_map_element_in(delay, map_id:, element_id:, index:, serialized_inputs:, reactor_class_info:,
43
43
  strict_ordering:, parent_context_id:, parent_reactor_class_name:, step_name:,
44
- batch_size: nil, serialized_context: nil)
45
- RubyReactor::SidekiqWorkers::MapElementWorker.perform_in(
44
+ batch_size: nil, serialized_context: nil, fail_fast: nil)
45
+ job_id = RubyReactor::SidekiqWorkers::MapElementWorker.perform_in(
46
46
  delay,
47
47
  {
48
48
  "map_id" => map_id,
@@ -55,9 +55,13 @@ module RubyReactor
55
55
  "parent_reactor_class_name" => parent_reactor_class_name,
56
56
  "step_name" => step_name,
57
57
  "batch_size" => batch_size,
58
- "serialized_context" => serialized_context
58
+ "serialized_context" => serialized_context,
59
+ "fail_fast" => fail_fast
59
60
  }
60
61
  )
62
+ # Return an AsyncResult so RetryManager#handle_async_retry recognises the
63
+ # element was successfully requeued and yields a RetryQueuedResult.
64
+ RubyReactor::AsyncResult.new(job_id: job_id)
61
65
  end
62
66
  # rubocop:enable Metrics/ParameterLists
63
67
 
@@ -78,23 +82,5 @@ module RubyReactor
78
82
  end
79
83
 
80
84
  # rubocop:enable Metrics/ParameterLists
81
- # rubocop:disable Metrics/ParameterLists
82
- def self.perform_map_execution_async(map_id:, serialized_inputs:, reactor_class_info:, strict_ordering:,
83
- parent_context_id:, parent_reactor_class_name:, step_name:, fail_fast: nil)
84
- # rubocop:enable Metrics/ParameterLists
85
- job_id = RubyReactor::SidekiqWorkers::MapExecutionWorker.perform_async(
86
- {
87
- "map_id" => map_id,
88
- "serialized_inputs" => serialized_inputs,
89
- "reactor_class_info" => reactor_class_info,
90
- "strict_ordering" => strict_ordering,
91
- "parent_context_id" => parent_context_id,
92
- "parent_reactor_class_name" => parent_reactor_class_name,
93
- "step_name" => step_name,
94
- "fail_fast" => fail_fast
95
- }
96
- )
97
- RubyReactor::AsyncResult.new(job_id: job_id)
98
- end
99
85
  end
100
86
  end
@@ -60,6 +60,10 @@ module RubyReactor
60
60
  # budget or appear as an error in dashboards. After the configured
61
61
  # cap is reached we escalate by marking the reactor as failed.
62
62
  handle_snooze(serialized_context, reactor_class_name, context, snooze_count, e)
63
+ rescue RubyReactor::RateLimitRegistry::UnknownLimitError => e
64
+ # Permanent configuration error — snoozing or retrying the same job
65
+ # will keep failing. Mark the context failed immediately.
66
+ escalate_snooze(context, snooze_count, e)
63
67
  end
64
68
  end
65
69
 
@@ -183,31 +183,35 @@ module RubyReactor
183
183
  )
184
184
  end
185
185
 
186
- def dispatch_async_map(map_id, arguments, context, reactor_class_info, step_name)
187
- if arguments[:batch_size]
188
- # Use new Dispatcher with Backpressure
189
- RubyReactor::Map::Dispatcher.perform(
190
- map_id: map_id,
191
- parent_context_id: context.context_id,
192
- parent_reactor_class_name: context.reactor_class.name,
193
- source: arguments[:source],
194
- batch_size: arguments[:batch_size],
195
- step_name: step_name,
196
- argument_mappings: arguments[:argument_mappings],
197
- strict_ordering: arguments[:strict_ordering],
198
- mapped_reactor_class: arguments[:mapped_reactor_class],
199
- fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
200
- )
201
- queue_collector(map_id, context, step_name, arguments[:strict_ordering])
202
- "map:#{map_id}"
203
- else
204
- queue_single_worker(map_id: map_id, arguments: arguments, context: context,
205
- reactor_class_info: reactor_class_info, step_name: step_name)
206
- end
186
+ def dispatch_async_map(map_id, arguments, context, _reactor_class_info, step_name)
187
+ # Every async map runs through the per-element Dispatcher path. When no
188
+ # batch_size is given we default to the full source size (one fan-out
189
+ # batch), so there is a single execution path: each element runs in its
190
+ # own worker, with the map counter/collector tracking completion. This
191
+ # lets elements with async steps or async retries hand off correctly
192
+ # instead of being forced to run synchronously in a single worker.
193
+ batch_size = arguments[:batch_size] || arguments[:source].size
194
+
195
+ RubyReactor::Map::Dispatcher.perform(
196
+ map_id: map_id,
197
+ parent_context_id: context.context_id,
198
+ parent_reactor_class_name: context.reactor_class.name,
199
+ source: arguments[:source],
200
+ batch_size: batch_size,
201
+ step_name: step_name,
202
+ argument_mappings: arguments[:argument_mappings],
203
+ strict_ordering: arguments[:strict_ordering],
204
+ mapped_reactor_class: arguments[:mapped_reactor_class],
205
+ fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
206
+ )
207
+ queue_collector(map_id, context, step_name, arguments[:strict_ordering])
208
+ "map:#{map_id}"
207
209
  end
208
210
 
209
211
  def prepare_async_execution(context, map_id, count)
210
212
  storage = RubyReactor.configuration.storage_adapter
213
+ middlewares = context.middlewares || Executor.middlewares_for(context.reactor_class)
214
+ middlewares.on(:before_async_enqueue, context)
211
215
  serialized_context = ContextSerializer.serialize(context)
212
216
  storage.store_context(context.context_id, serialized_context, context.reactor_class.name)
213
217
  storage.set_map_counter(map_id, count, context.reactor_class.name)
@@ -268,18 +272,6 @@ module RubyReactor
268
272
  strict_ordering: strict_ordering, timeout: 3600
269
273
  )
270
274
  end
271
-
272
- def queue_single_worker(map_id:, arguments:, context:, reactor_class_info:, step_name:)
273
- inputs = { source: arguments[:source], mappings: arguments[:argument_mappings] || {} }
274
- serialized_inputs = ContextSerializer.serialize_value(inputs)
275
-
276
- RubyReactor.configuration.async_router.perform_map_execution_async(
277
- map_id: map_id, serialized_inputs: serialized_inputs,
278
- reactor_class_info: reactor_class_info, strict_ordering: arguments[:strict_ordering],
279
- parent_context_id: context.context_id, parent_reactor_class_name: context.reactor_class.name,
280
- step_name: step_name.to_s, fail_fast: arguments[:fail_fast].nil? || arguments[:fail_fast]
281
- )
282
- end
283
275
  end
284
276
  end
285
277
  end
@@ -19,7 +19,10 @@ module RubyReactor
19
19
 
20
20
  def failure(errors)
21
21
  error = RubyReactor::Error::InputValidationError.new(errors)
22
- RubyReactor.Failure(error)
22
+ # Carry the structured field errors on the Failure itself so every
23
+ # validation failure shape (reactor inputs via `run`, step args, step
24
+ # output) exposes `validation_errors` uniformly.
25
+ RubyReactor.Failure(error, validation_errors: errors)
23
26
  end
24
27
  end
25
28
  end
@@ -5,14 +5,16 @@ require "dry-validation"
5
5
  module RubyReactor
6
6
  module Validation
7
7
  class InputValidator < Base
8
- attr_reader :schema
8
+ attr_reader :schema, :wrap_key
9
9
 
10
- def initialize(schema)
10
+ def initialize(schema, wrap_key: nil)
11
11
  super()
12
12
  @schema = schema
13
+ @wrap_key = wrap_key
13
14
  end
14
15
 
15
16
  def call(data)
17
+ data = { @wrap_key => data } if @wrap_key
16
18
  result = schema.call(data)
17
19
 
18
20
  if result.success?
@@ -12,6 +12,88 @@ module RubyReactor
12
12
  def self.build_contract_from_block(&block)
13
13
  Class.new(Dry::Validation::Contract, &block).new
14
14
  end
15
+
16
+ # Form 1 / 1b — a single named value with an optional type and predicates.
17
+ #
18
+ # build_inline(:age, :integer, false, { gteq?: 18 })
19
+ # => required(:age).filled(:integer, gteq?: 18)
20
+ # build_inline(:user, User, false, {})
21
+ # => required(:user).filled(type?: User)
22
+ # build_inline(:bio, :string, true, { max_size?: 100 })
23
+ # => optional(:bio).maybe(:string, max_size?: 100)
24
+ def self.build_inline(name, type, optional, predicates)
25
+ rules = [[name, type, optional, predicates]]
26
+ Dry::Schema.Params do
27
+ SchemaBuilder.apply_inline_rules(self, rules)
28
+ end
29
+ end
30
+
31
+ # Form 2 — the block receives the macro already bound to the name and
32
+ # optionality (`required(name)` / `optional(name)`). Because it is invoked
33
+ # as a block argument (not via instance_eval) the caller's `self` and
34
+ # lexical scope (class constants, helper methods) stay reachable.
35
+ def self.build_macro(name, optional, &block)
36
+ macro_method = optional ? :optional : :required
37
+ Dry::Schema.Params do
38
+ block.call(public_send(macro_method, name))
39
+ end
40
+ end
41
+
42
+ # Compose per-argument inline rules with an optional `validate_args`
43
+ # input (block or pre-built schema). Block/inline rules are applied first;
44
+ # a pre-built schema is merged last so its rules win.
45
+ def self.build_args(inline_rules, validate_input)
46
+ block = validate_input.is_a?(Proc) ? validate_input : nil
47
+ prebuilt = !validate_input.nil? && block.nil? ? schema_for(validate_input) : nil
48
+
49
+ composed = build_args_base(inline_rules, block)
50
+
51
+ if prebuilt && composed
52
+ composed.merge(prebuilt)
53
+ elsif prebuilt
54
+ prebuilt
55
+ else
56
+ composed
57
+ end
58
+ end
59
+
60
+ def self.build_args_base(inline_rules, block)
61
+ return Dry::Schema.Params(&block) if inline_rules.empty? && block
62
+ return nil if inline_rules.empty?
63
+
64
+ Dry::Schema.Params do
65
+ SchemaBuilder.apply_inline_rules(self, inline_rules)
66
+ instance_exec(&block) if block
67
+ end
68
+ end
69
+
70
+ # Apply a list of inline rules ([name, type, optional, predicates]) onto a
71
+ # dry-schema DSL context.
72
+ def self.apply_inline_rules(dsl, rules)
73
+ rules.each do |name, type, optional, predicates|
74
+ macro_method = optional ? :optional : :required
75
+ rule_method = optional ? :maybe : :filled
76
+ positional = type.is_a?(Symbol) ? [type] : []
77
+ kwargs = type.is_a?(Module) ? { type?: type, **predicates } : predicates
78
+
79
+ macro = dsl.public_send(macro_method, name)
80
+ if kwargs.empty?
81
+ macro.public_send(rule_method, *positional)
82
+ else
83
+ macro.public_send(rule_method, *positional, **kwargs)
84
+ end
85
+ end
86
+ end
87
+
88
+ # Unwrap an InputValidator into its underlying dry-schema; pass dry-schemas
89
+ # through unchanged.
90
+ def self.schema_for(schema_or_validator)
91
+ if schema_or_validator.is_a?(RubyReactor::Validation::InputValidator)
92
+ schema_or_validator.schema
93
+ else
94
+ schema_or_validator
95
+ end
96
+ end
15
97
  end
16
98
  end
17
99
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyReactor
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -122,34 +122,28 @@ module RubyReactor
122
122
  end
123
123
 
124
124
  {
125
- configured: {
126
- limits: config[:limits].map do |window|
127
- {
128
- name: window[:name],
129
- limit: window[:limit],
130
- period_seconds: window[:period_seconds]
131
- }
132
- end
133
- },
125
+ configured: { limits: map_limits(config[:limits]) },
134
126
  key: key,
135
127
  state: windows
136
128
  }
137
129
  rescue StandardError => e
138
130
  {
139
- configured: {
140
- limits: config[:limits]&.map do |window|
141
- {
142
- name: window[:name],
143
- limit: window[:limit],
144
- period_seconds: window[:period_seconds]
145
- }
146
- end
147
- },
131
+ configured: { limits: map_limits(config[:limits]) },
148
132
  key: nil,
149
133
  key_error: e.message
150
134
  }
151
135
  end
152
136
 
137
+ def map_limits(limits)
138
+ Array(limits).map do |window|
139
+ {
140
+ name: window[:name],
141
+ limit: window[:limit],
142
+ period_seconds: window[:period_seconds]
143
+ }
144
+ end
145
+ end
146
+
153
147
  def build_period(config, inputs, adapter)
154
148
  key = resolve_key(config[:key_proc], inputs)
155
149
  every = config[:every]
data/teley/Dockerfile ADDED
@@ -0,0 +1,60 @@
1
+ # Dockerfile for teley
2
+ FROM node:24-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Install git and other build deps
7
+ RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Clone teley repo
10
+ RUN git clone https://github.com/logaretm/teley.git .
11
+
12
+ # Allow native builds for dependencies (like esbuild) via workspace config
13
+ RUN echo "dangerouslyAllowAllBuilds: true" > pnpm-workspace.yaml
14
+
15
+ # Patch workers/src/durable-object.ts to support default_token
16
+ RUN node -e ' \
17
+ const fs = require("fs"); \
18
+ const file = "workers/src/durable-object.ts"; \
19
+ let content = fs.readFileSync(file, "utf8"); \
20
+ content = content.replace("const token = url.searchParams.get(\x27token\x27);", "const token = url.searchParams.get(\x27token\x27) || \x27default_token\x27;"); \
21
+ fs.writeFileSync(file, content, "utf8"); \
22
+ '
23
+
24
+ # Patch app/pages/live/[roomId].vue to support default_token fallback
25
+ RUN node -e ' \
26
+ const fs = require("fs"); \
27
+ const file = "app/pages/live/[roomId].vue"; \
28
+ let content = fs.readFileSync(file, "utf8"); \
29
+ content = content.replace("const token = (route.query.token as string) || \x27\x27;", "const token = (route.query.token as string) || \x27default_token\x27;"); \
30
+ fs.writeFileSync(file, content, "utf8"); \
31
+ '
32
+
33
+ # Patch app/composables/useSession.ts to default to ruby_reactor_session and default_token
34
+ RUN node -e ' \
35
+ const fs = require("fs"); \
36
+ const file = "app/composables/useSession.ts"; \
37
+ let content = fs.readFileSync(file, "utf8"); \
38
+ content = content.replace(/const initialize = async \(\): Promise<void> => {[\s\S]*?};/, "const initialize = async (): Promise<void> => {\n if (initialized.value) return;\n roomId.value = \x27ruby_reactor_session\x27;\n receiveToken.value = \x27default_token\x27;\n isNewSession.value = false;\n initialized.value = true;\n };"); \
39
+ fs.writeFileSync(file, content, "utf8"); \
40
+ '
41
+
42
+
43
+ # Enable corepack and install pnpm
44
+ RUN corepack enable && corepack prepare pnpm@latest --activate
45
+
46
+ # Install dependencies
47
+ RUN pnpm install --frozen-lockfile
48
+
49
+ # Build the Nuxt static files
50
+ RUN pnpm build
51
+
52
+ # Expose port
53
+ EXPOSE 3000
54
+
55
+ ENV HOST=0.0.0.0
56
+ ENV PORT=3000
57
+
58
+ # Start Cloudflare Pages local dev server via wrangler
59
+ WORKDIR /app/workers
60
+ CMD ["npx", "wrangler", "dev", "--ip", "0.0.0.0", "--port", "3000"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_reactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur
@@ -133,12 +133,15 @@ files:
133
133
  - lib/ruby_reactor/map/collector.rb
134
134
  - lib/ruby_reactor/map/dispatcher.rb
135
135
  - lib/ruby_reactor/map/element_executor.rb
136
- - lib/ruby_reactor/map/execution.rb
137
136
  - lib/ruby_reactor/map/helpers.rb
138
137
  - lib/ruby_reactor/map/result_enumerator.rb
139
138
  - lib/ruby_reactor/max_retries_exhausted_failure.rb
139
+ - lib/ruby_reactor/middleware.rb
140
+ - lib/ruby_reactor/middleware_runner.rb
141
+ - lib/ruby_reactor/open_telemetry.rb
140
142
  - lib/ruby_reactor/period.rb
141
143
  - lib/ruby_reactor/rate_limit.rb
144
+ - lib/ruby_reactor/rate_limit_registry.rb
142
145
  - lib/ruby_reactor/reactor.rb
143
146
  - lib/ruby_reactor/registry.rb
144
147
  - lib/ruby_reactor/retry_context.rb
@@ -152,7 +155,6 @@ files:
152
155
  - lib/ruby_reactor/sidekiq_adapter.rb
153
156
  - lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
154
157
  - lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
155
- - lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
156
158
  - lib/ruby_reactor/sidekiq_workers/worker.rb
157
159
  - lib/ruby_reactor/step.rb
158
160
  - lib/ruby_reactor/step/compose_step.rb
@@ -184,6 +186,7 @@ files:
184
186
  - llms-full.txt
185
187
  - llms.txt
186
188
  - sig/ruby_reactor.rbs
189
+ - teley/Dockerfile
187
190
  homepage: https://github.com/arturictus/ruby_reactor
188
191
  licenses: []
189
192
  metadata:
@@ -1,101 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyReactor
4
- module Map
5
- class Execution
6
- extend Helpers
7
-
8
- def self.perform(arguments)
9
- arguments = arguments.transform_keys(&:to_sym)
10
- storage = RubyReactor.configuration.storage_adapter
11
-
12
- parent_context = load_parent_context_from_storage(
13
- arguments[:parent_context_id], arguments[:parent_reactor_class_name], storage
14
- )
15
- reactor_class = resolve_reactor_class(arguments[:reactor_class_info])
16
- inputs = ContextSerializer.deserialize_value(arguments[:serialized_inputs])
17
-
18
- results = execute_all_elements(
19
- source: inputs[:source], mappings: inputs[:mappings],
20
- reactor_class: reactor_class, parent_context: parent_context,
21
- storage_options: {
22
- map_id: arguments[:map_id], storage: storage,
23
- parent_reactor_class_name: arguments[:parent_reactor_class_name],
24
- strict_ordering: arguments[:strict_ordering],
25
- fail_fast: arguments[:fail_fast]
26
- }
27
- )
28
-
29
- finalize_execution(results, parent_context, arguments[:step_name], arguments[:parent_reactor_class_name],
30
- storage)
31
- end
32
-
33
- def self.execute_all_elements(source:, mappings:, reactor_class:, parent_context:, storage_options:)
34
- # rubocop:disable Metrics/BlockLength
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
42
- element_inputs = build_element_inputs(mappings, parent_context, element)
43
-
44
- # Manually create and link context to ensure parent_context_id is set
45
- child_context = RubyReactor::Context.new(element_inputs, reactor_class)
46
- link_contexts(child_context, parent_context)
47
-
48
- # Ensure we store the element context linkage
49
- storage_options[:storage].store_map_element_context_id(
50
- storage_options[:map_id], child_context.context_id, storage_options[:parent_reactor_class_name]
51
- )
52
-
53
- # Set map metadata for failure handling
54
- metadata = {
55
- map_id: storage_options[:map_id],
56
- parent_reactor_class_name: storage_options[:parent_reactor_class_name],
57
- index: index
58
- }
59
- child_context.map_metadata = metadata
60
-
61
- executor = RubyReactor::Executor.new(reactor_class, {}, child_context)
62
- executor.execute
63
- result = executor.result
64
-
65
- store_result(result, index, storage_options)
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
-
73
- result
74
- end.compact
75
- # rubocop:enable Metrics/BlockLength
76
- end
77
-
78
- def self.link_contexts(child_context, parent_context)
79
- child_context.parent_context = parent_context
80
- child_context.root_context = parent_context.root_context || parent_context
81
- child_context.inline_async_execution = parent_context.inline_async_execution
82
- end
83
-
84
- def self.store_result(result, index, options)
85
- value = result.success? ? result.value : { _error: result.error }
86
- options[:storage].store_map_result(
87
- options[:map_id], index, ContextSerializer.serialize_value(value), options[:parent_reactor_class_name],
88
- strict_ordering: options[:strict_ordering]
89
- )
90
- end
91
-
92
- def self.finalize_execution(results, parent_context, step_name, parent_reactor_class_name, storage)
93
- step_config = Object.const_get(parent_reactor_class_name).steps[step_name.to_sym]
94
- final_result = apply_collect_block(results, step_config)
95
- resume_parent_execution(parent_context, step_name, final_result, storage)
96
- end
97
-
98
- private_class_method :execute_all_elements, :store_result, :finalize_execution
99
- end
100
- end
101
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sidekiq"
4
-
5
- module RubyReactor
6
- module SidekiqWorkers
7
- class MapExecutionWorker
8
- include ::Sidekiq::Worker
9
-
10
- def perform(arguments)
11
- RubyReactor::Map::Execution.perform(arguments)
12
- end
13
- end
14
- end
15
- end