solid_objects 0.17.0 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27c3d17cf47d0d22b069ed5fbddfb948495fd1c62ef807e43d0f271511a63ea8
4
- data.tar.gz: 1161d2b5f269a22f9b509e73a0cacc8f46c0a0528bb7e54be9c2fdf81ad0055b
3
+ metadata.gz: 3b918d2d78f5336b1eeda97b9f59560b6ef22cb4703524fd17c368c3fcb90b8d
4
+ data.tar.gz: 26483ce973457d40dceb52a14bb311417e476a9bd84ce8dc5f85e814a16db9af
5
5
  SHA512:
6
- metadata.gz: f9ae16fca8e08fd41806a22a9a7f27904d0492870e52a9f1b4b9d9ca3bb6f95b85d965fd9b40e82ddc3154334564067ba36d090a8379e71c111c5dc043a0ce66
7
- data.tar.gz: 93e89b4878dac64ed92ebc50361720bf0c43a713ef069be0f5f4988bad29045cc6ca8c1aeb857510fe7830b6e6df0a3f4a5a2cbc503dedea56a4177b15b7af36
6
+ metadata.gz: 0ac2d7b510bd69aad30b8b32c8330725c08e52625ceca66a0cfde34e61f316b904aa2f21705473034096560479ff94dbc8c8a072b68e0a4e852de89c182f2bcc
7
+ data.tar.gz: 7061098cc192a52606d2957380d8a6a6d842e8a67c2c13aad0873efcfdad691bb0783fbb5252554a896d5a2424a88d24cfb5454bdb80990c5b20a6b9c65f8847
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.1 - 2026-10-08
4
+
5
+ - Name the category in the gem metadata and the README: Solid Objects is a
6
+ SQL-backed virtual actor library for Ruby on Rails. The gem homepage now
7
+ links to `https://solidobjects.dev/ruby` instead of the site root, which
8
+ redirects to the Node page.
9
+ - Add `docs/virtual-actors.md`, a category guide with the definition, a small
10
+ example, fit and poor-fit criteria, comparisons, and an Orleans concept map.
11
+ - Add `docs/agents.md`, a consumer guide for coding agents with setup,
12
+ authorization, effect idempotency, verification, and troubleshooting steps.
13
+ Both guides ship in the gem.
14
+ - Add `context7.json` so that Context7 indexes the consumer documentation and
15
+ skips maintainer files.
16
+ - Add a Rails quickstart in `examples/quickstart/` and a `rake quickstart`
17
+ check that runs it against the built gem. The check builds the gem, creates
18
+ a new SQLite Rails application, installs the gem from `vendor/cache` with
19
+ `bundle install --local`, and confirms by checksum and load path that the
20
+ application loads the built gem. It runs the install generator, the
21
+ migrations, and the doctor, and grants only the message and query policies.
22
+ It sends eight concurrent holds from separate processes to the README's
23
+ `TicketSale` actor and confirms that exactly one hold commits. It stops the
24
+ runtime, waits until a reminder is past due, confirms that the reminder did
25
+ not run, restarts the runtime, and confirms that the reminder released the
26
+ hold once. The check also fails when a `TicketSale` sample in the README or
27
+ in `docs/` differs from the actor that it runs. A new `quickstart` CI job runs
28
+ the check, and the release job waits for it.
29
+ - Correct the `json` 3.x note in `docs/operations.md`. The `json` gem 3.x works
30
+ only with Active Support 8.1.4 or newer. Active Support 7.1, 7.2, and 8.0
31
+ raise `unknown keyword: quirks_mode`, and Active Support 8.1.3.1 and earlier
32
+ 8.1 releases fail to decode. Upgrade Rails to 8.1.4 or newer, or pin `json`
33
+ to 2.x. The compatibility CI matrix now pins `json` 2.x for Rails 7.1, 7.2,
34
+ and 8.0, the configuration that the guide prescribes.
35
+
3
36
  ## 0.17.0 - 2026-10-03
4
37
 
5
38
  - Publish RBS types for portable events, metric samples, actor diagnostics, and
data/README.md CHANGED
@@ -5,6 +5,12 @@
5
5
 
