ruby_reactor 0.3.2 → 0.4.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-config.json +15 -0
  3. data/.release-please-manifest.json +3 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/README.md +80 -4
  7. data/lib/ruby_reactor/context_serializer.rb +10 -1
  8. data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
  9. data/lib/ruby_reactor/rate_limit.rb +2 -2
  10. data/lib/ruby_reactor/sidekiq_workers/worker.rb +58 -1
  11. data/lib/ruby_reactor/version.rb +1 -1
  12. metadata +7 -52
  13. data/documentation/DAG.md +0 -457
  14. data/documentation/README.md +0 -135
  15. data/documentation/async_reactors.md +0 -381
  16. data/documentation/composition.md +0 -199
  17. data/documentation/core_concepts.md +0 -676
  18. data/documentation/data_pipelines.md +0 -230
  19. data/documentation/examples/inventory_management.md +0 -748
  20. data/documentation/examples/order_processing.md +0 -380
  21. data/documentation/examples/payment_processing.md +0 -565
  22. data/documentation/getting_started.md +0 -242
  23. data/documentation/images/failed_order_processing.png +0 -0
  24. data/documentation/images/payment_workflow.png +0 -0
  25. data/documentation/interrupts.md +0 -163
  26. data/documentation/locks_and_semaphores.md +0 -459
  27. data/documentation/retry_configuration.md +0 -362
  28. data/documentation/testing.md +0 -994
  29. data/gui/.gitignore +0 -24
  30. data/gui/README.md +0 -73
  31. data/gui/eslint.config.js +0 -23
  32. data/gui/index.html +0 -13
  33. data/gui/package-lock.json +0 -5925
  34. data/gui/package.json +0 -46
  35. data/gui/postcss.config.js +0 -6
  36. data/gui/public/vite.svg +0 -1
  37. data/gui/src/App.css +0 -42
  38. data/gui/src/App.tsx +0 -51
  39. data/gui/src/assets/react.svg +0 -1
  40. data/gui/src/components/DagVisualizer.tsx +0 -424
  41. data/gui/src/components/Dashboard.tsx +0 -163
  42. data/gui/src/components/ErrorBoundary.tsx +0 -47
  43. data/gui/src/components/ReactorDetail.tsx +0 -135
  44. data/gui/src/components/StepInspector.tsx +0 -492
  45. data/gui/src/components/__tests__/DagVisualizer.test.tsx +0 -140
  46. data/gui/src/components/__tests__/ReactorDetail.test.tsx +0 -111
  47. data/gui/src/components/__tests__/StepInspector.test.tsx +0 -408
  48. data/gui/src/globals.d.ts +0 -7
  49. data/gui/src/index.css +0 -14
  50. data/gui/src/lib/utils.ts +0 -13
  51. data/gui/src/main.tsx +0 -14
  52. data/gui/src/test/setup.ts +0 -11
  53. data/gui/tailwind.config.js +0 -11
  54. data/gui/tsconfig.app.json +0 -28
  55. data/gui/tsconfig.json +0 -7
  56. data/gui/tsconfig.node.json +0 -26
  57. data/gui/vite.config.ts +0 -8
  58. data/gui/vitest.config.ts +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de5c91a5450cb522097965aa4348760438b60babd05da33f9f7ec1a07fe9bd07
4
- data.tar.gz: d565e81c6ba6e057b56912311bf16dee95ce0c441eb76f2b893658b751568b2c
3
+ metadata.gz: 7769f6488ce48db7f07cd9a7bb9774f697cadc6433293ca926a9e1c27b28ce3e
4
+ data.tar.gz: 9dd0d5edd43c8b7629474ca2c12dd8f1c755d43f7fab3bc33aaefb86a183ba7c
5
5
  SHA512:
