ruby_reactor 0.4.1 → 0.5.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/.rubocop.yml +1 -0
- data/CHANGELOG.md +7 -0
- data/README.md +8 -2
- data/lib/ruby_reactor/configuration.rb +6 -1
- data/lib/ruby_reactor/context.rb +2 -1
- data/lib/ruby_reactor/context_serializer.rb +1 -3
- data/lib/ruby_reactor/dsl/reactor.rb +6 -2
- data/lib/ruby_reactor/executor/compensation_manager.rb +75 -47
- data/lib/ruby_reactor/executor/retry_manager.rb +15 -5
- data/lib/ruby_reactor/executor/step_executor.rb +36 -18
- data/lib/ruby_reactor/executor.rb +112 -36
- data/lib/ruby_reactor/map/collector.rb +4 -4
- data/lib/ruby_reactor/map/element_executor.rb +15 -1
- data/lib/ruby_reactor/map/helpers.rb +17 -4
- data/lib/ruby_reactor/middleware.rb +13 -0
- data/lib/ruby_reactor/middleware_runner.rb +29 -0
- data/lib/ruby_reactor/open_telemetry.rb +647 -0
- data/lib/ruby_reactor/reactor.rb +1 -0
- data/lib/ruby_reactor/rspec/test_subject.rb +0 -1
- data/lib/ruby_reactor/sidekiq_adapter.rb +7 -21
- data/lib/ruby_reactor/step/map_step.rb +25 -33
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/coordination_serializer.rb +12 -18
- data/teley/Dockerfile +60 -0
- metadata +5 -3
- data/lib/ruby_reactor/map/execution.rb +0 -101
- data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +0 -15
|
@@ -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
|
|
@@ -183,31 +183,35 @@ module RubyReactor
|
|
|
183
183
|
)
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
def dispatch_async_map(map_id, arguments, context,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
data/lib/ruby_reactor/version.rb
CHANGED
|
@@ -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
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
@@ -133,10 +133,12 @@ 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
|
|
142
144
|
- lib/ruby_reactor/reactor.rb
|
|
@@ -152,7 +154,6 @@ files:
|
|
|
152
154
|
- lib/ruby_reactor/sidekiq_adapter.rb
|
|
153
155
|
- lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
|
|
154
156
|
- lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
|
|
155
|
-
- lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb
|
|
156
157
|
- lib/ruby_reactor/sidekiq_workers/worker.rb
|
|
157
158
|
- lib/ruby_reactor/step.rb
|
|
158
159
|
- lib/ruby_reactor/step/compose_step.rb
|
|
@@ -184,6 +185,7 @@ files:
|
|
|
184
185
|
- llms-full.txt
|
|
185
186
|
- llms.txt
|
|
186
187
|
- sig/ruby_reactor.rbs
|
|
188
|
+
- teley/Dockerfile
|
|
187
189
|
homepage: https://github.com/arturictus/ruby_reactor
|
|
188
190
|
licenses: []
|
|
189
191
|
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
|