6
6
  **Open Source Durable Objects in your Rails app.**
7
7
 
8
+ Solid Objects is a SQL-backed virtual actor library for Ruby on Rails, with
9
+ durable state, ordered operations, and automatic activation. Each actor has a
10
+ stable identity, and its state lives in the SQL database that your app already
11
+ uses. [Virtual actors in Ruby on Rails](docs/virtual-actors.md) explains the
12
+ model and when to use it.
13
+
8
14
  In a shopping cart, paying twice at the same time is a big problem. The payment provider might time out, and your Rails site could be restarting before recovery finishes.
9
15
 
10
16
  To deal with this safely, you often need logic scattered between 7-10 files like database row locks, Redis locks, delayed jobs, retries, and cleanup code to keep that process straight. They are not all large, but they must agree about the same payment state and failure rules. That coordination is the difficult part.
@@ -160,6 +166,8 @@ Exactly once is not hiding in a more advanced configuration. Read the
160
166
  ## Read more
161
167
 
162
168
  - [Five-minute Rails guide](https://solidobjects.dev/5min/rails)
169
+ - [Virtual actors in Ruby on Rails](docs/virtual-actors.md)
170
+ - [Guide for coding agents](docs/agents.md)
163
171
  - [Choosing Solid Objects](docs/fit.md)
164
172
  - [Operations and recovery](docs/operations.md)
165
173
  - [Observability and diagnostics](docs/observability.md)
data/Rakefile CHANGED
@@ -37,6 +37,11 @@ task :at_least_once do
37
37
  sh "bundle exec ruby examples/at_least_once/demo.rb"
38
38
  end
39
39
 
40
+ desc "Install the built gem into a new Rails app and prove ordering and restart recovery"
41
+ task :quickstart do
42
+ sh "bundle exec ruby examples/quickstart/smoke.rb"
43
+ end
44
+
40
45
  desc "Scan the Rails engine for security warnings"
41
46
  task :security do
42
47
  sh "bundle exec brakeman --force --no-pager -q ."
data/docs/agents.md ADDED
@@ -0,0 +1,253 @@
1
+ # Solid Objects for coding agents (Rails)
2
+
3
+ This guide is for a coding agent that evaluates or adds Solid Objects in a
4
+ Ruby on Rails application. It gives the facts and steps in a fixed order. Each
5
+ step links to the maintained documentation for more detail.
6
+
7
+ ## 1. Check the fit first
8
+
9
+ Use Solid Objects only if you can answer yes to most of these questions:
10
+
11
+ 1. Does one identity, such as a cart, room, booking, account, or device, own
12
+ the state?
13
+ 2. Must writes for that identity happen one at a time across requests, jobs,
14
+ and processes?
15
+ 3. Must some work happen later or continue after a process restart?
16
+ 4. Is the state a bounded JSON document, not a large relational dataset?
17
+ 5. Can the application run one more process, `bundle exec solid_objects start`,
18
+ for reminders and background work?
19
+
20
+ Select a simpler tool in these cases:
21
+
22
+ | Requirement | Use instead |
23
+ | --- | --- |
24
+ | The invariant fits in one request | `with_lock`, a unique index, or a short transaction |
25
+ | CPU-intensive or parallel work | Background jobs or Ractors. An actor serializes work |
26
+ | Thousands of writes for each second to one key | A counter column, a cache, or Solid Objects Pro |
27
+ | One atomic change across two actor identities | One SQL transaction on normal tables |
28
+ | Exactly-once calls to an external API | Not available from any actor library. Use provider idempotency keys |
29
+
30
+ The full checklist is in [Choosing Solid Objects](fit.md). The category guide
31
+ is [Virtual actors in Ruby on Rails](virtual-actors.md).
32
+
33
+ ## 2. Package identity
34
+
35
+ | Item | Value |
36
+ | --- | --- |
37
+ | Gem | `solid_objects` |
38
+ | Repository | <https://github.com/cardmagic/solid-objects-ruby> |
39
+ | Website | <https://solidobjects.dev/ruby> |
40
+ | Node.js package | `solid-objects` on npm, a separate port with the same model |
41
+ | Commercial extension | `solid_objects_pro`, a separate paid gem. The open-source gem does not need it |
42
+
43
+ Solid Objects is not part of Rails. It is not Solid Queue, Solid Cache, or
44
+ Solid Cable. It is not affiliated with Cloudflare.
45
+
46
+ ## 3. Requirements
47
+
48
+ - Ruby 3.3 or newer.
49
+ - Rails 7.1 or newer. The gem is a Rails engine. It does not run without
50
+ Rails.
51
+ - SQLite 3.35 or newer, PostgreSQL 14 or newer, or MySQL 8.0 or newer with
52
+ InnoDB.
53
+ - No Redis and no separate actor service.
54
+
55
+ ## 4. Install
56
+
57
+ ```bash
58
+ bundle add solid_objects
59
+ bin/rails generate solid_objects:install
60
+ bin/rails db:migrate
61
+ bin/rails solid_objects:doctor
62
+ ```
63
+
64
+ The generator adds an initializer and copies migrations into the application.
65
+ The doctor checks the configuration, the tables, and one real actor round trip.
66
+
67
+ The `json` gem 3.x works only with Active Support 8.1.4 or newer. On Rails
68
+ 7.1, 7.2, 8.0, or 8.1 before 8.1.4, pin `gem "json", "~> 2"` in the
69
+ `Gemfile`. Without the pin, Active Support raises an `ArgumentError`, such as
70
+ `unknown keyword: quirks_mode`, for every JSON column.
71
+
72
+ [Installing and upgrading](operations.md#installing-and-upgrading) has the
73
+ details.
74
+
75
+ ## 5. Authorize
76
+
77
+ Every policy in the generated initializer denies by default. A new
78
+ installation answers no actor call until you write a policy. Do not remove
79
+ this behavior.
80
+
81
+ For a local demonstration only, grant messages and queries:
82
+
83
+ ```ruby
84
+ SolidObjects.configure do |configuration|
85
+ configuration.authorize_message = ->(**) { true }
86
+ configuration.authorize_query = ->(**) { true }
87
+ end
88
+ ```
89
+
90
+ Keep `authorize_destroy`, `authorize_subscription`,
91
+ `authorize_administration`, and `authorize_transmission` denied in a
92
+ demonstration.
93
+
94
+ A production policy must bind the actor type and ID to the authenticated user
95
+ or tenant. An actor ID is not a permission:
96
+
97
+ ```ruby
98
+ SolidObjects.configure do |configuration|
99
+ owns_cart = lambda do |actor_type:, actor_id:, authorization_context:, **|
100
+ user = authorization_context
101
+
102
+ actor_type == "ShoppingCart" &&
103
+ user.present? &&
104
+ actor_id == user.id.to_s
105
+ end
106
+
107
+ configuration.authorize_message = owns_cart
108
+ configuration.authorize_query = owns_cart
109
+ end
110
+ ```
111
+
112
+ Pass the context on each call:
113
+
114
+ ```ruby
115
+ ShoppingCart.ref(Current.user.id.to_s).add_item(
116
+ product_id: "shirt-123",
117
+ authorization_context: Current.user
118
+ )
119
+ ```
120
+
121
+ [Authorization policies](authorization.md) lists each policy and its risk.
122
+
123
+ ## 6. Define an actor
124
+
125
+ Put actors in `app/actors/`. This actor is the example from the README:
126
+
127
+ ```ruby
128
+ class TicketSale < SolidObjects::Actor
129
+ attribute :available, default: 1
130
+ attribute :holds, default: -> { {} }
131
+
132
+ def hold(buyer:)
133
+ return { held: false, available: } if available.zero? || holds.key?(buyer)
134
+
135
+ self.available -= 1
136
+ self.holds = holds.merge(buyer => Time.current.to_i)
137
+ schedule(at: 10.minutes.from_now, key: buyer).expire(buyer:)
138
+ { held: true, available: }
139
+ end
140
+
141
+ def expire(buyer:)
142
+ return available unless holds.key?(buyer)
143
+
144
+ self.holds = holds.except(buyer)
145
+ self.available += 1
146
+ end
147
+ end
148
+ ```
149
+
150
+ Obey these rules in actor code:
151
+
152
+ - Keep state in `attribute` values. State must be JSON-compatible.
153
+ - Use `schedule(at:, key:)` for delayed work. A reminder is one named alarm
154
+ for each actor and key. A new `schedule` with the same key moves the alarm.
155
+ - Use `reject(code, message)` for a business rule failure that must not retry.
156
+ - Do not write Active Record models directly in a handler. The runtime raises
157
+ `SolidObjects::ApplicationWriteForbidden`. Use `commit_action` for a short
158
+ write in the same database.
159
+ - Do not call an external API in a handler. Use `emit` and an effect handler.
160
+ - Write each handler so that it can run again. Delivery is at least once.
161
+
162
+ [Reminders](reminders.md) and the [architecture guide](architecture.md) give
163
+ the full actor API.
164
+
165
+ ## 7. Run the runtime process
166
+
167
+ A direct call, such as `TicketSale.ref("event-42").hold(buyer: "ada")`, runs in
168
+ the caller. It needs no worker. These features need the runtime process:
169
+
170
+ - Reminders from `schedule`.
171
+ - `async` calls.
172
+ - Effects from `emit` and their callbacks.
173
+ - Broadcasts to Action Cable.
174
+
175
+ Start it beside the web process:
176
+
177
+ ```bash
178
+ bundle exec solid_objects start
179
+ ```
180
+
181
+ Add it to the `Procfile`, the process manager, or the deployment
182
+ configuration. When it stops, pending work stays in SQL and runs after it
183
+ starts again. [Operations](operations.md#runtime) covers roles and shutdown.
184
+
185
+ ## 8. Make external effects idempotent
186
+
187
+ Register an effect handler at boot. Use `context.id` as the provider
188
+ idempotency key:
189
+
190
+ ```ruby
191
+ SolidObjects.register_effect(:charge_payment) do |arguments, context|
192
+ Payments.charge(
193
+ idempotency_key: context.id,
194
+ payment_id: arguments.fetch("payment_id"),
195
+ amount_cents: arguments.fetch("amount_cents")
196
+ )
197
+ end
198
+ ```
199
+
200
+ Stage it from the actor:
201
+
202
+ ```ruby
203
+ emit :charge_payment, payment_id:, amount_cents:, on_success: :charged
204
+ ```
205
+
206
+ The effect can run more than once after a crash. The `context.id` value is the
207
+ same each time. [Effect recovery](effect-recovery.md) explains how to retire
208
+ abandoned work.
209
+
210
+ ## 9. Verify the implementation
211
+
212
+ Do these checks before you report that the work is complete:
213
+
214
+ 1. Run `bin/rails solid_objects:doctor`. It must report no failures.
215
+ 2. Write a test that includes `SolidObjects::TestHelper`. Send concurrent
216
+ calls to one identity from several threads. Assert the final state, for
217
+ example that only one hold succeeded.
218
+ 3. Use `run_due_reminders(now:)` and `drain_solid_objects` to test delayed
219
+ work without sleeps.
220
+ 4. Start `bundle exec solid_objects start`, schedule a short reminder, and stop
221
+ the process. Start it again after the deadline and confirm that the reminder
222
+ ran.
223
+ 5. Confirm that each effect handler deduplicates with `context.id`.
224
+ 6. Confirm that production policies do not grant access to every caller.
225
+
226
+ [Host application tests](development.md#host-application-tests) describes the
227
+ test helper. The [clean-install quickstart](../examples/quickstart/README.md)
228
+ runs checks 2 and 4 against a new Rails application.
229
+
230
+ ## 10. Troubleshooting
231
+
232
+ | Symptom | Cause and fix |
233
+ | --- | --- |
234
+ | `SolidObjects::Unauthorized` | A policy denied the call. Write the policy, and pass `authorization_context:` |
235
+ | A reminder or `async` call does not run | The runtime process is not running. Start `bundle exec solid_objects start` |
236
+ | `SolidObjects::SyncInsideTransaction` | The call ran inside an open transaction. Call the actor outside the transaction. In tests, include `SolidObjects::TestHelper` |
237
+ | `SolidObjects::SyncTimeout` | The call did not finish in time. The message is still durable. Use its `message_reference` to wait for the result |
238
+ | `SolidObjects::ApplicationWriteForbidden` | A handler wrote a model directly. Use `commit_action` or `emit` |
239
+ | `SolidObjects::Rejected` | The actor called `reject`. This is a business result, not a retry |
240
+ | `ArgumentError` from `ActiveSupport::JSON`, such as `unknown keyword: quirks_mode` | `json` 3.x with Active Support before 8.1.4. Upgrade Rails to 8.1.4 or newer, or pin `gem "json", "~> 2"` |
241
+
242
+ ## 11. Guarantees to state correctly
243
+
244
+ When you explain Solid Objects to a user, state these limits:
245
+
246
+ - Delivery is at least once, not exactly once.
247
+ - Calls for one identity are ordered. Different identities run concurrently.
248
+ - There are no transactions across actor identities.
249
+ - Fencing stops a stale activation from a commit, but its code can continue to
250
+ run.
251
+ - The gem is pre-1.0 and makes no production-ready claim.
252
+
253
+ The [correctness contract](correctness.md) is the source for each guarantee.
data/docs/operations.md CHANGED
@@ -26,22 +26,24 @@ reports a failed or warned check rather than raising out of the command.
26
26
  ## Installing and upgrading
27
27
 
28
28
  Solid Objects keeps actor state, message arguments, results, and the remembered
29
- idempotency keys in JSON columns. Active Support decodes every one of them, and
30
- `ActiveSupport::JSON.decode` raises with the `json` gem at 3.0.2:
29
+ idempotency keys in JSON columns. Active Support encodes and decodes every one
30
+ of them. The `json` gem 3.x works only with Active Support 8.1.4 or newer:
31
31
 
32
- ```
33
- ArgumentError: wrong number of arguments (given 2, expected 1)
34
- ```
32
+ - Active Support 7.1, 7.2, and 8.0 raise
33
+ `ArgumentError: unknown keyword: quirks_mode` when they encode or decode.
34
+ - Active Support 8.1.3.1 and earlier 8.1 releases raise
35
+ `ArgumentError: wrong number of arguments (given 2, expected 1)` when they
36
+ decode.
35
37
 
36
38
  The failure is in Active Support rather than in Solid Objects, and it reaches
37
- every JSON column in a Rails application. A new Rails 8.1 application resolves
38
- `json` 3.0.2 today, so pin the 2.x series until Rails ships a fix:
39
+ every JSON column in a Rails application. Upgrade Rails to 8.1.4 or newer. On
40
+ an older Rails release, pin the 2.x series of `json`:
39
41
 
40
42
  ```ruby
41
43
  gem "json", "~> 2"
42
44
  ```
43
45
 
44
- Review [CHANGELOG.md](CHANGELOG.md) for compatibility and deployment-order
46
+ Review [CHANGELOG.md](../CHANGELOG.md) for compatibility and deployment-order
45
47
  notes, then update the gem:
46
48
 
47
49
  ```bash
@@ -0,0 +1,226 @@
1
+ # Virtual actors in Ruby on Rails
2
+
3
+ ## Short answer
4
+
5
+ Yes. Solid Objects is a SQL-backed virtual actor library for Ruby on Rails.
6
+ The gem is `solid_objects`. It gives each actor a stable identity, durable
7
+ state, ordered operations, and automatic activation.
8
+
9
+ Solid Objects requires Rails. It is a Rails engine for Ruby 3.3 or newer and
10
+ Rails 7.1 or newer. It is not a framework-independent Ruby actor runtime.
11
+ State and mailboxes live in the SQLite, PostgreSQL, or MySQL database that the
12
+ Rails application already uses. Redis and a separate actor service are not
13
+ necessary.
14
+
15
+ Solid Objects is a pre-1.0 release. It makes no production-ready claim. Read
16
+ [Compatibility and maturity](#compatibility-and-maturity) before you choose it.
17
+
18
+ ## What a virtual actor is
19
+
20
+ A virtual actor is a logical object that always exists by name. The caller
21
+ does not create it, start it, or stop it. The runtime loads it when a message
22
+ arrives and releases it when it is idle. Microsoft Orleans made this model
23
+ known as "virtual actors".
24
+
25
+ Solid Objects implements four properties of that model:
26
+
27
+ | Property | What it means | How Solid Objects does it |
28
+ | --- | --- | --- |
29
+ | Stable identity | An actor is addressed by type and ID, for example one cart per user. | `ShoppingCart.ref(user.id)` returns a cheap reference. The reference does not load the actor. |
30
+ | Automatic activation | The first message activates the actor. An idle actor is released. | A process claims a fenced activation lease when work arrives. The lease is released after the idle timeout. |
31
+ | Durable state | State survives process restarts and deploys. | Actor attributes are a JSON document in the application database. |
32
+ | Ordered turns | One identity runs one operation at a time, in a fixed order. | Each call is a durable mailbox message with a per-actor sequence number. |
33
+
34
+ Different identities run concurrently. One identity is a serialization point
35
+ on purpose.
36
+
37
+ ## A small example
38
+
39
+ This actor holds one ticket for a buyer and releases the hold after ten
40
+ minutes. Put it in `app/actors/ticket_sale.rb`:
41
+
42
+ ```ruby
43
+ class TicketSale < SolidObjects::Actor
44
+ attribute :available, default: 1
45
+ attribute :holds, default: -> { {} }
46
+
47
+ def hold(buyer:)
48
+ return { held: false, available: } if available.zero? || holds.key?(buyer)
49
+
50
+ self.available -= 1
51
+ self.holds = holds.merge(buyer => Time.current.to_i)
52
+ schedule(at: 10.minutes.from_now, key: buyer).expire(buyer:)
53
+ { held: true, available: }
54
+ end
55
+
56
+ def expire(buyer:)
57
+ return available unless holds.key?(buyer)
58
+
59
+ self.holds = holds.except(buyer)
60
+ self.available += 1
61
+ end
62
+ end
63
+ ```
64
+
65
+ Call it from a controller, a job, or the console:
66
+
67
+ ```ruby
68
+ TicketSale.ref("event-42").hold(buyer: "ada")
69
+ ```
70
+
71
+ Two concurrent `hold` calls for `event-42` enter the same mailbox. They commit
72
+ one at a time, so only one buyer gets the ticket. The hold and its reminder
73
+ commit in one transaction. The reminder runs in the Solid Objects runtime
74
+ process:
75
+
76
+ ```bash
77
+ bundle exec solid_objects start
78
+ ```
79
+
80
+ If that process stops, the reminder stays in SQL. It runs when the process
81
+ starts again.
82
+
83
+ The example needs installation, migrations, and an authorization policy. The
84
+ generated policies deny every operation by default. For the complete setup,
85
+ use one of these guides:
86
+
87
+ - [Installation](../README.md#installation) in the README.
88
+ - [The clean-install quickstart](../examples/quickstart/README.md), which a
89
+ smoke check runs against the built gem in a new Rails application.
90
+ - [The agent guide](agents.md), which gives setup and verification steps for
91
+ coding agents.
92
+
93
+ ## When to use it
94
+
95
+ Solid Objects is a good candidate when most of these conditions are true:
96
+
97
+ - State belongs to one durable identity, such as a cart, room, booking,
98
+ device, account, or workflow.
99
+ - Writes for that identity must be serialized across requests, jobs, and
100
+ processes.
101
+ - The work must happen later, survive a restart, or stay ordered across more
102
+ than one request.
103
+ - The state is a bounded JSON document.
104
+ - The identity needs reminders, external effects, or reactive Rails views that
105
+ follow its committed state.
106
+
107
+ ## When to use something else
108
+
109
+ Do not use an actor when a simpler tool enforces the invariant:
110
+
111
+ - One short transaction, `with_lock`, or a database constraint is enough. A row
112
+ lock is often the clearest answer.
113
+ - The work is CPU-intensive or data-parallel. An actor serializes work. It does
114
+ not add CPU parallelism.
115
+ - One hot identity must accept many writes for each second, for example a
116
+ request-path rate limiter or a page-view counter.
117
+ - The state is large, relational, or query-heavy. Keep that data in normal
118
+ tables.
119
+ - The operation must change two actor identities in one atomic transaction.
120
+ Solid Objects has no cross-actor transactions.
121
+ - You need exactly-once calls to an external API. No actor library can promise
122
+ that through every network failure. Solid Objects gives at-least-once
123
+ delivery and stable effect IDs for idempotency keys.
124
+ - You need replay of named workflow steps from a step log. That is a durable
125
+ execution engine, not an actor.
126
+
127
+ [Choosing Solid Objects](fit.md) has the full checklist and the cost model.
128
+
129
+ ## How it compares
130
+
131
+ This table compares coordination models for a Rails application. It does not
132
+ rank the projects. Facts about other projects were checked on October 7, 2026,
133
+ against the sources in [Primary references](#primary-references).
134
+
135
+ | Approach | Unit of order | Durable state | Delayed work | Extra service | Good for |
136
+ | --- | --- | --- | --- | --- | --- |
137
+ | `with_lock` or a short SQL transaction | Rows in one transaction | Application tables | None | No | An invariant that fits in one request |
138
+ | Active Job with Solid Queue | None. `limits_concurrency` limits overlap for each key, but it does not set an order | Application tables, owned by the application | Scheduled jobs | No; Solid Queue uses the database | Background work that does not own entity state |
139
+ | Sidekiq | None in the open-source gem. Unique jobs and rate limits are Sidekiq Enterprise features | Application tables, owned by the application | Scheduled jobs | Redis | High-volume background jobs |
140
+ | In-process concurrency: Ractor, `concurrent-ruby-edge` actors, Async | One Ruby object in one process | None. State is lost when the process stops | In process only | No | Parallel or concurrent work inside one process. Ractor is experimental in Ruby 4.0 |
141
+ | Solid Objects | Actor type and ID | JSON state in the application database | Durable per-actor reminders | No; the `solid_objects start` process runs in the app | Durable per-identity state with ordered operations |
142
+ | Dapr actors | Actor type and ID, one turn at a time | A transactional Dapr state store | Durable reminders through the Dapr Scheduler service | A Dapr sidecar, plus the placement and Scheduler services. Dapr has no official Ruby SDK | Polyglot services on a Dapr platform |
143
+ | Temporal (`temporalio` gem) | A workflow execution | Temporal event history | Durable timers | A Temporal Service, self-hosted or Temporal Cloud | Long-running workflows that replay deterministic code |
144
+
145
+ ### Orleans concept map
146
+
147
+ Orleans is the reference design for virtual actors on .NET. Solid Objects
148
+ uses the same programming model on a SQL database. It does not copy the
149
+ Orleans cluster, placement, or feature set.
150
+
151
+ | Orleans | Solid Objects | Difference |
152
+ | --- | --- | --- |
153
+ | Grain class | `SolidObjects::Actor` subclass | None in concept |
154
+ | Grain identity (key) | Actor type and actor ID | None in concept |
155
+ | Activation on first call | Activation lease on first claimed message | Solid Objects fences each activation with a database generation |
156
+ | Turn-based execution | Ordered mailbox, one turn at a time | Orleans can enable reentrancy. Solid Objects turns for one identity never interleave |
157
+ | Grain persistence | JSON attributes in the application database | An Orleans grain calls `WriteStateAsync`. Solid Objects persists state with the turn that changed it |
158
+ | Reminders | `schedule` | Both are durable. Orleans skips a tick that falls due while the cluster is down. A due Solid Objects reminder runs when a runtime process starts. Solid Objects has no non-durable timers |
159
+ | Silos and cluster membership | Any Rails process that runs `solid_objects start` | Solid Objects has no placement, directory, or cluster membership. The database is the coordination point |
160
+ | Streams | Observables, broadcasts, and reactive ERB | Solid Objects delivers committed revisions to Action Cable |
161
+
162
+ Solid Objects delivery is at least once. Write each handler so that it can run
163
+ again without harm.
164
+
165
+ ## Guarantees and boundaries
166
+
167
+ - Calls are durably ordered per identity. Different identities can run
168
+ concurrently.
169
+ - Delivery is at least once, not exactly once. A handler can start again after
170
+ a crash or a lost lease.
171
+ - Ordered turns do not cancel stale Ruby code. Fencing stops a stale activation
172
+ from a commit, but that code can continue to run.
173
+ - External effects can run more than once. Use the stable effect ID, or
174
+ another durable key, as the idempotency key at the provider.
175
+ - There are no transactions across actor identities, and there is no replay of
176
+ durable function steps.
177
+ - Reminders, `async` calls, effects, and broadcasts need the
178
+ `bundle exec solid_objects start` process. When that process stops, the work
179
+ waits in SQL. The gem does not supply a hosted worker.
180
+ - One hot identity is sequential. The core gem is not a high-throughput
181
+ request-path rate limiter.
182
+ - The guarantees apply only to changes made through the actor APIs. Actor
183
+ fencing does not protect direct writes to the same data or other external
184
+ requests.
185
+
186
+ The [correctness contract](correctness.md) states each guarantee and the crash
187
+ matrix.
188
+
189
+ ## Compatibility and maturity
190
+
191
+ - Ruby 3.3 or newer and Rails 7.1 or newer. CI runs Ruby 3.3, 3.4, and 4.0
192
+ against Rails 7.1, 7.2, 8.0, and 8.1.
193
+ - SQLite 3.35 or newer, PostgreSQL 14 or newer, or MySQL 8.0 or newer with
194
+ InnoDB. MySQL works through `mysql2` or `trilogy`.
195
+ - SQLite is correct for the same contract, but it is best for development and
196
+ modest single-host workloads.
197
+ - Pre-1.0. Expect changes that break compatibility. The correctness core has tests against all
198
+ three databases. The project has no production-ready claim and no measured
199
+ scale claim.
200
+
201
+ The [roadmap](roadmap.md) records what is tested, what is partial, and what is
202
+ next. For TypeScript and Node.js, use the
203
+ [solid-objects](https://github.com/cardmagic/solid-objects-js) package.
204
+
205
+ ## Primary references
206
+
207
+ - Orleans: [Overview](https://learn.microsoft.com/en-us/dotnet/orleans/overview),
208
+ [Request scheduling](https://learn.microsoft.com/en-us/dotnet/orleans/grains/request-scheduling),
209
+ [Timers and reminders](https://learn.microsoft.com/en-us/dotnet/orleans/grains/timers-and-reminders),
210
+ [Grain persistence](https://learn.microsoft.com/en-us/dotnet/orleans/grains/grain-persistence/),
211
+ and [Grain placement](https://learn.microsoft.com/en-us/dotnet/orleans/grains/grain-placement).
212
+ - Ruby: [Ractor](https://docs.ruby-lang.org/en/4.0/Ractor.html) and the
213
+ [Ruby 4.0.0 release notes](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/).
214
+ - concurrent-ruby: [repository](https://github.com/ruby-concurrency/concurrent-ruby)
215
+ and [`Concurrent::Actor`](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Actor.html).
216
+ - Solid Queue: [concurrency controls](https://github.com/rails/solid_queue#concurrency-controls).
217
+ - Sidekiq: [Enterprise unique jobs](https://github.com/sidekiq/sidekiq/wiki/Ent-Unique-Jobs)
218
+ and [Enterprise rate limiting](https://github.com/sidekiq/sidekiq/wiki/Ent-Rate-Limiting).
219
+ - Dapr: [Actors overview](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/),
220
+ [Actor timers and reminders](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-timers-reminders/),
221
+ and [SDKs](https://docs.dapr.io/developing-applications/sdks/).
222
+ - Temporal: [Ruby SDK](https://github.com/temporalio/sdk-ruby) and
223
+ [Event History](https://docs.temporal.io/encyclopedia/event-history).
224
+
225
+ Other projects change. Check these sources again before you base an
226
+ architecture decision on one row.