ruby_reactor 0.4.0 → 0.4.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-config.json +3 -0
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +8 -0
- data/lib/ruby_reactor/context.rb +5 -2
- data/lib/ruby_reactor/context_serializer.rb +46 -2
- data/lib/ruby_reactor/dsl/reactor.rb +10 -1
- data/lib/ruby_reactor/executor/result_handler.rb +1 -12
- data/lib/ruby_reactor/executor.rb +7 -1
- data/lib/ruby_reactor/reactor.rb +11 -4
- data/lib/ruby_reactor/storage/redis_adapter.rb +10 -3
- data/lib/ruby_reactor/storage/redis_locking.rb +17 -0
- data/lib/ruby_reactor/utils/backtrace_location.rb +37 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +68 -8
- data/lib/ruby_reactor/web/coordination_serializer.rb +180 -0
- data/lib/ruby_reactor/web/public/assets/index-CCnNVQy5.css +1 -0
- data/lib/ruby_reactor/web/public/assets/index-D7IBZvos.js +21 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor.rb +7 -2
- metadata +6 -4
- data/lib/ruby_reactor/web/public/assets/index-VdeLgH9k.js +0 -19
- data/lib/ruby_reactor/web/public/assets/index-_z-6BvuM.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 729d8c7da4954534a2775a360d79fc96326d76b5ca69c7361ca0433e4bd6571e
|
|
4
|
+
data.tar.gz: e9e545d2ea937135f19c7c04697993125fdfc38d57fc71d6a7b0aede9bf5efa9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23aac29ebf4c4e018fc3ab4bffbc46d79fa68ea74d783034fb984094cb7d97e791fc194c767ab59fb4992be267c171f6cbcc1897fb6160df4bedeb88faa3f080
|
|
7
|
+
data.tar.gz: edffc8600293e035e1d318a4f4b7f7240ec67aa06704e941465e06c6eadda16e1939c36e2a826bd0b92183728360cb3babab4b26b877548559f61a64f2977c4a
|
data/.release-please-config.json
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.1](https://github.com/arturictus/ruby_reactor/compare/v0.4.0...v0.4.1) (2026-05-25)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* trigger release pipeline ([#29](https://github.com/arturictus/ruby_reactor/issues/29)) ([862478b](https://github.com/arturictus/ruby_reactor/commit/862478b3d0811b00e920119057bf4c1bfb1808af))
|
|
9
|
+
* trigger release workflows ([#31](https://github.com/arturictus/ruby_reactor/issues/31)) ([ed44dcd](https://github.com/arturictus/ruby_reactor/commit/ed44dcd00e3288e2fab99f9794821943dacc1d4b))
|
|
10
|
+
|
|
3
11
|
## [0.4.0](https://github.com/arturictus/ruby_reactor/compare/ruby_reactor-v0.3.2...ruby_reactor/v0.4.0) (2026-05-17)
|
|
4
12
|
|
|
5
13
|
|
data/lib/ruby_reactor/context.rb
CHANGED
|
@@ -5,7 +5,7 @@ module RubyReactor
|
|
|
5
5
|
attr_accessor :inputs, :intermediate_results, :private_data, :current_step, :retry_count, :concurrency_key,
|
|
6
6
|
:retry_context, :reactor_class, :execution_trace, :inline_async_execution, :undo_stack,
|
|
7
7
|
:parent_context, :root_context, :composed_contexts, :context_id, :map_operations, :map_metadata,
|
|
8
|
-
:cancelled, :cancellation_reason, :parent_context_id, :status, :failure_reason
|
|
8
|
+
:cancelled, :cancellation_reason, :parent_context_id, :retried_from_id, :status, :failure_reason
|
|
9
9
|
|
|
10
10
|
def initialize(inputs = {}, reactor_class = nil)
|
|
11
11
|
@context_id = SecureRandom.uuid
|
|
@@ -29,6 +29,7 @@ module RubyReactor
|
|
|
29
29
|
@failure_reason = nil
|
|
30
30
|
@parent_context = nil
|
|
31
31
|
@parent_context_id = nil
|
|
32
|
+
@retried_from_id = nil
|
|
32
33
|
@root_context = nil
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -119,7 +120,8 @@ module RubyReactor
|
|
|
119
120
|
cancellation_reason: @cancellation_reason,
|
|
120
121
|
status: @status,
|
|
121
122
|
failure_reason: ContextSerializer.serialize_value(@failure_reason),
|
|
122
|
-
parent_context_id: @parent_context&.context_id || @parent_context_id
|
|
123
|
+
parent_context_id: @parent_context&.context_id || @parent_context_id,
|
|
124
|
+
retried_from_id: @retried_from_id
|
|
123
125
|
}
|
|
124
126
|
end
|
|
125
127
|
|
|
@@ -144,6 +146,7 @@ module RubyReactor
|
|
|
144
146
|
context.status = data["status"] || "pending"
|
|
145
147
|
context.failure_reason = ContextSerializer.deserialize_value(data["failure_reason"])
|
|
146
148
|
context.parent_context_id = data["parent_context_id"]
|
|
149
|
+
context.retried_from_id = data["retried_from_id"]
|
|
147
150
|
|
|
148
151
|
context
|
|
149
152
|
end
|
|
@@ -181,19 +181,63 @@ module RubyReactor
|
|
|
181
181
|
def simplify_for_api(value)
|
|
182
182
|
case value
|
|
183
183
|
when Hash
|
|
184
|
-
value.each_with_object({}) do |(k, v), hash|
|
|
184
|
+
simplified = value.each_with_object({}) do |(k, v), hash|
|
|
185
185
|
hash[k.to_s] = simplify_for_api(v)
|
|
186
186
|
end
|
|
187
|
+
enrich_failure_for_api(simplified)
|
|
187
188
|
when Array
|
|
188
189
|
value.map { |v| simplify_for_api(v) }
|
|
189
190
|
when Success, Failure, Context
|
|
190
|
-
simplify_for_api(value.to_h)
|
|
191
|
+
enrich_failure_for_api(simplify_for_api(value.to_h))
|
|
191
192
|
when Symbol
|
|
192
193
|
value.to_s
|
|
193
194
|
else
|
|
194
195
|
value
|
|
195
196
|
end
|
|
196
197
|
end
|
|
198
|
+
|
|
199
|
+
def enrich_failure_for_api(hash)
|
|
200
|
+
return hash unless hash.is_a?(Hash)
|
|
201
|
+
return hash unless failure_payload?(hash)
|
|
202
|
+
|
|
203
|
+
hash = flatten_typed_failure(hash) if hash["_type"] == "Failure"
|
|
204
|
+
|
|
205
|
+
if hash["code_snippet"].is_a?(Array) && !hash["code_snippet"].empty?
|
|
206
|
+
return hash
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
file_path, line_number = resolve_failure_location(hash)
|
|
210
|
+
return hash unless file_path && line_number
|
|
211
|
+
|
|
212
|
+
snippet = RubyReactor::Utils::CodeExtractor.extract(file_path, line_number)
|
|
213
|
+
return hash unless snippet
|
|
214
|
+
|
|
215
|
+
normalized_snippet = snippet.map { |line| line.transform_keys(&:to_s) }
|
|
216
|
+
|
|
217
|
+
hash.merge(
|
|
218
|
+
"file_path" => file_path,
|
|
219
|
+
"line_number" => line_number,
|
|
220
|
+
"code_snippet" => normalized_snippet
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def failure_payload?(hash)
|
|
225
|
+
hash["_type"] == "Failure" || (hash.key?("step_name") && hash["backtrace"].is_a?(Array))
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def flatten_typed_failure(hash)
|
|
229
|
+
flattened = hash.dup
|
|
230
|
+
flattened.delete("_type")
|
|
231
|
+
flattened.merge("message" => hash["error"] || hash["message"])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def resolve_failure_location(hash)
|
|
235
|
+
file_path = hash["file_path"]
|
|
236
|
+
line_number = hash["line_number"]
|
|
237
|
+
return [file_path, line_number] if file_path && line_number
|
|
238
|
+
|
|
239
|
+
RubyReactor::Utils::BacktraceLocation.extract(hash["backtrace"])
|
|
240
|
+
end
|
|
197
241
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
198
242
|
|
|
199
243
|
private
|
|
@@ -152,12 +152,21 @@ module RubyReactor
|
|
|
152
152
|
# Entry point for running the reactor
|
|
153
153
|
def run(inputs = {})
|
|
154
154
|
reactor = new
|
|
155
|
-
reactor.run(inputs)
|
|
155
|
+
result = reactor.run(inputs)
|
|
156
|
+
attach_execution_id!(result, reactor.context.context_id)
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
def call(inputs = {})
|
|
159
160
|
run(inputs)
|
|
160
161
|
end
|
|
162
|
+
|
|
163
|
+
def attach_execution_id!(result, execution_id)
|
|
164
|
+
return result if result.respond_to?(:execution_id) && result.execution_id
|
|
165
|
+
|
|
166
|
+
result.define_singleton_method(:execution_id) { execution_id }
|
|
167
|
+
result
|
|
168
|
+
end
|
|
169
|
+
private :attach_execution_id!
|
|
161
170
|
end
|
|
162
171
|
end
|
|
163
172
|
end
|
|
@@ -189,18 +189,7 @@ module RubyReactor
|
|
|
189
189
|
end
|
|
190
190
|
|
|
191
191
|
def extract_location(backtrace)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# Filter out internal reactor frames if needed, or just take the first one
|
|
195
|
-
# For now, let's take the first line of the backtrace which should be the error source
|
|
196
|
-
# But we might want to skip our own internal frames if we want to point to user code
|
|
197
|
-
# Let's start with the top frame, assuming backtrace is already correct (from original error)
|
|
198
|
-
|
|
199
|
-
first_line = backtrace.first
|
|
200
|
-
match = first_line.match(/^(.+?):(\d+)(?::in `.*')?$/)
|
|
201
|
-
return [nil, nil] unless match
|
|
202
|
-
|
|
203
|
-
[match[1], match[2].to_i]
|
|
192
|
+
RubyReactor::Utils::BacktraceLocation.extract(backtrace)
|
|
204
193
|
end
|
|
205
194
|
end
|
|
206
195
|
end
|
|
@@ -75,7 +75,7 @@ module RubyReactor
|
|
|
75
75
|
@result
|
|
76
76
|
ensure
|
|
77
77
|
release_locks
|
|
78
|
-
save_context
|
|
78
|
+
save_context if persist_context?
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def resume_execution
|
|
@@ -134,6 +134,12 @@ module RubyReactor
|
|
|
134
134
|
storage.store_context(@context.context_id, serialized_context, reactor_class_name)
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
def persist_context?
|
|
138
|
+
@context.status.to_s != "pending" ||
|
|
139
|
+
@context.execution_trace.any? ||
|
|
140
|
+
@context.intermediate_results.any?
|
|
141
|
+
end
|
|
142
|
+
|
|
137
143
|
private
|
|
138
144
|
|
|
139
145
|
def acquire_locks
|
data/lib/ruby_reactor/reactor.rb
CHANGED
|
@@ -10,10 +10,17 @@ module RubyReactor
|
|
|
10
10
|
|
|
11
11
|
def self.find(id)
|
|
12
12
|
reactor_class_name = name
|
|
13
|
-
|
|
14
|
-
raise Error::ValidationError, "Context '#{id}' not found" unless
|
|
15
|
-
|
|
16
|
-
context =
|
|
13
|
+
raw_data = configuration.storage_adapter.retrieve_context(id, reactor_class_name)
|
|
14
|
+
raise Error::ValidationError, "Context '#{id}' not found" unless raw_data
|
|
15
|
+
|
|
16
|
+
context = case raw_data
|
|
17
|
+
when String
|
|
18
|
+
ContextSerializer.deserialize(raw_data)
|
|
19
|
+
when Hash
|
|
20
|
+
Context.deserialize_from_retry(raw_data)
|
|
21
|
+
else
|
|
22
|
+
raise Error::ValidationError, "Invalid context format for '#{id}'"
|
|
23
|
+
end
|
|
17
24
|
new(context)
|
|
18
25
|
end
|
|
19
26
|
|
|
@@ -196,13 +196,20 @@ module RubyReactor
|
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
def determine_status(data)
|
|
199
|
-
|
|
199
|
+
status = data["status"].to_s
|
|
200
|
+
return status if status && %w[failed paused completed running skipped pending].include?(status)
|
|
200
201
|
return "cancelled" if data["cancelled"]
|
|
201
202
|
# Heuristic
|
|
202
203
|
return "failed" if data["retry_count"]&.positive? && !data["current_step"].nil?
|
|
203
|
-
return "
|
|
204
|
+
return "running" if data["current_step"]
|
|
205
|
+
return "completed" if execution_evidence?(data)
|
|
204
206
|
|
|
205
|
-
"
|
|
207
|
+
"pending"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def execution_evidence?(data)
|
|
211
|
+
(data["execution_trace"] || []).any? ||
|
|
212
|
+
(data["intermediate_results"] || {}).any?
|
|
206
213
|
end
|
|
207
214
|
|
|
208
215
|
def store_map_element_context_id(map_id, context_id, reactor_class_name)
|
|
@@ -245,6 +245,23 @@ module RubyReactor
|
|
|
245
245
|
def period_marker?(key_base, every, now: Time.now.utc)
|
|
246
246
|
@redis.exists?(RubyReactor::Period.key(key_base, every, now: now))
|
|
247
247
|
end
|
|
248
|
+
|
|
249
|
+
# TTL in seconds for a held lock (-2 if key does not exist).
|
|
250
|
+
def lock_ttl(prefixed_key)
|
|
251
|
+
@redis.ttl(prefixed_key)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# TTL in seconds for the current rate-limit bucket (-2 if unset).
|
|
255
|
+
def rate_limit_ttl(key_base, every, now: Time.now.to_i)
|
|
256
|
+
period_seconds = RubyReactor::Period.period_seconds(every)
|
|
257
|
+
bucket = now / period_seconds
|
|
258
|
+
@redis.ttl("rate:#{key_base}:#{every}:#{bucket}")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# TTL in seconds for a period marker (-2 if unset).
|
|
262
|
+
def period_ttl(key_base, every, now: Time.now.utc)
|
|
263
|
+
@redis.ttl(RubyReactor::Period.key(key_base, every, now: now))
|
|
264
|
+
end
|
|
248
265
|
end
|
|
249
266
|
# rubocop:enable Naming/PredicateMethod
|
|
250
267
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Utils
|
|
5
|
+
class BacktraceLocation
|
|
6
|
+
# Ruby 3.x backtraces use single-quoted method names; older formats use backticks.
|
|
7
|
+
LINE_PATTERN = /^(.+?):(\d+)(?::in .*)?$/
|
|
8
|
+
|
|
9
|
+
def self.parse(line)
|
|
10
|
+
match = line.match(LINE_PATTERN)
|
|
11
|
+
return nil unless match
|
|
12
|
+
|
|
13
|
+
[match[1], match[2].to_i]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.internal_path?(file_path)
|
|
17
|
+
file_path.start_with?(RubyReactor.internal_lib_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.extract(backtrace)
|
|
21
|
+
return [nil, nil] unless backtrace.is_a?(Array) && backtrace.any?
|
|
22
|
+
|
|
23
|
+
skip_internal = ENV["RUBY_REACTOR_DEBUG"] != "true"
|
|
24
|
+
|
|
25
|
+
backtrace.each do |line|
|
|
26
|
+
file_path, line_number = parse(line)
|
|
27
|
+
next unless file_path
|
|
28
|
+
next if skip_internal && internal_path?(file_path)
|
|
29
|
+
|
|
30
|
+
return [file_path, line_number]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
parse(backtrace.first) || [nil, nil]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/ruby_reactor/version.rb
CHANGED
data/lib/ruby_reactor/web/api.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "roda"
|
|
4
|
+
require_relative "coordination_serializer"
|
|
4
5
|
|
|
5
6
|
module RubyReactor
|
|
6
7
|
module Web
|
|
@@ -35,16 +36,12 @@ module RubyReactor
|
|
|
35
36
|
|
|
36
37
|
structure = self.class.build_structure(reactor_class) if reactor_class.respond_to?(:steps)
|
|
37
38
|
|
|
39
|
+
api_status = self.class.reactor_status(data)
|
|
40
|
+
|
|
38
41
|
response_data = {
|
|
39
42
|
id: data[:context_id],
|
|
40
43
|
class: data[:reactor_class].to_s,
|
|
41
|
-
status:
|
|
42
|
-
data[:status].to_s
|
|
43
|
-
elsif data[:cancelled]
|
|
44
|
-
"cancelled"
|
|
45
|
-
else
|
|
46
|
-
(data[:current_step] ? "running" : "completed")
|
|
47
|
-
end,
|
|
44
|
+
status: api_status,
|
|
48
45
|
current_step: data[:current_step].to_s,
|
|
49
46
|
retry_count: data[:retry_count] || 0,
|
|
50
47
|
undo_stack: data[:undo_stack] || [],
|
|
@@ -58,6 +55,11 @@ module RubyReactor
|
|
|
58
55
|
data[:composed_contexts] || {},
|
|
59
56
|
data[:reactor_class]&.to_s
|
|
60
57
|
),
|
|
58
|
+
coordination: CoordinationSerializer.build(
|
|
59
|
+
reactor_class,
|
|
60
|
+
inputs: data[:inputs],
|
|
61
|
+
context_id: data[:context_id]
|
|
62
|
+
),
|
|
61
63
|
error: data[:failure_reason]
|
|
62
64
|
}
|
|
63
65
|
|
|
@@ -66,7 +68,43 @@ module RubyReactor
|
|
|
66
68
|
|
|
67
69
|
# POST /api/reactors/:id/retry
|
|
68
70
|
r.post "retry" do
|
|
69
|
-
|
|
71
|
+
data = RubyReactor::Configuration.instance.storage_adapter.find_context_by_id(reactor_id)
|
|
72
|
+
unless data
|
|
73
|
+
response.status = 404
|
|
74
|
+
next { error: "Reactor not found" }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
deserialized = ContextSerializer.deserialize_value(data)
|
|
78
|
+
unless self.class.reactor_status(deserialized) == "failed"
|
|
79
|
+
response.status = 422
|
|
80
|
+
next { error: "Reactor can only be retried when failed" }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
reactor_class_name = deserialized[:reactor_class].to_s
|
|
84
|
+
reactor_class = Context.resolve_reactor_class(reactor_class_name)
|
|
85
|
+
unless reactor_class
|
|
86
|
+
response.status = 422
|
|
87
|
+
next { error: "Reactor class '#{reactor_class_name}' not found" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
inputs = self.class.extract_retry_inputs(deserialized)
|
|
92
|
+
result = reactor_class.run(inputs)
|
|
93
|
+
new_id = result.execution_id
|
|
94
|
+
|
|
95
|
+
new_reactor = reactor_class.find(new_id)
|
|
96
|
+
new_reactor.context.retried_from_id = reactor_id
|
|
97
|
+
new_reactor.context.retry_count = (deserialized[:retry_count] || 0) + 1
|
|
98
|
+
new_reactor.send(:save_context)
|
|
99
|
+
|
|
100
|
+
{ success: true, id: new_id }
|
|
101
|
+
rescue RubyReactor::Error::ValidationError => e
|
|
102
|
+
response.status = 422
|
|
103
|
+
{ error: e.message }
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
response.status = 500
|
|
106
|
+
{ error: e.message }
|
|
107
|
+
end
|
|
70
108
|
end
|
|
71
109
|
|
|
72
110
|
# POST /api/reactors/:id/cancel
|
|
@@ -107,6 +145,28 @@ module RubyReactor
|
|
|
107
145
|
end
|
|
108
146
|
end
|
|
109
147
|
|
|
148
|
+
def self.reactor_status(data)
|
|
149
|
+
status = data[:status].to_s
|
|
150
|
+
return status if %w[failed paused completed running skipped pending].include?(status)
|
|
151
|
+
return "cancelled" if data[:cancelled]
|
|
152
|
+
return "running" if data[:current_step]
|
|
153
|
+
return "completed" if execution_evidence?(data)
|
|
154
|
+
|
|
155
|
+
"pending"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.execution_evidence?(data)
|
|
159
|
+
(data[:execution_trace] || []).any? ||
|
|
160
|
+
(data[:intermediate_results] || {}).any?
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.extract_retry_inputs(data)
|
|
164
|
+
inputs = data[:inputs] || {}
|
|
165
|
+
return {} unless inputs.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
inputs.transform_keys(&:to_sym)
|
|
168
|
+
end
|
|
169
|
+
|
|
110
170
|
def self.build_structure(reactor_class)
|
|
111
171
|
return {} unless reactor_class.respond_to?(:steps)
|
|
112
172
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
module Web
|
|
5
|
+
class CoordinationSerializer
|
|
6
|
+
class << self
|
|
7
|
+
def build(reactor_class, inputs:, context_id:)
|
|
8
|
+
return {} unless reactor_class
|
|
9
|
+
|
|
10
|
+
adapter = RubyReactor.configuration.storage_adapter
|
|
11
|
+
normalized_inputs = normalize_inputs(inputs)
|
|
12
|
+
result = {}
|
|
13
|
+
|
|
14
|
+
if reactor_class.lock_config
|
|
15
|
+
result[:lock] = build_lock(reactor_class.lock_config, normalized_inputs, context_id, adapter)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if reactor_class.semaphore_config
|
|
19
|
+
result[:semaphore] = build_semaphore(reactor_class.semaphore_config, normalized_inputs, adapter)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if reactor_class.rate_limit_config
|
|
23
|
+
result[:rate_limit] = build_rate_limit(reactor_class.rate_limit_config, normalized_inputs, adapter)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if reactor_class.period_config
|
|
27
|
+
result[:period] = build_period(reactor_class.period_config, normalized_inputs, adapter)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def normalize_inputs(inputs)
|
|
36
|
+
return {} unless inputs.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
inputs.transform_keys(&:to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resolve_key(key_proc, inputs)
|
|
42
|
+
key_proc.call(inputs).to_s
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_lock(config, inputs, context_id, adapter)
|
|
46
|
+
key = resolve_key(config[:key_proc], inputs)
|
|
47
|
+
prefixed = "lock:#{key}"
|
|
48
|
+
info = adapter.lock_info(prefixed)
|
|
49
|
+
ttl = adapter.lock_ttl(prefixed)
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
configured: {
|
|
53
|
+
ttl: config[:ttl],
|
|
54
|
+
wait: config[:wait],
|
|
55
|
+
auto_extend: config.fetch(:auto_extend, true)
|
|
56
|
+
},
|
|
57
|
+
key: key,
|
|
58
|
+
state: lock_state(info, context_id, ttl)
|
|
59
|
+
}
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
lock_error_payload(config, e)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def lock_state(info, context_id, ttl)
|
|
65
|
+
if info
|
|
66
|
+
{
|
|
67
|
+
held: true,
|
|
68
|
+
owner: info[:owner],
|
|
69
|
+
owned_by_this_context: info[:owner] == context_id,
|
|
70
|
+
reentrant_count: info[:count],
|
|
71
|
+
ttl: ttl
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
{ held: false, ttl: ttl }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def lock_error_payload(config, error)
|
|
79
|
+
{
|
|
80
|
+
configured: {
|
|
81
|
+
ttl: config[:ttl],
|
|
82
|
+
wait: config[:wait],
|
|
83
|
+
auto_extend: config.fetch(:auto_extend, true)
|
|
84
|
+
},
|
|
85
|
+
key: nil,
|
|
86
|
+
key_error: error.message
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_semaphore(config, inputs, adapter)
|
|
91
|
+
key = resolve_key(config[:key_proc], inputs)
|
|
92
|
+
state = adapter.semaphore_state(key)
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
configured: {
|
|
96
|
+
limit: config[:limit],
|
|
97
|
+
wait: config[:wait]
|
|
98
|
+
},
|
|
99
|
+
key: key,
|
|
100
|
+
state: state
|
|
101
|
+
}
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
{
|
|
104
|
+
configured: { limit: config[:limit], wait: config[:wait] },
|
|
105
|
+
key: nil,
|
|
106
|
+
key_error: e.message
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_rate_limit(config, inputs, adapter)
|
|
111
|
+
key = resolve_key(config[:key_proc], inputs)
|
|
112
|
+
now = Time.now.to_i
|
|
113
|
+
windows = config[:limits].map do |window|
|
|
114
|
+
every = window[:name].to_sym
|
|
115
|
+
{
|
|
116
|
+
name: window[:name],
|
|
117
|
+
limit: window[:limit],
|
|
118
|
+
period_seconds: window[:period_seconds],
|
|
119
|
+
count: adapter.rate_limit_count(key, every, now: now),
|
|
120
|
+
ttl: adapter.rate_limit_ttl(key, every, now: now)
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
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
|
+
},
|
|
134
|
+
key: key,
|
|
135
|
+
state: windows
|
|
136
|
+
}
|
|
137
|
+
rescue StandardError => e
|
|
138
|
+
{
|
|
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
|
+
},
|
|
148
|
+
key: nil,
|
|
149
|
+
key_error: e.message
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_period(config, inputs, adapter)
|
|
154
|
+
key = resolve_key(config[:key_proc], inputs)
|
|
155
|
+
every = config[:every]
|
|
156
|
+
bucket_key = RubyReactor::Period.key(key, every)
|
|
157
|
+
marked = adapter.period_marker?(key, every)
|
|
158
|
+
ttl = adapter.period_ttl(key, every)
|
|
159
|
+
|
|
160
|
+
{
|
|
161
|
+
configured: { every: every.to_s },
|
|
162
|
+
key: key,
|
|
163
|
+
bucket_key: bucket_key,
|
|
164
|
+
state: {
|
|
165
|
+
marked: marked,
|
|
166
|
+
ttl: ttl
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
rescue StandardError => e
|
|
170
|
+
{
|
|
171
|
+
configured: { every: config[:every].to_s },
|
|
172
|
+
key: nil,
|
|
173
|
+
bucket_key: nil,
|
|
174
|
+
key_error: e.message
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|