6
- metadata.gz: dde49e045303aeb4bcf414ea30b7d85d8fb8a782f873dd9ebb4a9d7d0524e74de37ba4e53ddc11ac6dea360bf1226f946d0c50e74e685da2ac70394065f436d6
7
- data.tar.gz: 7b9b0b5bf75866876bdc1811dfc1118272963337431148f480d6570e19bfc2e9d93a6fdfd91a3629734501a590e23be579b98ff2e2f4f125831410351e0e6865
6
+ metadata.gz: afbabcc37d4dbba1a6fcb83fb15b88160979802b7cd5d69fd0aed47538080c4abd3196b40a7f7464047f94c2aafa6d5317e825446179e9129a960c0e5d119742
7
+ data.tar.gz: caef3d4d35e37a0c9bc35b273ad20f8a0dc31810e35cf2ea1b50df57d347762c85d2a42aa6b3125401e324725d0e8f17f9d1acfecb5f8f3bafeeaf9a7308a5d2
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "ruby",
6
+ "package-name": "ruby_reactor",
7
+ "version-file": "lib/ruby_reactor/version.rb",
8
+ "changelog-path": "CHANGELOG.md",
9
+ "bump-minor-pre-major": true,
10
+ "bump-patch-for-minor-pre-major": false,
11
+ "draft": false,
12
+ "prerelease": false
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.4.8
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## [0.4.0](https://github.com/arturictus/ruby_reactor/compare/ruby_reactor-v0.3.2...ruby_reactor/v0.4.0) (2026-05-17)
4
+
5
+
6
+ ### Features
7
+
8
+ * `AsyncResult` returning intermediate_results ([#10](https://github.com/arturictus/ruby_reactor/issues/10)) ([0cb96d6](https://github.com/arturictus/ruby_reactor/commit/0cb96d66e88097665998601276e38e1c2249c581))
9
+ * enhance deserialization error handling in Sidekiq worker ([#23](https://github.com/arturictus/ruby_reactor/issues/23)) ([60dde95](https://github.com/arturictus/ruby_reactor/commit/60dde95606d52cc6a9d352ad0117b4092a1ebb9d))
10
+ * Enhance failure messages with step, reactor, redacted inputs, a… ([#11](https://github.com/arturictus/ruby_reactor/issues/11)) ([952feae](https://github.com/arturictus/ruby_reactor/commit/952feaeb6ebbe5fbe2daf470263d8e769ba64138))
11
+ * Introduce reactor interrupt functionality, allowing pausing and… ([#13](https://github.com/arturictus/ruby_reactor/issues/13)) ([53d0861](https://github.com/arturictus/ruby_reactor/commit/53d0861f0238f0e2247e581b0a27cba2f42cfba6))
12
+ * Rspec helpers ([#19](https://github.com/arturictus/ruby_reactor/issues/19)) ([cb71f80](https://github.com/arturictus/ruby_reactor/commit/cb71f80c0708dacf6c10c0beac88446b00f30f54))
13
+ * Web Dashboard ([#14](https://github.com/arturictus/ruby_reactor/issues/14)) ([80255dd](https://github.com/arturictus/ruby_reactor/commit/80255dd40800af8f6ed804de9c6f151331742fd5))
data/README.md CHANGED
@@ -132,22 +132,40 @@ puts result.value # => "Hello from Ruby Reactor!"
132
132
 
133
133
  ## Web Dashboard
134
134
 
135
- RubyReactor comes with a built-in web dashboard to inspect reactor executions, view logs, and retry failed steps.
135
+ RubyReactor ships with a built-in web dashboard to inspect reactor executions, view logs, and retry failed steps. The dashboard is a Rack app (a [Roda](https://roda.jeremyevans.net/) application) bundled inside the gem with its pre-compiled JS/CSS assets — no extra install or asset build step is required.
136
136
 
137
137
  ### Rails Installation
138
138
 
139
- Mount the dashboard engine in your `config/routes.rb`:
139
+ Add the gem to your `Gemfile`:
140
+
141
+ ```ruby
142
+ gem "ruby_reactor"
143
+ ```
144
+
145
+ Then mount the dashboard in your `config/routes.rb`:
140
146
 
141
147
  ```ruby
142
148
  Rails.application.routes.draw do
143
149
  # ... other routes
144
- mount RubyReactor::Web::Application => '/ruby_reactor'
150
+ mount RubyReactor::Web::Application => "/ruby_reactor"
145
151
  end
146
152
  ```
147
153
 
154
+ That's it — visit `/ruby_reactor` and the UI loads. `RubyReactor::Web::Application` is autoloaded by Zeitwerk on first reference, so no extra `require` is needed.
155
+
156
+ ### Rack / Sinatra / Standalone
157
+
158
+ Because it's a plain Rack app, you can mount it anywhere `call(env)` is accepted:
159
+
160
+ ```ruby
161
+ # config.ru
162
+ require "ruby_reactor/web/application"
163
+ run RubyReactor::Web::Application
164
+ ```
165
+
148
166
  ![RubyReactor Dashboard Screenshot](documentation/images/failed_order_processing.png)
149
167
 
150
- You can secure the dashboard using standard Rails authentication methods (e.g., `authenticate` block with Devise).
168
+ You can secure the dashboard using standard Rails authentication methods (e.g., wrapping the `mount` line in an `authenticate` block with Devise, or in a `constraints` block).
151
169
 
152
170
  ## Usage
153
171
 
@@ -871,6 +889,64 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
871
889
 
872
890
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
873
891
 
892
+ ### Running Redis for the test suite
893
+
894
+ The gem's RSpec suite expects Redis on port `6780` (see [spec/spec_helper.rb](spec/spec_helper.rb)). Start it via Docker Compose:
895
+
896
+ ```bash
897
+ docker compose up -d redis-test
898
+ ```
899
+
900
+ Then run the suite:
901
+
902
+ ```bash
903
+ bundle exec rspec
904
+ ```
905
+
906
+ Stop it when done:
907
+
908
+ ```bash
909
+ docker compose stop redis-test
910
+ ```
911
+
912
+ ### Running the demo Rails app
913
+
914
+ The demo Rails app under [demo_app/](demo_app/) has its own Redis (port `6380`) and bind-mounts the repo so edits to `lib/` are live. Two ways to run it:
915
+
916
+ **Option A — fully containerized (Redis + Rails + Sidekiq):**
917
+
918
+ ```bash
919
+ docker compose up demo-redis demo-app demo-sidekiq
920
+ ```
921
+
922
+ App available at <http://localhost:3789>.
923
+
924
+ **Option B — Redis in Docker, Rails on host:**
925
+
926
+ ```bash
927
+ docker compose up -d demo-redis
928
+
929
+ cd demo_app
930
+ bin/rails db:prepare
931
+ REDIS_URL=redis://localhost:6380/1 bin/rails server
932
+ # in another shell, if you need Sidekiq:
933
+ REDIS_URL=redis://localhost:6380/1 bundle exec sidekiq
934
+ ```
935
+
936
+ To run the demo app specs:
937
+
938
+ ```bash
939
+ cd demo_app
940
+ bundle exec rspec
941
+ ```
942
+
943
+ Tear everything down:
944
+
945
+ ```bash
946
+ docker compose down # stop containers
947
+ docker compose down -v # also remove demo_redis_data + bundle_cache volumes
948
+ ```
949
+
874
950
  ## Contributing
875
951
 
876
952
  Bug reports and pull requests are welcome on GitHub at https://github.com/arturictus/ruby_reactor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/arturictus/ruby_reactor/blob/main/CODE_OF_CONDUCT.md).
@@ -145,7 +145,7 @@ module RubyReactor
145
145
  when "Regexp"
146
146
  Regexp.new(value["source"], value["options"])
147
147
  when "GlobalID"
148
- GlobalID::Locator.locate(value["gid"])
148
+ locate_global_id(value["gid"])
149
149
  when "Template::Element"
150
150
  RubyReactor::Template::Element.new(value["map_name"], value["path"])
151
151
  when "Template::Input"
@@ -198,6 +198,15 @@ module RubyReactor
198
198
 
199
199
  private
200
200
 
201
+ def locate_global_id(gid)
202
+ unless defined?(GlobalID::Locator)
203
+ raise RubyReactor::Error::DeserializationError,
204
+ "globalid gem is required to deserialize GlobalID values (gid: #{gid})"
205
+ end
206
+
207
+ GlobalID::Locator.locate(gid)
208
+ end
209
+
201
210
  def validate_size(data)
202
211
  size = data.bytesize
203
212
  return if size <= MAX_CONTEXT_SIZE
@@ -60,6 +60,7 @@ module RubyReactor
60
60
  end
61
61
 
62
62
  def [](index)
63
+ index += count if index.negative?
63
64
  return nil if index.negative? || index >= count
64
65
 
65
66
  results = @storage.retrieve_map_results_batch(
@@ -80,15 +81,15 @@ module RubyReactor
80
81
  end
81
82
 
82
83
  def last
83
- self[count - 1]
84
+ self[-1]
84
85
  end
85
86
 
86
87
  def successes
87
- lazy.select { |result| result.is_a?(RubyReactor::Success) }.map(&:value)
88
+ lazy.grep(RubyReactor::Success).map(&:value)
88
89
  end
89
90
 
90
91
  def failures
91
- lazy.select { |result| result.is_a?(RubyReactor::Failure) }.map(&:error)
92
+ lazy.grep(RubyReactor::Failure).map(&:error)
92
93
  end
93
94
 
94
95
  private
@@ -15,7 +15,7 @@ module RubyReactor
15
15
  class ExceededError < StandardError
16
16
  attr_reader :retry_after_seconds, :key_base, :limit, :period_seconds, :period_name
17
17
 
18
- def initialize(message, retry_after_seconds:, key_base:, limit:, period_seconds:, period_name:)
18
+ def initialize(message, retry_after_seconds:, key_base:, limit:, period_seconds:, period_name:) # rubocop:disable Metrics/ParameterLists
19
19
  super(message)
20
20
  @retry_after_seconds = retry_after_seconds
21
21
  @key_base = key_base
@@ -42,7 +42,7 @@ module RubyReactor
42
42
  @limits.each do |spec|
43
43
  argv << spec[:period_seconds]
44
44
  argv << spec[:limit]
45
- argv << spec[:period_seconds] * 2 # TTL: generous, auto-cleans stale buckets
45
+ argv << (spec[:period_seconds] * 2) # TTL: generous, auto-cleans stale buckets
46
46
  end
47
47
 
48
48
  allowed, retry_after, failed_index = adapter.rate_limit_check_and_increment(keys, argv)
@@ -18,7 +18,16 @@ module RubyReactor
18
18
  end
19
19
 
20
20
  def perform(serialized_context, reactor_class_name = nil, snooze_count = 0)
21
- context = ContextSerializer.deserialize(serialized_context)
21
+ begin
22
+ context = ContextSerializer.deserialize(serialized_context)
23
+ rescue RubyReactor::Error::DeserializationError,
24
+ RubyReactor::Error::SchemaVersionError => e
25
+ # Permanent failures — retrying the same blob will keep failing.
26
+ # Mark the context as failed (best-effort) and return so Sidekiq
27
+ # does not burn its retry budget.
28
+ handle_deserialization_failure(serialized_context, reactor_class_name, e)
29
+ return
30
+ end
22
31
 
23
32
  # If reactor_class_name is provided, use it to get the reactor class
24
33
  # This handles cases where the class can't be found via const_get
@@ -110,6 +119,54 @@ module RubyReactor
110
119
  RubyReactor.configuration.logger.error("RubyReactor infrastructure failure: #{exception.message}")
111
120
  RubyReactor.configuration.logger.error("Job details: #{msg.inspect}")
112
121
  end
122
+
123
+ def handle_deserialization_failure(serialized_context, reactor_class_name, error)
124
+ metadata = extract_failure_metadata(serialized_context)
125
+ context_id = metadata[:context_id]
126
+ resolved_reactor_class_name = reactor_class_name || metadata[:reactor_class_name]
127
+
128
+ RubyReactor.configuration.logger.error(
129
+ "RubyReactor deserialization failure for context " \
130
+ "#{context_id || "unknown"}: #{error.class.name}: #{error.message}"
131
+ )
132
+
133
+ return unless context_id && resolved_reactor_class_name
134
+
135
+ payload = build_failed_context_payload(context_id, resolved_reactor_class_name, error)
136
+ RubyReactor.configuration.storage_adapter.store_context(
137
+ context_id,
138
+ payload,
139
+ resolved_reactor_class_name
140
+ )
141
+ rescue StandardError => e
142
+ # Don't let a persistence failure mask the original deserialization error.
143
+ RubyReactor.configuration.logger.error(
144
+ "RubyReactor failed to persist deserialization failure: #{e.class.name}: #{e.message}"
145
+ )
146
+ end
147
+
148
+ def extract_failure_metadata(serialized_context)
149
+ data = JSON.parse(serialized_context)
150
+ {
151
+ context_id: data["context_id"],
152
+ reactor_class_name: data["reactor_class"]
153
+ }
154
+ rescue StandardError
155
+ {}
156
+ end
157
+
158
+ def build_failed_context_payload(context_id, reactor_class_name, error)
159
+ JSON.generate(
160
+ "schema_version" => ContextSerializer::SCHEMA_VERSION,
161
+ "context_id" => context_id,
162
+ "reactor_class" => reactor_class_name,
163
+ "status" => "failed",
164
+ "failure_reason" => {
165
+ "message" => error.message,
166
+ "exception_class" => error.class.name
167
+ }
168
+ )
169
+ end
113
170
  end
114
171
  end
115
172
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyReactor
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_reactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-05-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: dry-validation
@@ -89,57 +88,15 @@ executables: []
89
88
  extensions: []
90
89
  extra_rdoc_files: []
91
90
  files:
91
+ - ".release-please-config.json"
92
+ - ".release-please-manifest.json"
92
93
  - ".rspec"
93
94
  - ".rubocop.yml"
95
+ - ".tool-versions"
96
+ - CHANGELOG.md
94
97
  - CODE_OF_CONDUCT.md
95
98
  - README.md
96
99
  - Rakefile
97
- - documentation/DAG.md
98
- - documentation/README.md
99
- - documentation/async_reactors.md
100
- - documentation/composition.md
101
- - documentation/core_concepts.md
102
- - documentation/data_pipelines.md
103
- - documentation/examples/inventory_management.md
104
- - documentation/examples/order_processing.md
105
- - documentation/examples/payment_processing.md
106
- - documentation/getting_started.md
107
- - documentation/images/failed_order_processing.png
108
- - documentation/images/payment_workflow.png
109
- - documentation/interrupts.md
110
- - documentation/locks_and_semaphores.md
111
- - documentation/retry_configuration.md
112
- - documentation/testing.md
113
- - gui/.gitignore
114
- - gui/README.md
115
- - gui/eslint.config.js
116
- - gui/index.html
117
- - gui/package-lock.json
118
- - gui/package.json
119
- - gui/postcss.config.js
120
- - gui/public/vite.svg
121
- - gui/src/App.css
122
- - gui/src/App.tsx
123
- - gui/src/assets/react.svg
124
- - gui/src/components/DagVisualizer.tsx
125
- - gui/src/components/Dashboard.tsx
126
- - gui/src/components/ErrorBoundary.tsx
127
- - gui/src/components/ReactorDetail.tsx
128
- - gui/src/components/StepInspector.tsx
129
- - gui/src/components/__tests__/DagVisualizer.test.tsx
130
- - gui/src/components/__tests__/ReactorDetail.test.tsx
131
- - gui/src/components/__tests__/StepInspector.test.tsx
132
- - gui/src/globals.d.ts
133
- - gui/src/index.css
134
- - gui/src/lib/utils.ts
135
- - gui/src/main.tsx
136
- - gui/src/test/setup.ts
137
- - gui/tailwind.config.js
138
- - gui/tsconfig.app.json
139
- - gui/tsconfig.json
140
- - gui/tsconfig.node.json
141
- - gui/vite.config.ts
142
- - gui/vitest.config.ts
143
100
  - lib/ruby_reactor.rb
144
101
  - lib/ruby_reactor/configuration.rb
145
102
  - lib/ruby_reactor/context.rb
@@ -233,7 +190,6 @@ metadata:
233
190
  homepage_uri: https://github.com/arturictus/ruby_reactor
234
191
  source_code_uri: https://github.com/arturictus/ruby_reactor
235
192
  changelog_uri: https://github.com/arturictus/ruby_reactor/blob/main/CHANGELOG.md
236
- post_install_message:
237
193
  rdoc_options: []
238
194
  require_paths:
239
195
  - lib
@@ -248,8 +204,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
204
  - !ruby/object:Gem::Version
249
205
  version: '0'
250
206
  requirements: []
251
- rubygems_version: 3.4.19
252
- signing_key:
207
+ rubygems_version: 4.0.10
253
208
  specification_version: 4
254
209
  summary: A dynamic, concurrent, dependency-resolving saga orchestrator for Ruby.
255
210
  test_files: []