ruby_reactor 0.5.4 → 0.6.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/speckit-agent-context-update/SKILL.md +32 -0
- data/.claude/skills/speckit-analyze/SKILL.md +262 -0
- data/.claude/skills/speckit-checklist/SKILL.md +374 -0
- data/.claude/skills/speckit-clarify/SKILL.md +286 -0
- data/.claude/skills/speckit-constitution/SKILL.md +157 -0
- data/.claude/skills/speckit-converge/SKILL.md +277 -0
- data/.claude/skills/speckit-implement/SKILL.md +224 -0
- data/.claude/skills/speckit-plan/SKILL.md +171 -0
- data/.claude/skills/speckit-specify/SKILL.md +346 -0
- data/.claude/skills/speckit-tasks/SKILL.md +215 -0
- data/.claude/skills/speckit-taskstoissues/SKILL.md +110 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/extensions/.registry +19 -0
- data/.specify/extensions/agent-context/README.md +66 -0
- data/.specify/extensions/agent-context/agent-context-config.yml +5 -0
- data/.specify/extensions/agent-context/commands/speckit.agent-context.update.md +27 -0
- data/.specify/extensions/agent-context/extension.yml +34 -0
- data/.specify/extensions/agent-context/scripts/bash/update-agent-context.sh +282 -0
- data/.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 +353 -0
- data/.specify/extensions.yml +23 -0
- data/.specify/feature.json +3 -0
- data/.specify/init-options.json +9 -0
- data/.specify/integration.json +15 -0
- data/.specify/integrations/claude.manifest.json +17 -0
- data/.specify/integrations/speckit.manifest.json +17 -0
- data/.specify/memory/constitution.md +134 -0
- data/.specify/scripts/bash/check-prerequisites.sh +189 -0
- data/.specify/scripts/bash/common.sh +619 -0
- data/.specify/scripts/bash/create-new-feature.sh +299 -0
- data/.specify/scripts/bash/setup-plan.sh +84 -0
- data/.specify/scripts/bash/setup-tasks.sh +91 -0
- data/.specify/templates/checklist-template.md +40 -0
- data/.specify/templates/constitution-template.md +50 -0
- data/.specify/templates/plan-template.md +113 -0
- data/.specify/templates/spec-template.md +131 -0
- data/.specify/templates/tasks-template.md +252 -0
- data/.specify/workflows/speckit/workflow.yml +77 -0
- data/.specify/workflows/workflow-registry.json +13 -0
- data/CHANGELOG.md +7 -0
- data/README.md +51 -24
- data/lib/ruby_reactor/adapters/active_job/compat.rb +24 -0
- data/lib/ruby_reactor/adapters/active_job/map_collector_worker.rb +19 -0
- data/lib/ruby_reactor/adapters/active_job/map_element_worker.rb +19 -0
- data/lib/ruby_reactor/adapters/active_job/router.rb +91 -0
- data/lib/ruby_reactor/adapters/active_job/sweeper_worker.rb +16 -0
- data/lib/ruby_reactor/adapters/active_job/worker.rb +24 -0
- data/lib/ruby_reactor/adapters/sidekiq/map_collector_worker.rb +15 -0
- data/lib/ruby_reactor/adapters/sidekiq/map_element_worker.rb +15 -0
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +91 -0
- data/lib/ruby_reactor/adapters/sidekiq/sweeper_worker.rb +19 -0
- data/lib/ruby_reactor/adapters/sidekiq/worker.rb +25 -0
- data/lib/ruby_reactor/configuration.rb +24 -4
- data/lib/ruby_reactor/map/element_executor.rb +1 -1
- data/lib/ruby_reactor/rspec/active_job_helpers.rb +52 -0
- data/lib/ruby_reactor/rspec/async_test_helpers.rb +41 -0
- data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +3 -3
- data/lib/ruby_reactor/rspec/test_subject.rb +11 -7
- data/lib/ruby_reactor/rspec.rb +4 -0
- data/lib/ruby_reactor/sweeper_job.rb +70 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/worker.rb +226 -0
- data/lib/ruby_reactor.rb +40 -1
- data/specs/active_job.md +259 -0
- metadata +54 -6
- data/lib/ruby_reactor/sidekiq_adapter.rb +0 -87
- data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +0 -13
- data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +0 -13
- data/lib/ruby_reactor/sidekiq_workers/sweeper_worker.rb +0 -73
- data/lib/ruby_reactor/sidekiq_workers/worker.rb +0 -222
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyReactor
|
|
4
|
+
# Framework-agnostic resume/snooze/escalate logic shared by every queueing
|
|
5
|
+
# backend's worker class. Each backend (`Adapters::Sidekiq::Worker`,
|
|
6
|
+
# `Adapters::ActiveJob::Worker`, ...) includes this and supplies its own
|
|
7
|
+
# `self.class.perform_in` (native on Sidekiq::Worker, via
|
|
8
|
+
# `Adapters::ActiveJob::Compat` on ActiveJob::Base) — nothing here references
|
|
9
|
+
# a specific backend.
|
|
10
|
+
module Worker
|
|
11
|
+
# Identity-only payload: storage is the source of truth. Rehydrate the live
|
|
12
|
+
# context from storage by id, then resume. A nil read means the context was
|
|
13
|
+
# swept, expired, or already terminal-and-collected — nothing to resume.
|
|
14
|
+
def perform(context_id, reactor_class_name = nil, snooze_count = 0)
|
|
15
|
+
# Normalize so a nil/omitted name resolves to the same storage key the
|
|
16
|
+
# enqueue path wrote (always via reactor_storage_name). Without this a
|
|
17
|
+
# nil here builds "reactor::context:<id>" and misses the stored
|
|
18
|
+
# "reactor:AnonymousReactor:context:<id>", silently no-op'ing.
|
|
19
|
+
reactor_class_name ||= RubyReactor.reactor_storage_name(nil)
|
|
20
|
+
data = RubyReactor.configuration.storage_adapter.retrieve_context(context_id, reactor_class_name)
|
|
21
|
+
return if data.nil?
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
context = ContextSerializer.deserialize_hash(data)
|
|
25
|
+
rescue RubyReactor::Error::DeserializationError,
|
|
26
|
+
RubyReactor::Error::SchemaVersionError => e
|
|
27
|
+
# Permanent failures — re-reading the same stored blob will keep
|
|
28
|
+
# failing. Mark the context as failed (best-effort) and return so
|
|
29
|
+
# the job does not burn its retry budget.
|
|
30
|
+
handle_deserialization_failure(context_id, reactor_class_name, e)
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
resolve_reactor_class!(context, reactor_class_name)
|
|
35
|
+
unless context.reactor_class
|
|
36
|
+
# Still unresolved (class not loaded, or an anonymous reactor with no
|
|
37
|
+
# storage name to look up) — Executor.new below would blow up on a nil
|
|
38
|
+
# class and burn the job's retry budget forever. Fail the context now.
|
|
39
|
+
error = RubyReactor::Error::DeserializationError.new(
|
|
40
|
+
"reactor class '#{reactor_class_name}' could not be resolved"
|
|
41
|
+
)
|
|
42
|
+
handle_deserialization_failure(context_id, reactor_class_name, error)
|
|
43
|
+
return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mark that we're executing inline to prevent nested async calls
|
|
47
|
+
context.inline_async_execution = true
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
# Resume execution from the failed step
|
|
51
|
+
executor = Executor.new(context.reactor_class, {}, context)
|
|
52
|
+
executor.resume_execution
|
|
53
|
+
# No explicit save here: resume_execution's ensure block already persists
|
|
54
|
+
# the final root state (`save_context unless skip_context_persist?`), and
|
|
55
|
+
# in the worker the executor's context IS the root, so an extra checkpoint!
|
|
56
|
+
# would just re-write the identical blob to the identical key. The
|
|
57
|
+
# skip_context_persist? guard (stale-batch redelivery of an already-terminal
|
|
58
|
+
# context) is likewise honored there.
|
|
59
|
+
|
|
60
|
+
# Return the executor (which now has the result stored in it)
|
|
61
|
+
executor
|
|
62
|
+
rescue RubyReactor::Lock::AcquisitionError,
|
|
63
|
+
RubyReactor::Semaphore::AcquisitionError,
|
|
64
|
+
RubyReactor::RateLimit::ExceededError,
|
|
65
|
+
RubyReactor::OrderedLock::WaitError => e
|
|
66
|
+
# Snooze on expected concurrency, rate, or ordering contention.
|
|
67
|
+
# OrderedLock::WaitError carries a poison-pill-derived retry hint,
|
|
68
|
+
# consumed by compute_snooze_delay below. We avoid the framework's native
|
|
69
|
+
# retry path so this doesn't burn the job's retry budget or appear
|
|
70
|
+
# as an error in dashboards. After the configured cap is reached we
|
|
71
|
+
# escalate by marking the reactor as failed.
|
|
72
|
+
handle_snooze(context_id, reactor_class_name, context, snooze_count, e)
|
|
73
|
+
rescue RubyReactor::RateLimitRegistry::UnknownLimitError => e
|
|
74
|
+
# Permanent configuration error — snoozing or retrying the same job
|
|
75
|
+
# will keep failing. Mark the context failed immediately.
|
|
76
|
+
escalate_snooze(context, snooze_count, e)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# If reactor_class_name is provided, use it to get the reactor class.
|
|
83
|
+
# This handles cases where the class can't be found via const_get.
|
|
84
|
+
def resolve_reactor_class!(context, reactor_class_name)
|
|
85
|
+
return unless reactor_class_name && context.reactor_class.nil?
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
context.reactor_class = Object.const_get(reactor_class_name)
|
|
89
|
+
rescue NameError
|
|
90
|
+
# If not found, try to find it in the current namespace
|
|
91
|
+
# This is a fallback for test environments
|
|
92
|
+
context.reactor_class = reactor_class_name.constantize if reactor_class_name.respond_to?(:constantize)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def handle_snooze(context_id, reactor_class_name, context, snooze_count, error)
|
|
97
|
+
config = RubyReactor.configuration
|
|
98
|
+
max = config.lock_snooze_max_attempts
|
|
99
|
+
|
|
100
|
+
# OrderedLock::WaitError bypasses the snooze cap. The gate's
|
|
101
|
+
# poison_pill_timeout is the only meaningful upper bound on how long a
|
|
102
|
+
# nonce can legitimately wait; capping snoozes would either fail jobs
|
|
103
|
+
# prematurely or strand the nonce in `assigned_at` until poison_pill
|
|
104
|
+
# eventually advances past it. Snooze until the gate passes (or poison
|
|
105
|
+
# auto-advance moves the cursor past us).
|
|
106
|
+
# The per-context liveness lock (`async:<id>`) is also uncapped: a
|
|
107
|
+
# duplicate of the *same* execution may wait arbitrarily long for the
|
|
108
|
+
# live original to finish (e.g. a sweeper re-enqueue racing a slow but
|
|
109
|
+
# alive worker). Capping it would fail a legitimately-waiting duplicate.
|
|
110
|
+
capped = !(error.is_a?(RubyReactor::OrderedLock::WaitError) ||
|
|
111
|
+
error.is_a?(RubyReactor::Lock::ContextLockContention))
|
|
112
|
+
|
|
113
|
+
if capped && max != :infinity && snooze_count >= max
|
|
114
|
+
escalate_snooze(context, snooze_count, error)
|
|
115
|
+
return
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
delay = compute_snooze_delay(config, error)
|
|
119
|
+
# Re-enqueue by id: the context is already persisted in storage, so the
|
|
120
|
+
# rescheduled job rehydrates fresh state (no stale blob).
|
|
121
|
+
self.class.perform_in(delay, context_id, reactor_class_name, snooze_count + 1)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Use the error's `retry_after_seconds` hint when available
|
|
125
|
+
# (RateLimit::ExceededError carries the time until the bucket rolls);
|
|
126
|
+
# otherwise fall back to the configured base + jitter for lock/semaphore
|
|
127
|
+
# contention which has no precise hint.
|
|
128
|
+
#
|
|
129
|
+
# OrderedLock::WaitError is deliberately excluded from the hint path: its
|
|
130
|
+
# `retry_after_seconds` is the poison-pill window (the upper bound before
|
|
131
|
+
# a *dead* blocker is force-advanced), NOT how long the *live* blocker
|
|
132
|
+
# will take — which is usually milliseconds. Snoozing for the full window
|
|
133
|
+
# would make every out-of-order nonce sleep up to poison_pill_timeout even
|
|
134
|
+
# though its blocker finishes immediately, collapsing throughput. Re-poll
|
|
135
|
+
# at the base delay instead; poison auto-advance still clears a genuinely
|
|
136
|
+
# dead blocker on a later gate.
|
|
137
|
+
def compute_snooze_delay(config, error)
|
|
138
|
+
jitter = config.lock_snooze_jitter.to_f
|
|
139
|
+
jitter_amount = jitter.positive? ? rand(0.0..jitter) : 0.0
|
|
140
|
+
|
|
141
|
+
if hinted_retry?(error)
|
|
142
|
+
[error.retry_after_seconds.to_f, 0.1].max + jitter_amount
|
|
143
|
+
else
|
|
144
|
+
config.lock_snooze_base_delay.to_f + jitter_amount
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def hinted_retry?(error)
|
|
149
|
+
return false if error.is_a?(RubyReactor::OrderedLock::WaitError)
|
|
150
|
+
|
|
151
|
+
error.respond_to?(:retry_after_seconds) && error.retry_after_seconds
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def escalate_snooze(context, snooze_count, error)
|
|
155
|
+
RubyReactor.configuration.logger.warn(
|
|
156
|
+
"RubyReactor snooze limit reached after #{snooze_count} attempts " \
|
|
157
|
+
"for context #{context.context_id}: #{error.message}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
context.status = :failed
|
|
161
|
+
context.failure_reason = {
|
|
162
|
+
message: error.message,
|
|
163
|
+
exception_class: error.class.name,
|
|
164
|
+
snooze_attempts: snooze_count
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
serialized = ContextSerializer.serialize(context)
|
|
168
|
+
reactor_class_name = RubyReactor.reactor_storage_name(context.reactor_class)
|
|
169
|
+
RubyReactor.configuration.storage_adapter.store_context(
|
|
170
|
+
context.context_id,
|
|
171
|
+
serialized,
|
|
172
|
+
reactor_class_name
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Escalation is a terminal Failure that never reaches the Executor's
|
|
176
|
+
# ensure path, so advance the ordered-lock cursor here. Without this
|
|
177
|
+
# the nonce stays stranded in assigned_at (successors stall for the
|
|
178
|
+
# full poison_pill_timeout) and, worse, the strict-mode chain marker
|
|
179
|
+
# is never recorded — successors would RUN instead of being skipped.
|
|
180
|
+
info = Executor::OrderedLockSupport.info_from(context)
|
|
181
|
+
Executor::OrderedLockSupport.advance_with_retry(info, failed: true) if info
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def log_infrastructure_failure(msg, exception)
|
|
185
|
+
RubyReactor.configuration.logger.error("RubyReactor infrastructure failure: #{exception.message}")
|
|
186
|
+
RubyReactor.configuration.logger.error("Job details: #{msg.inspect}")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# The id-only payload already carries context_id and reactor_class_name, so
|
|
190
|
+
# there is no blob to parse for metadata — just mark the stored context
|
|
191
|
+
# failed (best-effort) so the job stops retrying a permanently-broken blob.
|
|
192
|
+
def handle_deserialization_failure(context_id, reactor_class_name, error)
|
|
193
|
+
RubyReactor.configuration.logger.error(
|
|
194
|
+
"RubyReactor deserialization failure for context " \
|
|
195
|
+
"#{context_id || "unknown"}: #{error.class.name}: #{error.message}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return unless context_id && reactor_class_name
|
|
199
|
+
|
|
200
|
+
payload = build_failed_context_payload(context_id, reactor_class_name, error)
|
|
201
|
+
RubyReactor.configuration.storage_adapter.store_context(
|
|
202
|
+
context_id,
|
|
203
|
+
payload,
|
|
204
|
+
reactor_class_name
|
|
205
|
+
)
|
|
206
|
+
rescue StandardError => e
|
|
207
|
+
# Don't let a persistence failure mask the original deserialization error.
|
|
208
|
+
RubyReactor.configuration.logger.error(
|
|
209
|
+
"RubyReactor failed to persist deserialization failure: #{e.class.name}: #{e.message}"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def build_failed_context_payload(context_id, reactor_class_name, error)
|
|
214
|
+
JSON.generate(
|
|
215
|
+
"schema_version" => ContextSerializer::SCHEMA_VERSION,
|
|
216
|
+
"context_id" => context_id,
|
|
217
|
+
"reactor_class" => reactor_class_name,
|
|
218
|
+
"status" => "failed",
|
|
219
|
+
"failure_reason" => {
|
|
220
|
+
"message" => error.message,
|
|
221
|
+
"exception_class" => error.class.name
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
data/lib/ruby_reactor.rb
CHANGED
|
@@ -26,6 +26,13 @@ rescue LoadError
|
|
|
26
26
|
# sidekiq is optional, async features won't be available
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Load active_job if available (for the ActiveJob async adapter)
|
|
30
|
+
begin
|
|
31
|
+
require "active_job"
|
|
32
|
+
rescue LoadError
|
|
33
|
+
# active_job is optional, only needed when using the ActiveJob adapter
|
|
34
|
+
end
|
|
35
|
+
|
|
29
36
|
loader = Zeitwerk::Loader.for_gem
|
|
30
37
|
loader.inflector.inflect("api" => "API", "rspec" => "RSpec")
|
|
31
38
|
loader.setup
|
|
@@ -356,7 +363,39 @@ module RubyReactor
|
|
|
356
363
|
def self.start_sweeper!
|
|
357
364
|
return unless configuration.sweeper_enabled
|
|
358
365
|
|
|
359
|
-
|
|
366
|
+
sweeper_job_class.schedule_next
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# The sweeper job class living alongside the configured `async_router`
|
|
370
|
+
# (e.g. `Adapters::Sidekiq::Router` -> `Adapters::Sidekiq::SweeperWorker`),
|
|
371
|
+
# so the chain is kicked through whichever backend is configured instead of
|
|
372
|
+
# a hardcoded Sidekiq class. Known built-in routers are mapped explicitly;
|
|
373
|
+
# a custom `configuration.async_router` falls back to a sibling-namespace
|
|
374
|
+
# lookup, with a clear error (instead of a bare NameError) when that
|
|
375
|
+
# convention doesn't hold.
|
|
376
|
+
def self.sweeper_job_class
|
|
377
|
+
router = configuration.async_router
|
|
378
|
+
case router.name
|
|
379
|
+
when "RubyReactor::Adapters::Sidekiq::Router" then Adapters::Sidekiq::SweeperWorker
|
|
380
|
+
when "RubyReactor::Adapters::ActiveJob::Router" then Adapters::ActiveJob::SweeperWorker
|
|
381
|
+
else
|
|
382
|
+
inferred_sweeper_job_class(router)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def self.inferred_sweeper_job_class(router)
|
|
387
|
+
namespace_name = router.name.to_s.rpartition("::").first
|
|
388
|
+
raise sweeper_job_class_error(router) if namespace_name.empty?
|
|
389
|
+
|
|
390
|
+
Object.const_get(namespace_name).const_get(:SweeperWorker)
|
|
391
|
+
rescue NameError
|
|
392
|
+
raise sweeper_job_class_error(router)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def self.sweeper_job_class_error(router)
|
|
396
|
+
"RubyReactor: cannot infer a sweeper job class for custom async_router " \
|
|
397
|
+
"#{router.inspect}. Define a `SweeperWorker` class alongside it " \
|
|
398
|
+
"(same namespace), or override `RubyReactor.sweeper_job_class`."
|
|
360
399
|
end
|
|
361
400
|
|
|
362
401
|
# Run both recovery sweepers exactly once and return their counts. The
|
data/specs/active_job.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Pluggable Background Adapters (Sidekiq → ActiveJob)
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Today RubyReactor is hardwired to Sidekiq for everything async: enqueuing,
|
|
6
|
+
resuming, map-element fan-out/collection, and the recovery sweeper. We want
|
|
7
|
+
to support other background processors — starting with ActiveJob — without
|
|
8
|
+
duplicating the reactor-resume/snooze/escalate logic per adapter.
|
|
9
|
+
|
|
10
|
+
## What's already abstracted (good news)
|
|
11
|
+
|
|
12
|
+
The **enqueue side** is already behind a seam:
|
|
13
|
+
|
|
14
|
+
- `RubyReactor.configuration.async_router` (default `RubyReactor::Adapters::Sidekiq::Router`,
|
|
15
|
+
[configuration.rb:126-128](../lib/ruby_reactor/configuration.rb#L126-L128)) is the
|
|
16
|
+
only thing the core engine calls to go async. Call sites:
|
|
17
|
+
[reactor.rb:117](../lib/ruby_reactor/reactor.rb#L117),
|
|
18
|
+
[reactor.rb:319](../lib/ruby_reactor/reactor.rb#L319),
|
|
19
|
+
[step/map_step.rb:274](../lib/ruby_reactor/step/map_step.rb#L274),
|
|
20
|
+
[step/map_step.rb:285](../lib/ruby_reactor/step/map_step.rb#L285),
|
|
21
|
+
[map/dispatcher.rb:177](../lib/ruby_reactor/map/dispatcher.rb#L177),
|
|
22
|
+
[map/element_executor.rb:155](../lib/ruby_reactor/map/element_executor.rb#L155),
|
|
23
|
+
[map/element_executor.rb:176](../lib/ruby_reactor/map/element_executor.rb#L176),
|
|
24
|
+
[executor/retry_manager.rb:59,80](../lib/ruby_reactor/executor/retry_manager.rb#L59),
|
|
25
|
+
[executor/step_executor.rb:219](../lib/ruby_reactor/executor/step_executor.rb#L219),
|
|
26
|
+
[sweeper.rb:48](../lib/ruby_reactor/sweeper.rb#L48),
|
|
27
|
+
[map/sweeper.rb:99](../lib/ruby_reactor/map/sweeper.rb#L99).
|
|
28
|
+
- The router contract is 5 class methods on `SidekiqAdapter`
|
|
29
|
+
([sidekiq_adapter.rb](../lib/ruby_reactor/sidekiq_adapter.rb)):
|
|
30
|
+
`perform_async`, `perform_in`, `perform_map_element_async`,
|
|
31
|
+
`perform_map_element_in`, `perform_map_collection_async`. All return
|
|
32
|
+
`RubyReactor::AsyncResult`.
|
|
33
|
+
- An adapter for any other queueing backend just needs to implement that
|
|
34
|
+
same 5-method contract and assign it to `config.async_router`. **This part
|
|
35
|
+
needs no rework.**
|
|
36
|
+
|
|
37
|
+
## What's NOT abstracted (the actual gap)
|
|
38
|
+
|
|
39
|
+
The **worker/job side** — the classes the queue invokes — bakes Sidekiq in
|
|
40
|
+
directly. **Decision: these move into `RubyReactor::Adapters::Sidekiq::*`**
|
|
41
|
+
(renamed from `RubyReactor::SidekiqWorkers::*`), with a sibling
|
|
42
|
+
`RubyReactor::Adapters::ActiveJob::*` for the new adapter — see
|
|
43
|
+
[Namespace](#namespace) below.
|
|
44
|
+
|
|
45
|
+
| Class (today) | File | Sidekiq coupling |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `SidekiqWorkers::Worker` | [worker.rb](../lib/ruby_reactor/sidekiq_workers/worker.rb) | `include ::Sidekiq::Worker`, `sidekiq_options`, `sidekiq_retries_exhausted`, and internally calls `self.class.perform_in(...)` to reschedule snoozes (lines 116, 91-117) |
|
|
48
|
+
| `SidekiqWorkers::MapElementWorker` | [map_element_worker.rb](../lib/ruby_reactor/sidekiq_workers/map_element_worker.rb) | `include ::Sidekiq::Worker`; body is a 1-line delegate to `Map::ElementExecutor.perform` |
|
|
49
|
+
| `SidekiqWorkers::MapCollectorWorker` | [map_collector_worker.rb](../lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb) | same — 1-line delegate to `Map::Collector.perform` |
|
|
50
|
+
| `SidekiqWorkers::SweeperWorker` | [sweeper_worker.rb](../lib/ruby_reactor/sidekiq_workers/sweeper_worker.rb) | `include ::Sidekiq::Worker`, `sidekiq_options retry: false`, self-reschedules via `perform_in` / class-level `perform_in` in `schedule_next` |
|
|
51
|
+
|
|
52
|
+
Plus test-side coupling:
|
|
53
|
+
|
|
54
|
+
- [rspec/sidekiq_helpers.rb](../lib/ruby_reactor/rspec/sidekiq_helpers.rb) hardcodes
|
|
55
|
+
the 3 Sidekiq worker classes and `Sidekiq::Testing` fake-mode draining.
|
|
56
|
+
- [rspec/test_subject.rb:194-203,433](../lib/ruby_reactor/rspec/test_subject.rb#L194-L203)
|
|
57
|
+
gates job-processing on `defined?(Sidekiq::Testing)` and forces
|
|
58
|
+
`async_router` back to `SidekiqAdapter`.
|
|
59
|
+
- [ruby_reactor.rb:22-27](../lib/ruby_reactor.rb#L22-L27) optionally requires
|
|
60
|
+
`sidekiq`; [ruby_reactor.rb:359](../lib/ruby_reactor.rb#L359) calls
|
|
61
|
+
`SidekiqWorkers::SweeperWorker.schedule_next` directly from
|
|
62
|
+
`RubyReactor.start_sweeper!`.
|
|
63
|
+
|
|
64
|
+
The real logic worth extracting lives almost entirely in
|
|
65
|
+
`SidekiqWorkers::Worker#perform` (~65 lines): rehydrate context from
|
|
66
|
+
storage, deserialize, resolve `reactor_class`, mark
|
|
67
|
+
`inline_async_execution`, run `Executor#resume_execution`, and on
|
|
68
|
+
lock/semaphore/rate-limit/ordered-lock contention either snooze (re-enqueue
|
|
69
|
+
with a computed delay) or escalate to `failed`. None of that is
|
|
70
|
+
Sidekiq-specific — it only *touches* Sidekiq via `self.class.perform_in` to
|
|
71
|
+
reschedule.
|
|
72
|
+
|
|
73
|
+
## Proposal
|
|
74
|
+
|
|
75
|
+
### 1. Normalize the enqueue API at the job-class boundary, not in the shared logic
|
|
76
|
+
|
|
77
|
+
`Sidekiq::Worker` gives every job class `.perform_async` / `.perform_in` for
|
|
78
|
+
free. ActiveJob doesn't — it has `.perform_later` and
|
|
79
|
+
`.set(wait: delay).perform_later`. Rather than teach the shared logic two
|
|
80
|
+
different reschedule calls, give every framework-specific job class the
|
|
81
|
+
same two class methods, so the shared mixin can keep calling
|
|
82
|
+
`self.class.perform_in(...)` unchanged:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
module RubyReactor
|
|
86
|
+
module Adapters
|
|
87
|
+
module ActiveJob
|
|
88
|
+
module Compat
|
|
89
|
+
def perform_async(*args) = perform_later(*args)
|
|
90
|
+
def perform_in(delay, *args) = set(wait: delay).perform_later(*args)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Extract `RubyReactor::Worker` — the framework-agnostic mixin
|
|
98
|
+
|
|
99
|
+
Move the body of `Adapters::Sidekiq::Worker#perform` (and its private snooze/
|
|
100
|
+
escalate/deserialization-failure helpers) into a plain module with no
|
|
101
|
+
`Sidekiq` reference:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
module RubyReactor
|
|
105
|
+
module Worker
|
|
106
|
+
def perform(context_id, reactor_class_name = nil, snooze_count = 0)
|
|
107
|
+
# ...exact same logic as today's SidekiqWorkers::Worker#perform...
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
# handle_snooze, compute_snooze_delay, hinted_retry?, escalate_snooze,
|
|
112
|
+
# log_infrastructure_failure, handle_deserialization_failure,
|
|
113
|
+
# build_failed_context_payload — unchanged, moved verbatim.
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`Adapters::Sidekiq::Worker` then becomes:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
module RubyReactor
|
|
122
|
+
module Adapters
|
|
123
|
+
module Sidekiq
|
|
124
|
+
class Worker
|
|
125
|
+
include ::Sidekiq::Worker
|
|
126
|
+
include RubyReactor::Worker
|
|
127
|
+
|
|
128
|
+
sidekiq_options retry: RubyReactor.configuration.sidekiq_retry_count,
|
|
129
|
+
dead: false, queue: RubyReactor.configuration.sidekiq_queue
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
And a new `Adapters::ActiveJob::Worker`:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
module RubyReactor
|
|
140
|
+
module Adapters
|
|
141
|
+
module ActiveJob
|
|
142
|
+
class Worker < ::ActiveJob::Base
|
|
143
|
+
extend Compat
|
|
144
|
+
include RubyReactor::Worker
|
|
145
|
+
|
|
146
|
+
queue_as { RubyReactor.configuration.sidekiq_queue } # or a renamed generic config
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Same pattern applies to `MapElementWorker` / `MapCollectorWorker` — they're
|
|
154
|
+
already a 1-line delegate, so genericizing is just swapping the include; no
|
|
155
|
+
logic to extract.
|
|
156
|
+
|
|
157
|
+
### 3. Sweeper
|
|
158
|
+
|
|
159
|
+
`SweeperWorker`'s window-claim-lock + self-reschedule logic
|
|
160
|
+
([sweeper_worker.rb:30-70](../lib/ruby_reactor/sidekiq_workers/sweeper_worker.rb#L30-L70))
|
|
161
|
+
is also framework-agnostic except for the `perform_in` call in
|
|
162
|
+
`schedule_next`. Extract the same way into `RubyReactor::SweeperJob`,
|
|
163
|
+
included into both `Adapters::Sidekiq::SweeperWorker` (`sidekiq_options retry: false`)
|
|
164
|
+
and an `Adapters::ActiveJob::SweeperWorker`. `RubyReactor.start_sweeper!`
|
|
165
|
+
([ruby_reactor.rb:356-360](../lib/ruby_reactor.rb#L356-L360)) needs to call
|
|
166
|
+
through whichever sweeper job class matches the configured adapter instead
|
|
167
|
+
of hardcoding `Adapters::Sidekiq::SweeperWorker`.
|
|
168
|
+
|
|
169
|
+
### 4. New `RubyReactor::Adapters::ActiveJob::Router`
|
|
170
|
+
|
|
171
|
+
Mirrors today's `RubyReactor::Adapters::Sidekiq::Router` (renamed from
|
|
172
|
+
`SidekiqAdapter`) exactly — same 5 methods, just pointing at the
|
|
173
|
+
`Adapters::ActiveJob::*` job classes instead of `Adapters::Sidekiq::*`. No
|
|
174
|
+
changes needed to any core call site; swap is purely
|
|
175
|
+
`config.async_router = RubyReactor::Adapters::ActiveJob::Router`.
|
|
176
|
+
|
|
177
|
+
### 5. Optional config sugar
|
|
178
|
+
|
|
179
|
+
`configuration.rb` already does this pattern for storage
|
|
180
|
+
([configuration.rb:114-121](../lib/ruby_reactor/configuration.rb#L114-L121)):
|
|
181
|
+
a single `storage.adapter` symbol resolves to a concrete adapter instance.
|
|
182
|
+
Could mirror it — `config.queue_adapter = :sidekiq | :active_job` resolving
|
|
183
|
+
both `async_router` and the sweeper job class — but this is sugar, not
|
|
184
|
+
required for the feature to work. Confirm whether you want it.
|
|
185
|
+
|
|
186
|
+
### 6. Test helpers
|
|
187
|
+
|
|
188
|
+
`rspec/sidekiq_helpers.rb` and the `Sidekiq::Testing` branch in
|
|
189
|
+
`rspec/test_subject.rb` only fire for Sidekiq today. For ActiveJob, Rails
|
|
190
|
+
already ships `ActiveJob::TestHelper` (`perform_enqueued_jobs`,
|
|
191
|
+
`have_enqueued_job`) which covers most of this generically. We'd still want
|
|
192
|
+
something equivalent to `drain_async_jobs` (loops until self-rescheduling
|
|
193
|
+
jobs — e.g. ordered-lock snoozes — stop producing new ones), so
|
|
194
|
+
`test_subject.rb`'s job-processing gate needs to branch on which
|
|
195
|
+
testing framework is active (or be driven by `configuration.async_router`)
|
|
196
|
+
rather than hardcoding `defined?(Sidekiq::Testing)`.
|
|
197
|
+
|
|
198
|
+
### 7. Loading
|
|
199
|
+
|
|
200
|
+
`ruby_reactor.rb:22-27` optionally `require "sidekiq"`. Add the same
|
|
201
|
+
optional-require pattern for `active_job`, so neither dependency is forced
|
|
202
|
+
on users who only need one.
|
|
203
|
+
|
|
204
|
+
## Namespace
|
|
205
|
+
|
|
206
|
+
**Decided: `RubyReactor::Adapters::Sidekiq::*` / `RubyReactor::Adapters::ActiveJob::*`.**
|
|
207
|
+
|
|
208
|
+
Renames/moves required (mechanical, but touches every reference):
|
|
209
|
+
|
|
210
|
+
| Today | Becomes |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `RubyReactor::SidekiqAdapter` | `RubyReactor::Adapters::Sidekiq::Router` |
|
|
213
|
+
| `RubyReactor::SidekiqWorkers::Worker` | `RubyReactor::Adapters::Sidekiq::Worker` |
|
|
214
|
+
| `RubyReactor::SidekiqWorkers::MapElementWorker` | `RubyReactor::Adapters::Sidekiq::MapElementWorker` |
|
|
215
|
+
| `RubyReactor::SidekiqWorkers::MapCollectorWorker` | `RubyReactor::Adapters::Sidekiq::MapCollectorWorker` |
|
|
216
|
+
| `RubyReactor::SidekiqWorkers::SweeperWorker` | `RubyReactor::Adapters::Sidekiq::SweeperWorker` |
|
|
217
|
+
| (new) | `RubyReactor::Adapters::ActiveJob::Router` |
|
|
218
|
+
| (new) | `RubyReactor::Adapters::ActiveJob::{Worker,MapElementWorker,MapCollectorWorker,SweeperWorker}` |
|
|
219
|
+
|
|
220
|
+
Files move from `lib/ruby_reactor/sidekiq_workers/*.rb` +
|
|
221
|
+
`lib/ruby_reactor/sidekiq_adapter.rb` to
|
|
222
|
+
`lib/ruby_reactor/adapters/sidekiq/*.rb` (Zeitwerk-driven, so the directory
|
|
223
|
+
move IS the rename — no manual `module` boilerplate beyond nesting). New
|
|
224
|
+
ActiveJob side lives in `lib/ruby_reactor/adapters/active_job/*.rb`.
|
|
225
|
+
|
|
226
|
+
References that need updating for the rename:
|
|
227
|
+
- [configuration.rb:107](../lib/ruby_reactor/configuration.rb#L107) — `@async_router ||= RubyReactor::SidekiqAdapter`
|
|
228
|
+
- [ruby_reactor.rb:359](../lib/ruby_reactor.rb#L359) — `SidekiqWorkers::SweeperWorker.schedule_next`
|
|
229
|
+
- [rspec/sidekiq_helpers.rb](../lib/ruby_reactor/rspec/sidekiq_helpers.rb) — `worker_classes` list
|
|
230
|
+
- [rspec/test_subject.rb:196](../lib/ruby_reactor/rspec/test_subject.rb#L196) — stub target
|
|
231
|
+
- [spec/ruby_reactor/sidekiq_workers/worker_spec.rb](../spec/ruby_reactor/sidekiq_workers/worker_spec.rb),
|
|
232
|
+
[spec/ruby_reactor/sidekiq_workers/sweeper_worker_spec.rb](../spec/ruby_reactor/sidekiq_workers/sweeper_worker_spec.rb) —
|
|
233
|
+
`described_class` references, move to `spec/ruby_reactor/adapters/sidekiq/`
|
|
234
|
+
|
|
235
|
+
## Retry config — decided: generic
|
|
236
|
+
|
|
237
|
+
`sidekiq_retry_count` / `sidekiq_queue` rename to `config.job_retry_count` /
|
|
238
|
+
`config.queue_name`, used by both adapters:
|
|
239
|
+
|
|
240
|
+
- `Adapters::Sidekiq::Worker` → `sidekiq_options retry: config.job_retry_count, dead: false, queue: config.queue_name`
|
|
241
|
+
- `Adapters::ActiveJob::Worker` → maps to `retry_on StandardError, attempts: config.job_retry_count` (infra
|
|
242
|
+
failures only — reactor-specific errors are already caught by the shared
|
|
243
|
+
snooze/escalate logic in `RubyReactor::Worker` before they'd ever reach
|
|
244
|
+
the framework's retry layer) and `queue_as { config.queue_name }`.
|
|
245
|
+
- `sidekiq_retries_exhausted` (currently an empty hook) → ActiveJob
|
|
246
|
+
equivalent is `retry_on ... do |job, error| ... end` / `discard_on`.
|
|
247
|
+
|
|
248
|
+
`configuration.rb` changes: add `job_retry_count`/`queue_name` as the
|
|
249
|
+
canonical attrs; keep `sidekiq_retry_count`/`sidekiq_queue` as deprecated
|
|
250
|
+
aliases delegating to the new names so existing configs don't break.
|
|
251
|
+
|
|
252
|
+
None of this requires touching the enqueue-side call sites in
|
|
253
|
+
`reactor.rb`, `executor/*`, `map/*` — that seam already works (it just calls
|
|
254
|
+
through `config.async_router`, whose value changes, not its call sites).
|
|
255
|
+
The work is isolated to: `lib/ruby_reactor/worker.rb` (new),
|
|
256
|
+
`lib/ruby_reactor/sweeper_job.rb` (new), the `sidekiq_workers/` →
|
|
257
|
+
`adapters/sidekiq/` move + rename, `adapters/active_job/*.rb` (new, 5
|
|
258
|
+
classes: `Router` + 4 job classes), and the test-helper /
|
|
259
|
+
`start_sweeper!` branching described above.
|
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.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
@@ -88,16 +88,64 @@ executables: []
|
|
|
88
88
|
extensions: []
|
|
89
89
|
extra_rdoc_files: []
|
|
90
90
|
files:
|
|
91
|
+
- ".claude/skills/speckit-agent-context-update/SKILL.md"
|
|
92
|
+
- ".claude/skills/speckit-analyze/SKILL.md"
|
|
93
|
+
- ".claude/skills/speckit-checklist/SKILL.md"
|
|
94
|
+
- ".claude/skills/speckit-clarify/SKILL.md"
|
|
95
|
+
- ".claude/skills/speckit-constitution/SKILL.md"
|
|
96
|
+
- ".claude/skills/speckit-converge/SKILL.md"
|
|
97
|
+
- ".claude/skills/speckit-implement/SKILL.md"
|
|
98
|
+
- ".claude/skills/speckit-plan/SKILL.md"
|
|
99
|
+
- ".claude/skills/speckit-specify/SKILL.md"
|
|
100
|
+
- ".claude/skills/speckit-tasks/SKILL.md"
|
|
101
|
+
- ".claude/skills/speckit-taskstoissues/SKILL.md"
|
|
91
102
|
- ".release-please-config.json"
|
|
92
103
|
- ".release-please-manifest.json"
|
|
93
104
|
- ".rspec"
|
|
94
105
|
- ".rubocop.yml"
|
|
106
|
+
- ".specify/extensions.yml"
|
|
107
|
+
- ".specify/extensions/.registry"
|
|
108
|
+
- ".specify/extensions/agent-context/README.md"
|
|
109
|
+
- ".specify/extensions/agent-context/agent-context-config.yml"
|
|
110
|
+
- ".specify/extensions/agent-context/commands/speckit.agent-context.update.md"
|
|
111
|
+
- ".specify/extensions/agent-context/extension.yml"
|
|
112
|
+
- ".specify/extensions/agent-context/scripts/bash/update-agent-context.sh"
|
|
113
|
+
- ".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1"
|
|
114
|
+
- ".specify/feature.json"
|
|
115
|
+
- ".specify/init-options.json"
|
|
116
|
+
- ".specify/integration.json"
|
|
117
|
+
- ".specify/integrations/claude.manifest.json"
|
|
118
|
+
- ".specify/integrations/speckit.manifest.json"
|
|
119
|
+
- ".specify/memory/constitution.md"
|
|
120
|
+
- ".specify/scripts/bash/check-prerequisites.sh"
|
|
121
|
+
- ".specify/scripts/bash/common.sh"
|
|
122
|
+
- ".specify/scripts/bash/create-new-feature.sh"
|
|
123
|
+
- ".specify/scripts/bash/setup-plan.sh"
|
|
124
|
+
- ".specify/scripts/bash/setup-tasks.sh"
|
|
125
|
+
- ".specify/templates/checklist-template.md"
|
|
126
|
+
- ".specify/templates/constitution-template.md"
|
|
127
|
+
- ".specify/templates/plan-template.md"
|
|
128
|
+
- ".specify/templates/spec-template.md"
|
|
129
|
+
- ".specify/templates/tasks-template.md"
|
|
130
|
+
- ".specify/workflows/speckit/workflow.yml"
|
|
131
|
+
- ".specify/workflows/workflow-registry.json"
|
|
95
132
|
- ".tool-versions"
|
|
96
133
|
- CHANGELOG.md
|
|
97
134
|
- CODE_OF_CONDUCT.md
|
|
98
135
|
- README.md
|
|
99
136
|
- Rakefile
|
|
100
137
|
- lib/ruby_reactor.rb
|
|
138
|
+
- lib/ruby_reactor/adapters/active_job/compat.rb
|
|
139
|
+
- lib/ruby_reactor/adapters/active_job/map_collector_worker.rb
|
|
140
|
+
- lib/ruby_reactor/adapters/active_job/map_element_worker.rb
|
|
141
|
+
- lib/ruby_reactor/adapters/active_job/router.rb
|
|
142
|
+
- lib/ruby_reactor/adapters/active_job/sweeper_worker.rb
|
|
143
|
+
- lib/ruby_reactor/adapters/active_job/worker.rb
|
|
144
|
+
- lib/ruby_reactor/adapters/sidekiq/map_collector_worker.rb
|
|
145
|
+
- lib/ruby_reactor/adapters/sidekiq/map_element_worker.rb
|
|
146
|
+
- lib/ruby_reactor/adapters/sidekiq/router.rb
|
|
147
|
+
- lib/ruby_reactor/adapters/sidekiq/sweeper_worker.rb
|
|
148
|
+
- lib/ruby_reactor/adapters/sidekiq/worker.rb
|
|
101
149
|
- lib/ruby_reactor/configuration.rb
|
|
102
150
|
- lib/ruby_reactor/context.rb
|
|
103
151
|
- lib/ruby_reactor/context_serializer.rb
|
|
@@ -150,6 +198,8 @@ files:
|
|
|
150
198
|
- lib/ruby_reactor/retry_context.rb
|
|
151
199
|
- lib/ruby_reactor/retry_queued_result.rb
|
|
152
200
|
- lib/ruby_reactor/rspec.rb
|
|
201
|
+
- lib/ruby_reactor/rspec/active_job_helpers.rb
|
|
202
|
+
- lib/ruby_reactor/rspec/async_test_helpers.rb
|
|
153
203
|
- lib/ruby_reactor/rspec/helpers.rb
|
|
154
204
|
- lib/ruby_reactor/rspec/matchers.rb
|
|
155
205
|
- lib/ruby_reactor/rspec/sidekiq_helpers.rb
|
|
@@ -157,11 +207,6 @@ files:
|
|
|
157
207
|
- lib/ruby_reactor/rspec/storage_reset.rb
|
|
158
208
|
- lib/ruby_reactor/rspec/test_subject.rb
|
|
159
209
|
- lib/ruby_reactor/semaphore.rb
|
|
160
|
-
- lib/ruby_reactor/sidekiq_adapter.rb
|
|
161
|
-
- lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb
|
|
162
|
-
- lib/ruby_reactor/sidekiq_workers/map_element_worker.rb
|
|
163
|
-
- lib/ruby_reactor/sidekiq_workers/sweeper_worker.rb
|
|
164
|
-
- lib/ruby_reactor/sidekiq_workers/worker.rb
|
|
165
210
|
- lib/ruby_reactor/step.rb
|
|
166
211
|
- lib/ruby_reactor/step/compose_step.rb
|
|
167
212
|
- lib/ruby_reactor/step/map_step.rb
|
|
@@ -171,6 +216,7 @@ files:
|
|
|
171
216
|
- lib/ruby_reactor/storage/redis_locking.rb
|
|
172
217
|
- lib/ruby_reactor/storage/redis_ordered_locking.rb
|
|
173
218
|
- lib/ruby_reactor/sweeper.rb
|
|
219
|
+
- lib/ruby_reactor/sweeper_job.rb
|
|
174
220
|
- lib/ruby_reactor/template/base.rb
|
|
175
221
|
- lib/ruby_reactor/template/dynamic_source.rb
|
|
176
222
|
- lib/ruby_reactor/template/element.rb
|
|
@@ -191,9 +237,11 @@ files:
|
|
|
191
237
|
- lib/ruby_reactor/web/public/assets/index-D7IBZvos.js
|
|
192
238
|
- lib/ruby_reactor/web/public/index.html
|
|
193
239
|
- lib/ruby_reactor/web/public/vite.svg
|
|
240
|
+
- lib/ruby_reactor/worker.rb
|
|
194
241
|
- llms-full.txt
|
|
195
242
|
- llms.txt
|
|
196
243
|
- sig/ruby_reactor.rbs
|
|
244
|
+
- specs/active_job.md
|
|
197
245
|
- teley/Dockerfile
|
|
198
246
|
homepage: https://github.com/arturictus/ruby_reactor
|
|
199
247
|
licenses: []
|