solid_objects 0.17.4 → 1.0.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/CHANGELOG.md +45 -0
- data/README.md +17 -3
- data/docs/agents.md +15 -7
- data/docs/guides/expiring-reservations.md +1 -1
- data/docs/guides/ordered-jobs.md +1 -1
- data/docs/guides/race-conditions.md +1 -1
- data/docs/guides/transactional-outbox.md +1 -1
- data/docs/observability.md +1 -0
- data/docs/operations.md +26 -0
- data/docs/reminders.md +20 -0
- data/docs/roadmap.md +20 -3
- data/docs/versioning.md +50 -0
- data/docs/virtual-actors.md +9 -4
- data/lib/solid_objects/activation.rb +4 -2
- data/lib/solid_objects/actor.rb +28 -2
- data/lib/solid_objects/doctor.rb +15 -3
- data/lib/solid_objects/engine.rb +4 -0
- data/lib/solid_objects/errors.rb +3 -0
- data/lib/solid_objects/executor.rb +13 -4
- data/lib/solid_objects/json_compatibility.rb +36 -0
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +1 -0
- data/sig/generated/lib/solid_objects/actor.rbs +20 -0
- data/sig/generated/lib/solid_objects/doctor.rbs +9 -2
- data/sig/generated/lib/solid_objects/errors.rbs +3 -0
- data/sig/generated/lib/solid_objects/executor.rbs +3 -0
- data/sig/generated/lib/solid_objects/json_compatibility.rbs +22 -0
- metadata +13 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11624a306bdeca71c5da082c39329f579c8d5d9b88945db2e49b534d2c317bc2
|
|
4
|
+
data.tar.gz: 7ac2b6c343b243169f92273179591db817a0e51c24e0312eaa59f8ee46c1e17f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbf6ea115998afb97d759724469a29d8dfffa832ca52a6713027cbc5822d56ce2e40b55213deadefc598636beddeaebb49777e3e59d61a20b344a6b8bba1c670
|
|
7
|
+
data.tar.gz: 29e113dbdb487e5eaf3d9061dc494c6e6aaa54e2bf5fc2eba1bcd0e4a9db7684a1a7bffb65efc69cd0436d07c2b5e9523ab9075d8652f0c005affe545fd3d52e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
- Version 1.0 freezes the public API under semantic versioning.
|
|
6
|
+
[docs/versioning.md](docs/versioning.md) gives the promise and its limits.
|
|
7
|
+
Version 1.0 is not a claim of measured scale: the benchmarks are
|
|
8
|
+
development measurements, and longer soak tests are open work.
|
|
9
|
+
- **Breaking:** a turn that calls `schedule` and then calls no operation on the
|
|
10
|
+
result now fails with `SolidObjects::ScheduleWithoutOperation`. The runtime
|
|
11
|
+
does not retry that turn, and the message becomes a dead letter. Before this
|
|
12
|
+
release, such a call stored no reminder and gave no error. Correct code,
|
|
13
|
+
such as `schedule(at: time).expire`, does not change. The turn can keep the
|
|
14
|
+
result and call the operation later. A `schedule` call with no operation in
|
|
15
|
+
an observable fails the turn with `SolidObjects::QueryMutatedState`.
|
|
16
|
+
JavaScript has the same behavior.
|
|
17
|
+
- `bin/rails solid_objects:doctor` has a new `json` check, and the boot prints
|
|
18
|
+
a warning, when Active Support cannot decode JSON with the installed `json`
|
|
19
|
+
gem. This is the case for `json` 3.x on Rails older than 8.1.4. The check
|
|
20
|
+
decodes one document and does not compare version numbers.
|
|
21
|
+
- The gem summary and description now start with the problems that the gem
|
|
22
|
+
solves. The README has a status statement and names the projects that
|
|
23
|
+
Solid Objects is not.
|
|
24
|
+
|
|
25
|
+
## 0.17.5 - 2026-10-12
|
|
26
|
+
|
|
27
|
+
- Back off repeated interrupted-message recovery using `retry_delay`.
|
|
28
|
+
The first claimed attempt recovers immediately; after attempts 2, 3, 4,
|
|
29
|
+
and onward, the delay callback receives 1, 2, 3, and onward (defaults:
|
|
30
|
+
1, 2, 4 seconds, capped at 60 seconds). Total persisted claim attempts,
|
|
31
|
+
including earlier handler failures, determine the delay. The ready timestamp
|
|
32
|
+
survives worker replacement and preserves mailbox order while other actors
|
|
33
|
+
can proceed. Lease loss alone still does not dead-letter a message; handler
|
|
34
|
+
failure attempt limits, setting names, defaults, and schema stay unchanged.
|
|
35
|
+
- Emit `activation.lost` with the message ID and total attempt count when a
|
|
36
|
+
turn loses its fence, including while saving a handler failure or rejection,
|
|
37
|
+
matching JavaScript instrumentation. Shutdown requested
|
|
38
|
+
by that callback stops the worker before another pass. Skip a cached actor
|
|
39
|
+
whose first message is delayed so later ready messages cannot starve peers.
|
|
40
|
+
|
|
41
|
+
- Preserve caller lifecycle guarantees with regression tests for concurrent
|
|
42
|
+
synchronous calls retaining their staged effects and for caller process
|
|
43
|
+
re-registration after stale-process cleanup and retention. Both cases
|
|
44
|
+
already work in Ruby; no runtime behavior or configuration changes.
|
|
45
|
+
The concurrency test waits for the second caller's busy-actor wait before
|
|
46
|
+
allowing the first caller to commit its staged effect.
|
|
47
|
+
|
|
3
48
|
## 0.17.4 - 2026-10-12
|
|
4
49
|
|
|
5
50
|
- Renew activations between turns in a drain pass when renewal is due,
|
data/README.md
CHANGED
|
@@ -11,6 +11,18 @@ stable identity, and its state lives in the SQL database that your app already
|
|
|
11
11
|
uses. [Virtual actors in Ruby on Rails](docs/virtual-actors.md) explains the
|
|
12
12
|
model and when to use it.
|
|
13
13
|
|
|
14
|
+
**Status.** Version 1.0: the public API follows semantic versioning and does
|
|
15
|
+
not break before 2.0. [Versioning](docs/versioning.md) gives the limits of the
|
|
16
|
+
promise. First public release: August 6, 2026. One maintainer.
|
|
17
|
+
|
|
18
|
+
**Production use.** One first-party application with more than 100,000 users
|
|
19
|
+
runs it in production. That is a count of users, not a measure of load. No
|
|
20
|
+
production use by other people is known. The [benchmarks](docs/benchmarks.md)
|
|
21
|
+
are development measurements, and longer soak tests are open work.
|
|
22
|
+
|
|
23
|
+
**It is not** Solid Queue, SolidJS, or the `service_actor` gem, and it is not
|
|
24
|
+
affiliated with Rails or Cloudflare.
|
|
25
|
+
|
|
14
26
|
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.
|
|
15
27
|
|
|
16
28
|
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.
|
|
@@ -193,9 +205,11 @@ explanations remain in `docs/`. The README stops here.
|
|
|
193
205
|
|
|
194
206
|
## Status and license
|
|
195
207
|
|
|
196
|
-
|
|
197
|
-
against SQLite, PostgreSQL, and MySQL
|
|
198
|
-
|
|
208
|
+
Version 1.0 promises a stable public API. Its correctness core is tested
|
|
209
|
+
against SQLite, PostgreSQL, and MySQL. It makes no claim of measured scale:
|
|
210
|
+
the [benchmarks](docs/benchmarks.md) are development measurements, and longer
|
|
211
|
+
soak tests are open work. [Versioning](docs/versioning.md) says what a release
|
|
212
|
+
can change.
|
|
199
213
|
|
|
200
214
|
Solid Objects is released under the [MIT License](MIT-LICENSE). It is an
|
|
201
215
|
independent project and is not affiliated with, sponsored by, or endorsed by
|
data/docs/agents.md
CHANGED
|
@@ -69,18 +69,22 @@ bin/rails solid_objects:doctor
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
The generator adds an initializer and copies migrations into the application.
|
|
72
|
-
The doctor checks the configuration, the tables, and one real
|
|
72
|
+
The doctor checks the `json` gem, the configuration, the tables, and one real
|
|
73
|
+
actor round trip.
|
|
73
74
|
|
|
74
|
-
Install the current release. `bundle add solid_objects` selects it. Do not
|
|
75
|
-
|
|
76
|
-
releases.
|
|
75
|
+
Install the current release. `bundle add solid_objects` selects it. Do not use
|
|
76
|
+
an API that you remember from a 0.x release; the API changed between 0.x
|
|
77
|
+
releases. From version 1.0, the public API follows
|
|
78
|
+
[semantic versioning](versioning.md). The current version is on
|
|
79
|
+
<https://rubygems.org/gems/solid_objects>.
|
|
77
80
|
|
|
78
81
|
The generated policies deny every call. Do step 5 before you call an actor.
|
|
79
82
|
|
|
80
83
|
The `json` gem 3.x works only with Active Support 8.1.4 or newer. On Rails
|
|
81
84
|
7.1, 7.2, 8.0, or 8.1 before 8.1.4, pin `gem "json", "~> 2"` in the
|
|
82
85
|
`Gemfile`. Without the pin, Active Support raises an `ArgumentError`, such as
|
|
83
|
-
`unknown keyword: quirks_mode`, for every JSON column.
|
|
86
|
+
`unknown keyword: quirks_mode`, for every JSON column. The doctor reports this
|
|
87
|
+
problem as a failure of its `json` check, and the boot prints a warning.
|
|
84
88
|
|
|
85
89
|
[Installing and upgrading](operations.md#installing-and-upgrading) has the
|
|
86
90
|
details.
|
|
@@ -173,6 +177,9 @@ Obey these rules in actor code:
|
|
|
173
177
|
write in the same database.
|
|
174
178
|
- Do not call an external API in a handler. Use `emit` and an effect handler.
|
|
175
179
|
- Write each handler so that it can run again. Delivery is at least once.
|
|
180
|
+
- Repeated lease losses back off but do not become dead letters. Observe
|
|
181
|
+
`activation.lost` and set an application stopping policy if a hard bound is
|
|
182
|
+
required; increasing the handler-failure attempt limit does not bound lease loss.
|
|
176
183
|
- A reminder changes state only when it runs, and it runs only while
|
|
177
184
|
`solid_objects start` runs. Do not compute expiry from the clock in a query;
|
|
178
185
|
read the state that the reminder committed.
|
|
@@ -181,7 +188,7 @@ Avoid these mistakes:
|
|
|
181
188
|
|
|
182
189
|
| Mistake | Correct form |
|
|
183
190
|
| --- | --- |
|
|
184
|
-
| `schedule(at: deadline)` with no operation after it | `schedule(at: deadline, key: buyer).expire(buyer:)`. `schedule`
|
|
191
|
+
| `schedule(at: deadline)` with no operation after it | `schedule(at: deadline, key: buyer).expire(buyer:)`. A `schedule` call with no operation after it fails the turn with `SolidObjects::ScheduleWithoutOperation` |
|
|
185
192
|
| `reject "room full"` | `reject(:room_full, "The room is full")` |
|
|
186
193
|
| `id` inside an actor | `actor_id`. An actor has no `id` method |
|
|
187
194
|
| `register_effect(:name) { \|context, arguments\| ... }` | `register_effect(:name) { \|arguments, context\| ... }`. The arguments come first |
|
|
@@ -275,6 +282,7 @@ When you explain Solid Objects to a user, state these limits:
|
|
|
275
282
|
- There are no transactions across actor identities.
|
|
276
283
|
- Fencing stops a stale activation from a commit, but its code can continue to
|
|
277
284
|
run.
|
|
278
|
-
-
|
|
285
|
+
- Version 1.0 promises a stable public API. It makes no claim of measured scale,
|
|
286
|
+
and no production use by other people is known.
|
|
279
287
|
|
|
280
288
|
The [correctness contract](correctness.md) is the source for each guarantee.
|
|
@@ -164,7 +164,7 @@ See [the transactional outbox guide](transactional-outbox.md).
|
|
|
164
164
|
- Delivery is at least once. Each operation must be safe to run again.
|
|
165
165
|
- There are no transactions across actors. A reservation that spans two shows needs its own design.
|
|
166
166
|
- One busy show runs its calls one at a time. Many shows run at the same time.
|
|
167
|
-
-
|
|
167
|
+
- Version 1.0 promises a stable public API. It makes no claim of measured scale.
|
|
168
168
|
|
|
169
169
|
## More information
|
|
170
170
|
|
data/docs/guides/ordered-jobs.md
CHANGED
|
@@ -221,7 +221,7 @@ You do not assign sequence numbers. The actor mailbox gives the order.
|
|
|
221
221
|
- Delivery is at least once. Each operation must be safe to run again.
|
|
222
222
|
- The gem provides no transactions across actors. A transfer between two accounts needs its own design, for example one SQL transaction on normal tables.
|
|
223
223
|
- One busy account runs its entries one at a time.
|
|
224
|
-
-
|
|
224
|
+
- Version 1.0 promises a stable public API. It makes no claim of measured scale.
|
|
225
225
|
|
|
226
226
|
## More information
|
|
227
227
|
|
|
@@ -224,7 +224,7 @@ Delivery is at least once, so an effect can run more than once. See [effect idem
|
|
|
224
224
|
- Delivery is at least once, not exactly once. Write each operation so that it can run again.
|
|
225
225
|
- There are no transactions across actors. A change that must touch two events atomically needs one SQL transaction on normal tables.
|
|
226
226
|
- One busy event runs its calls one at a time. Many events can run at the same time.
|
|
227
|
-
-
|
|
227
|
+
- Version 1.0 promises a stable public API. It makes no claim of measured scale.
|
|
228
228
|
|
|
229
229
|
## More information
|
|
230
230
|
|
|
@@ -145,7 +145,7 @@ One successful turn commits the actor state, the staged effects, the same-databa
|
|
|
145
145
|
- Solid Objects requires Ruby 3.3 or newer and Rails 7.1 or newer.
|
|
146
146
|
- Delivery is at least once. Each effect handler must deduplicate with `context.id`.
|
|
147
147
|
- Solid Objects provides no transactions across actors.
|
|
148
|
-
-
|
|
148
|
+
- Version 1.0 promises a stable public API. It makes no claim of measured scale.
|
|
149
149
|
|
|
150
150
|
## More information
|
|
151
151
|
|
data/docs/observability.md
CHANGED
|
@@ -45,6 +45,7 @@ JavaScript does not await exporters. Telemetry is not a durable audit trail.
|
|
|
45
45
|
| ------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
46
46
|
| `activation.started/completed/failed` | Local actor activation hook lifecycle |
|
|
47
47
|
| `message.started/completed/failed/rejected` | One attempt's execution outcome |
|
|
48
|
+
| `activation.lost` | A message turn lost its activation fence; includes message ID and total attempt count |
|
|
48
49
|
| `message.retry` | Failed attempt durably queued for another attempt |
|
|
49
50
|
| `dead_letter.created` | Message exhausted retries or failed permanently |
|
|
50
51
|
| `commit_action.started/completed/failed` | One registered commit action inside the commit transaction |
|
data/docs/operations.md
CHANGED
|
@@ -43,6 +43,11 @@ an older Rails release, pin the 2.x series of `json`:
|
|
|
43
43
|
gem "json", "~> 2"
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
`bin/rails solid_objects:doctor` reports this problem as a failure of its
|
|
47
|
+
`json` check, and the boot prints a warning that starts with
|
|
48
|
+
`[solid_objects]`. Both decode one JSON document with Active Support. They do
|
|
49
|
+
not compare version numbers.
|
|
50
|
+
|
|
46
51
|
Review [CHANGELOG.md](../CHANGELOG.md) for compatibility and deployment-order
|
|
47
52
|
notes, then update the gem:
|
|
48
53
|
|
|
@@ -606,3 +611,24 @@ heartbeat cleanup.
|
|
|
606
611
|
Back up actor tables with the same consistency guarantees as application data.
|
|
607
612
|
Restoring only instances without their mailboxes/outboxes, or vice versa, can
|
|
608
613
|
violate application expectations.
|
|
614
|
+
|
|
615
|
+
## Interrupted-message recovery
|
|
616
|
+
|
|
617
|
+
An interrupted first claimed attempt recovers immediately. Recovery after
|
|
618
|
+
persisted attempts 2, 3, 4, ... calls `retry_delay` with 1, 2, 3, ... . The
|
|
619
|
+
callback returns seconds; defaults grow from 1 to 2 to 4 seconds up to 60 seconds.
|
|
620
|
+
This uses total claim attempts, including earlier handler failures, not a
|
|
621
|
+
separate lease-loss counter. The delay is stored in ready-message availability
|
|
622
|
+
when a stale claim is recovered, survives worker replacement, and keeps later
|
|
623
|
+
messages behind that message. Other actors can still run. Custom delays,
|
|
624
|
+
including zero, remain authoritative.
|
|
625
|
+
|
|
626
|
+
Lease loss alone does not create a dead letter, even with `max_attempts: 1`.
|
|
627
|
+
Handler failures still use the full attempt count for `max_attempts` and
|
|
628
|
+
`retry_delay`. An always-losing message remains pending indefinitely. Observe
|
|
629
|
+
`activation.lost` (message ID and total attempt count) and impose an application
|
|
630
|
+
stopping policy if a hard bound is required. Calling `request_shutdown` from
|
|
631
|
+
instrumentation stops the worker before its next pass. Ruby uses a separate
|
|
632
|
+
renewal thread and waits after lost turns; it does not have the JavaScript
|
|
633
|
+
microtask starvation pattern. A handler must still return before its worker
|
|
634
|
+
can finish shutting down.
|
data/docs/reminders.md
CHANGED
|
@@ -16,6 +16,26 @@ end
|
|
|
16
16
|
Use `missed: :latest` to coalesce missed occurrences or `missed: :all` to
|
|
17
17
|
enqueue each one.
|
|
18
18
|
|
|
19
|
+
## A schedule call needs an operation
|
|
20
|
+
|
|
21
|
+
`schedule` returns an object, and the reminder exists only after you call an
|
|
22
|
+
operation on that object. A turn that calls `schedule` and then calls no
|
|
23
|
+
operation on the result fails with `SolidObjects::ScheduleWithoutOperation`.
|
|
24
|
+
The runtime does not retry that turn, and the message becomes a dead letter.
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Wrong. The turn fails.
|
|
28
|
+
schedule(at: 10.minutes.from_now, key: buyer)
|
|
29
|
+
|
|
30
|
+
# Correct.
|
|
31
|
+
schedule(at: 10.minutes.from_now, key: buyer).expire(buyer:)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
You can keep the result and call the operation later in the same turn.
|
|
35
|
+
|
|
36
|
+
An observable cannot call `schedule`, with or without an operation. The turn
|
|
37
|
+
that reads the observable fails with `SolidObjects::QueryMutatedState`.
|
|
38
|
+
|
|
19
39
|
## A reminder is one named alarm per actor
|
|
20
40
|
|
|
21
41
|
The uniqueness key is `(actor, reminder name)`. Scheduling a name that is
|
data/docs/roadmap.md
CHANGED
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
[observability](observability.md).
|
|
10
10
|
|
|
11
11
|
- Rails engine, install generator, migration, and CLI
|
|
12
|
+
- A stable public API from version 1.0, with the limits in
|
|
13
|
+
[versioning](versioning.md)
|
|
14
|
+
- A turn that calls `schedule` and then calls no operation on the result fails
|
|
15
|
+
with `ScheduleWithoutOperation`, which is not retried, matching JavaScript.
|
|
16
|
+
Before 1.0 such a call stored no reminder and gave no error. The projection
|
|
17
|
+
guard counts such a call in an observable as staged work, so that turn fails
|
|
18
|
+
with `QueryMutatedState`.
|
|
19
|
+
- A `json` check in `solid_objects:doctor` and a warning during the boot, for
|
|
20
|
+
the `json` gem 3.x on an Active Support release that cannot decode with it.
|
|
21
|
+
The check decodes one document and does not compare version numbers. This
|
|
22
|
+
surface is Rails-only.
|
|
12
23
|
- Explicit actor registry, references, JSON state, and state migrations
|
|
13
24
|
- Queries, observable projections, and personalized payloads reject state mutation
|
|
14
25
|
and staged durable work with `QueryMutatedState`, matching JavaScript. Query
|
|
@@ -25,7 +36,12 @@
|
|
|
25
36
|
repeatable read. The tests count statements, and do not measure latency.
|
|
26
37
|
- Activation leases, renewal between turns as well as in the renewal thread,
|
|
27
38
|
unique activation tokens, generations, and
|
|
28
|
-
fenced commits
|
|
39
|
+
fenced commits. Interrupted first attempts recover immediately; repeated
|
|
40
|
+
interruptions use durable retry delays from total claim attempts minus one,
|
|
41
|
+
preserving mailbox order and progress for other actors. Lease loss emits
|
|
42
|
+
`activation.lost` with the message ID and attempt, including losses while
|
|
43
|
+
saving a handler failure or rejection. There is no hard retry cap;
|
|
44
|
+
applications needing one must impose a stopping policy.
|
|
29
45
|
- Bounded activation passes, idle cache, hot-actor yield, and process records
|
|
30
46
|
- At-least-once retries, terminal domain rejection, strict poison ordering,
|
|
31
47
|
dead letters, and tail retry
|
|
@@ -240,5 +256,6 @@
|
|
|
240
256
|
latency, query counts, and the three reactive delivery paths are measured on
|
|
241
257
|
SQLite; adapter-specific and end-to-end browser measurements are not.
|
|
242
258
|
|
|
243
|
-
|
|
244
|
-
|
|
259
|
+
Version 1.0 freezes the public API under [semantic versioning](versioning.md).
|
|
260
|
+
It does not complete these hardening milestones. The project makes no claim of
|
|
261
|
+
measured scale until they have operational soak evidence.
|
data/docs/versioning.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Versioning
|
|
2
|
+
|
|
3
|
+
From version 1.0.0, Solid Objects follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## The promise
|
|
7
|
+
|
|
8
|
+
The public API is the set of classes, methods, generators, commands, and
|
|
9
|
+
configuration options that `README.md` and the files in `docs/` describe.
|
|
10
|
+
|
|
11
|
+
| Release | What it can change |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| Patch, for example 1.0.1 | Corrections of defects. No new behavior |
|
|
14
|
+
| Minor, for example 1.1.0 | New behavior. Code that uses the public API continues to work |
|
|
15
|
+
| Major, for example 2.0.0 | A change that breaks code that uses the public API |
|
|
16
|
+
|
|
17
|
+
## Supported versions
|
|
18
|
+
|
|
19
|
+
A minor release can raise the minimum version of Ruby, Rails, SQLite,
|
|
20
|
+
PostgreSQL, or MySQL. It does this only for a version that its own project no
|
|
21
|
+
longer supports, and `CHANGELOG.md` names the change. A patch release does not
|
|
22
|
+
raise a minimum version.
|
|
23
|
+
[Compatibility and maturity](virtual-actors.md#compatibility-and-maturity)
|
|
24
|
+
lists the current minimum versions.
|
|
25
|
+
|
|
26
|
+
## Outside the promise
|
|
27
|
+
|
|
28
|
+
- Classes, methods, and constants that the documentation does not describe.
|
|
29
|
+
- The database schema. A minor release can add tables, columns, and indexes,
|
|
30
|
+
and it supplies a migration for each change.
|
|
31
|
+
[Installing and upgrading](operations.md#installing-and-upgrading) gives the
|
|
32
|
+
steps.
|
|
33
|
+
- The text of error messages and log lines. The error classes are part of the
|
|
34
|
+
public API.
|
|
35
|
+
- The timing of background work, where no configuration option sets it.
|
|
36
|
+
- A correction of behavior that is different from the
|
|
37
|
+
[correctness contract](correctness.md). A patch release can make such a
|
|
38
|
+
correction.
|
|
39
|
+
|
|
40
|
+
## What version 1.0 does not claim
|
|
41
|
+
|
|
42
|
+
Version 1.0 is a promise about the API. It is not a claim of measured scale.
|
|
43
|
+
The [benchmarks](benchmarks.md) are development measurements and do not
|
|
44
|
+
guarantee capacity. Longer soak tests are open work. The project has one
|
|
45
|
+
maintainer, and no production use by other people is known.
|
|
46
|
+
|
|
47
|
+
## Releases before 1.0
|
|
48
|
+
|
|
49
|
+
The 0.x releases changed the API between minor versions. `CHANGELOG.md` lists
|
|
50
|
+
each change.
|
data/docs/virtual-actors.md
CHANGED
|
@@ -12,7 +12,8 @@ State and mailboxes live in the SQLite, PostgreSQL, or MySQL database that the
|
|
|
12
12
|
Rails application already uses. Redis and a separate actor service are not
|
|
13
13
|
necessary.
|
|
14
14
|
|
|
15
|
-
Solid Objects
|
|
15
|
+
Solid Objects 1.0 promises a stable public API. It makes no claim of measured
|
|
16
|
+
scale. Read
|
|
16
17
|
[Compatibility and maturity](#compatibility-and-maturity) before you choose it.
|
|
17
18
|
|
|
18
19
|
## What a virtual actor is
|
|
@@ -170,6 +171,10 @@ again without harm.
|
|
|
170
171
|
a crash or a lost lease.
|
|
171
172
|
- Ordered turns do not cancel stale Ruby code. Fencing stops a stale activation
|
|
172
173
|
from a commit, but that code can continue to run.
|
|
174
|
+
- An interrupted first attempt recovers immediately. Repeated interruptions
|
|
175
|
+
use growing, durable retry delays while keeping mailbox order. Lease loss
|
|
176
|
+
alone has no attempt limit; use instrumentation and an application stopping
|
|
177
|
+
policy when a hard bound is required.
|
|
173
178
|
- External effects can run more than once. Use the stable effect ID, or
|
|
174
179
|
another durable key, as the idempotency key at the provider.
|
|
175
180
|
- There are no transactions across actor identities, and there is no replay of
|
|
@@ -194,9 +199,9 @@ matrix.
|
|
|
194
199
|
InnoDB. MySQL works through `mysql2` or `trilogy`.
|
|
195
200
|
- SQLite is correct for the same contract, but it is best for development and
|
|
196
201
|
modest single-host workloads.
|
|
197
|
-
-
|
|
198
|
-
three databases. The project
|
|
199
|
-
scale
|
|
202
|
+
- Version 1.0: the public API follows [semantic versioning](versioning.md). The
|
|
203
|
+
correctness core has tests against all three databases. The project makes no
|
|
204
|
+
claim of measured scale, and no production use by other people is known.
|
|
200
205
|
|
|
201
206
|
The [roadmap](roadmap.md) records what is tested, what is partial, and what is
|
|
202
207
|
next. For TypeScript and Node.js, use the
|
|
@@ -52,7 +52,8 @@ module SolidObjects
|
|
|
52
52
|
# @rbs () -> bool
|
|
53
53
|
def ready?
|
|
54
54
|
now = SolidObjects.database_adapter.database_now
|
|
55
|
-
ReadyMessage.where(instance_id: lease.instance_id
|
|
55
|
+
ready_message = ReadyMessage.where(instance_id: lease.instance_id).order(:sequence).first
|
|
56
|
+
(ready_message && ready_message.available_at <= now) ||
|
|
56
57
|
ClaimedMessage.where(
|
|
57
58
|
instance_id: lease.instance_id,
|
|
58
59
|
process_id: lease.owner_id,
|
|
@@ -227,11 +228,12 @@ module SolidObjects
|
|
|
227
228
|
claimed_message.destroy!
|
|
228
229
|
return if message.completed? || message.dead?
|
|
229
230
|
|
|
231
|
+
delay = (message.attempt_count <= 1) ? 0 : SolidObjects.configuration.retry_delay.call(message.attempt_count - 1)
|
|
230
232
|
ReadyMessage.create!(
|
|
231
233
|
message:,
|
|
232
234
|
instance:,
|
|
233
235
|
sequence: message.sequence,
|
|
234
|
-
available_at: SolidObjects.database_adapter.database_now
|
|
236
|
+
available_at: SolidObjects.database_adapter.database_now + delay
|
|
235
237
|
)
|
|
236
238
|
end
|
|
237
239
|
|
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -13,6 +13,7 @@ module SolidObjects
|
|
|
13
13
|
REMINDER_HANDLE_KEY = "reminder_name"
|
|
14
14
|
|
|
15
15
|
ReminderIntent = Data.define(:name, :operation, :at, :arguments, :interval_seconds, :missed_policy)
|
|
16
|
+
ScheduleCall = Data.define(:at, :key)
|
|
16
17
|
UnscheduleIntent = Data.define(:name)
|
|
17
18
|
UnscheduleAllIntent = Data.define(:operation)
|
|
18
19
|
ReminderStatus = Data.define(:name, :operation, :key, :next_run_at, :interval_seconds,
|
|
@@ -154,6 +155,7 @@ module SolidObjects
|
|
|
154
155
|
# @rbs @effect_recovery_intents: Array[EffectRecoveryIntent]
|
|
155
156
|
# @rbs @commit_action_intents: Array[CommitActionIntent]
|
|
156
157
|
# @rbs @reminder_intents: Array[ReminderIntent]
|
|
158
|
+
# @rbs @open_schedule_calls: Array[ScheduleCall]
|
|
157
159
|
# @rbs @outbound_message_intents: Array[OutboundMessageIntent]
|
|
158
160
|
|
|
159
161
|
attr_reader :actor_id, :state
|
|
@@ -167,6 +169,7 @@ module SolidObjects
|
|
|
167
169
|
@effect_recovery_intents = []
|
|
168
170
|
@commit_action_intents = []
|
|
169
171
|
@reminder_intents = []
|
|
172
|
+
@open_schedule_calls = []
|
|
170
173
|
@outbound_message_intents = []
|
|
171
174
|
end
|
|
172
175
|
|
|
@@ -257,11 +260,14 @@ module SolidObjects
|
|
|
257
260
|
raise ArgumentError, "missed reminder policy must be all or latest"
|
|
258
261
|
end
|
|
259
262
|
reminder_key = validated_reminder_key(key)
|
|
263
|
+
call = ScheduleCall.new(at:, key: reminder_key)
|
|
264
|
+
open_schedule_calls << call
|
|
260
265
|
|
|
261
266
|
OperationDispatcher.new(
|
|
262
267
|
actor_type: self.class.actor_type,
|
|
263
268
|
handlers: self.class.definition.messages
|
|
264
269
|
) do |operation, arguments|
|
|
270
|
+
open_schedule_calls.delete_if { |open_call| open_call.equal?(call) }
|
|
265
271
|
name = reminder_name(operation:, key: reminder_key)
|
|
266
272
|
reminder_intents << ReminderIntent.new(
|
|
267
273
|
name:,
|
|
@@ -537,6 +543,18 @@ module SolidObjects
|
|
|
537
543
|
commit_action_intents.clear
|
|
538
544
|
reminder_intents.clear
|
|
539
545
|
outbound_message_intents.clear
|
|
546
|
+
open_schedule_calls.clear
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# @rbs () -> void
|
|
550
|
+
def ensure_schedule_calls_have_operations!
|
|
551
|
+
call = open_schedule_calls.first
|
|
552
|
+
return unless call
|
|
553
|
+
|
|
554
|
+
arguments = [ "at: #{call.at.inspect}", ("key: #{call.key.inspect}" if call.key) ].compact.join(", ")
|
|
555
|
+
raise ScheduleWithoutOperation,
|
|
556
|
+
"schedule(#{arguments}) has no operation after it, so it stores no reminder. " \
|
|
557
|
+
"Call an operation on the result, for example schedule(at: time).expire"
|
|
540
558
|
end
|
|
541
559
|
|
|
542
560
|
# @rbs () -> Integer
|
|
@@ -563,11 +581,19 @@ module SolidObjects
|
|
|
563
581
|
:effect_recovery_intents,
|
|
564
582
|
:commit_action_intents,
|
|
565
583
|
:reminder_intents,
|
|
566
|
-
:outbound_message_intents
|
|
584
|
+
:outbound_message_intents,
|
|
585
|
+
:open_schedule_calls
|
|
567
586
|
|
|
568
587
|
# @rbs () -> Array[Array[Hash[Symbol, Object]]]
|
|
569
588
|
def intent_snapshot
|
|
570
|
-
[
|
|
589
|
+
[
|
|
590
|
+
effect_intents,
|
|
591
|
+
effect_recovery_intents,
|
|
592
|
+
commit_action_intents,
|
|
593
|
+
reminder_intents,
|
|
594
|
+
outbound_message_intents,
|
|
595
|
+
open_schedule_calls
|
|
596
|
+
].map do |intents|
|
|
571
597
|
intents.map { |intent| intent.to_h.deep_dup }
|
|
572
598
|
end
|
|
573
599
|
end
|
data/lib/solid_objects/doctor.rb
CHANGED
|
@@ -99,14 +99,17 @@ module SolidObjects
|
|
|
99
99
|
|
|
100
100
|
# @rbs @connection: untyped
|
|
101
101
|
# @rbs @configuration: Configuration
|
|
102
|
+
# @rbs @json_compatibility: JsonCompatibility
|
|
102
103
|
|
|
103
|
-
# @rbs (?connection: untyped, ?configuration: Configuration) -> void
|
|
104
|
+
# @rbs (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
|
|
104
105
|
def initialize(
|
|
105
106
|
connection: SolidObjects::Record.connection,
|
|
106
|
-
configuration: SolidObjects.configuration
|
|
107
|
+
configuration: SolidObjects.configuration,
|
|
108
|
+
json_compatibility: JsonCompatibility.new
|
|
107
109
|
)
|
|
108
110
|
@connection = connection
|
|
109
111
|
@configuration = configuration
|
|
112
|
+
@json_compatibility = json_compatibility
|
|
110
113
|
end
|
|
111
114
|
|
|
112
115
|
# @rbs () -> Report
|
|
@@ -114,6 +117,7 @@ module SolidObjects
|
|
|
114
117
|
configuration_check = check_configuration
|
|
115
118
|
schema_check = check_schema
|
|
116
119
|
checks = [
|
|
120
|
+
check_json,
|
|
117
121
|
configuration_check,
|
|
118
122
|
schema_check,
|
|
119
123
|
check_authorization,
|
|
@@ -129,7 +133,15 @@ module SolidObjects
|
|
|
129
133
|
|
|
130
134
|
private
|
|
131
135
|
|
|
132
|
-
attr_reader :connection, :configuration
|
|
136
|
+
attr_reader :connection, :configuration, :json_compatibility
|
|
137
|
+
|
|
138
|
+
# @rbs () -> Check
|
|
139
|
+
def check_json
|
|
140
|
+
problem = json_compatibility.problem
|
|
141
|
+
return fail_check(:json, problem) if problem
|
|
142
|
+
|
|
143
|
+
pass(:json, "Active Support decodes JSON with the json gem #{JSON::VERSION}")
|
|
144
|
+
end
|
|
133
145
|
|
|
134
146
|
# @rbs () -> Check
|
|
135
147
|
def check_configuration
|
data/lib/solid_objects/engine.rb
CHANGED
|
@@ -15,6 +15,10 @@ module SolidObjects
|
|
|
15
15
|
SolidObjects::LogSubscriber.install
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
initializer "solid_objects.json_compatibility" do
|
|
19
|
+
JsonCompatibility.new.warn_about_problem
|
|
20
|
+
end
|
|
21
|
+
|
|
18
22
|
initializer "solid_objects.actors" do |application|
|
|
19
23
|
application.config.to_prepare { ApplicationActorLoader.new.call }
|
|
20
24
|
end
|
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -16,6 +16,18 @@ module SolidObjects
|
|
|
16
16
|
|
|
17
17
|
# @rbs () -> bool
|
|
18
18
|
def call
|
|
19
|
+
execute
|
|
20
|
+
rescue LostActivation
|
|
21
|
+
SolidObjects.instrument(:"activation.lost", **instrumentation_payload, duration_milliseconds: elapsed_milliseconds)
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :activation, :message
|
|
28
|
+
|
|
29
|
+
# @rbs () -> bool
|
|
30
|
+
def execute
|
|
19
31
|
state_before = actor.state.to_h
|
|
20
32
|
observables_before = actor.observable_values
|
|
21
33
|
message_context = MessageContext.new(
|
|
@@ -29,6 +41,7 @@ module SolidObjects
|
|
|
29
41
|
SolidObjects.instrument(:"recovery.reclaimed", **instrumentation_payload) if message.attempt_count > 1 && message.error.nil?
|
|
30
42
|
SolidObjects.instrument(:"message.started", **instrumentation_payload)
|
|
31
43
|
result = invoke_actor(message_context)
|
|
44
|
+
actor.ensure_schedule_calls_have_operations!
|
|
32
45
|
observable_changes = changed_observables(observables_before, actor.observable_values)
|
|
33
46
|
state_after = actor.state.to_h_with_byte_size
|
|
34
47
|
ensure_query_is_read_only!(state_before, state_after.value)
|
|
@@ -53,10 +66,6 @@ module SolidObjects
|
|
|
53
66
|
false
|
|
54
67
|
end
|
|
55
68
|
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
attr_reader :activation, :message
|
|
59
|
-
|
|
60
69
|
# @rbs () -> Actor
|
|
61
70
|
def actor
|
|
62
71
|
activation.actor
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class JsonCompatibility
|
|
5
|
+
PROBE = '{"probe":1}'
|
|
6
|
+
|
|
7
|
+
# @rbs @decode: ^(String) -> untyped
|
|
8
|
+
|
|
9
|
+
# @rbs (?decode: ^(String) -> untyped) -> void
|
|
10
|
+
def initialize(decode: ActiveSupport::JSON.method(:decode))
|
|
11
|
+
@decode = decode
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @rbs () -> String?
|
|
15
|
+
def problem
|
|
16
|
+
decode.call(PROBE)
|
|
17
|
+
nil
|
|
18
|
+
rescue ArgumentError => error
|
|
19
|
+
"Active Support #{ActiveSupport.version} cannot decode JSON with the json gem #{JSON::VERSION} " \
|
|
20
|
+
"(#{error.message}). Add gem \"json\", \"~> 2\" to the Gemfile, " \
|
|
21
|
+
"or use a Rails release that supports json 3"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs () -> void
|
|
25
|
+
def warn_about_problem
|
|
26
|
+
message = problem
|
|
27
|
+
return unless message
|
|
28
|
+
|
|
29
|
+
warn "[solid_objects] #{message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :decode
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/solid_objects.rb
CHANGED
|
@@ -20,6 +20,7 @@ require "solid_objects/telemetry"
|
|
|
20
20
|
require "solid_objects/diagnostics"
|
|
21
21
|
require "solid_objects/log_subscriber"
|
|
22
22
|
require "solid_objects/serialization"
|
|
23
|
+
require "solid_objects/json_compatibility"
|
|
23
24
|
require "solid_objects/context"
|
|
24
25
|
require "solid_objects/actor_registry"
|
|
25
26
|
require "solid_objects/state"
|
|
@@ -81,6 +81,19 @@ module SolidObjects
|
|
|
81
81
|
def members: () -> [ :name, :operation, :at, :arguments, :interval_seconds, :missed_policy ]
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
class ScheduleCall < Data
|
|
85
|
+
attr_reader at(): untyped
|
|
86
|
+
|
|
87
|
+
attr_reader key(): untyped
|
|
88
|
+
|
|
89
|
+
def self.new: (untyped at, untyped key) -> instance
|
|
90
|
+
| (at: untyped, key: untyped) -> instance
|
|
91
|
+
|
|
92
|
+
def self.members: () -> [ :at, :key ]
|
|
93
|
+
|
|
94
|
+
def members: () -> [ :at, :key ]
|
|
95
|
+
end
|
|
96
|
+
|
|
84
97
|
class UnscheduleIntent < Data
|
|
85
98
|
attr_reader name(): untyped
|
|
86
99
|
|
|
@@ -207,6 +220,8 @@ module SolidObjects
|
|
|
207
220
|
|
|
208
221
|
@outbound_message_intents: Array[OutboundMessageIntent]
|
|
209
222
|
|
|
223
|
+
@open_schedule_calls: Array[ScheduleCall]
|
|
224
|
+
|
|
210
225
|
@reminder_intents: Array[ReminderIntent]
|
|
211
226
|
|
|
212
227
|
@commit_action_intents: Array[CommitActionIntent]
|
|
@@ -348,6 +363,9 @@ module SolidObjects
|
|
|
348
363
|
# @rbs () -> void
|
|
349
364
|
def discard_intents: () -> void
|
|
350
365
|
|
|
366
|
+
# @rbs () -> void
|
|
367
|
+
def ensure_schedule_calls_have_operations!: () -> void
|
|
368
|
+
|
|
351
369
|
# @rbs () -> Integer
|
|
352
370
|
def intent_count: () -> Integer
|
|
353
371
|
|
|
@@ -366,6 +384,8 @@ module SolidObjects
|
|
|
366
384
|
|
|
367
385
|
attr_reader outbound_message_intents: untyped
|
|
368
386
|
|
|
387
|
+
attr_reader open_schedule_calls: untyped
|
|
388
|
+
|
|
369
389
|
# @rbs () -> Array[Array[Hash[Symbol, Object]]]
|
|
370
390
|
def intent_snapshot: () -> Array[Array[Hash[Symbol, Object]]]
|
|
371
391
|
|
|
@@ -47,12 +47,14 @@ module SolidObjects
|
|
|
47
47
|
def ping: (value: String) -> String
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
@json_compatibility: JsonCompatibility
|
|
51
|
+
|
|
50
52
|
@configuration: Configuration
|
|
51
53
|
|
|
52
54
|
@connection: untyped
|
|
53
55
|
|
|
54
|
-
# @rbs (?connection: untyped, ?configuration: Configuration) -> void
|
|
55
|
-
def initialize: (?connection: untyped, ?configuration: Configuration) -> void
|
|
56
|
+
# @rbs (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
|
|
57
|
+
def initialize: (?connection: untyped, ?configuration: Configuration, ?json_compatibility: JsonCompatibility) -> void
|
|
56
58
|
|
|
57
59
|
# @rbs () -> Report
|
|
58
60
|
def call: () -> Report
|
|
@@ -63,6 +65,11 @@ module SolidObjects
|
|
|
63
65
|
|
|
64
66
|
attr_reader configuration: untyped
|
|
65
67
|
|
|
68
|
+
attr_reader json_compatibility: untyped
|
|
69
|
+
|
|
70
|
+
# @rbs () -> Check
|
|
71
|
+
def check_json: () -> Check
|
|
72
|
+
|
|
66
73
|
# @rbs () -> Check
|
|
67
74
|
def check_configuration: () -> Check
|
|
68
75
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/json_compatibility.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class JsonCompatibility
|
|
5
|
+
PROBE: ::String
|
|
6
|
+
|
|
7
|
+
@decode: ^(String) -> untyped
|
|
8
|
+
|
|
9
|
+
# @rbs (?decode: ^(String) -> untyped) -> void
|
|
10
|
+
def initialize: (?decode: ^(String) -> untyped) -> void
|
|
11
|
+
|
|
12
|
+
# @rbs () -> String?
|
|
13
|
+
def problem: () -> String?
|
|
14
|
+
|
|
15
|
+
# @rbs () -> void
|
|
16
|
+
def warn_about_problem: () -> void
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader decode: untyped
|
|
21
|
+
end
|
|
22
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -276,12 +276,13 @@ dependencies:
|
|
|
276
276
|
- - ">="
|
|
277
277
|
- !ruby/object:Gem::Version
|
|
278
278
|
version: '2.7'
|
|
279
|
-
description: '
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
ordered mailboxes, fenced activation,
|
|
283
|
-
and reactive ERB. It runs on MySQL,
|
|
284
|
-
Ruby 3.3 or newer and Rails 7.1
|
|
279
|
+
description: 'Fix race conditions on one record in Rails: ordered operations, expiring
|
|
280
|
+
holds, and durable state in your existing SQL database. Solid Objects is a SQL-backed
|
|
281
|
+
virtual actor library for Ruby on Rails. It brings the Cloudflare Durable Objects
|
|
282
|
+
programming model to Rails: addressable objects with ordered mailboxes, fenced activation,
|
|
283
|
+
per-object reminders, transactional effects, and reactive ERB. It runs on MySQL,
|
|
284
|
+
PostgreSQL, and SQLite without Redis, and requires Ruby 3.3 or newer and Rails 7.1
|
|
285
|
+
or newer.'
|
|
285
286
|
email:
|
|
286
287
|
executables:
|
|
287
288
|
- solid_objects
|
|
@@ -378,6 +379,7 @@ files:
|
|
|
378
379
|
- docs/security.md
|
|
379
380
|
- docs/state-migrations.md
|
|
380
381
|
- docs/transmission.md
|
|
382
|
+
- docs/versioning.md
|
|
381
383
|
- docs/virtual-actors.md
|
|
382
384
|
- examples/application/README.md
|
|
383
385
|
- examples/application/app/actors/chat_room_actor.rb
|
|
@@ -464,6 +466,7 @@ files:
|
|
|
464
466
|
- lib/solid_objects/executor.rb
|
|
465
467
|
- lib/solid_objects/instance_pruner.rb
|
|
466
468
|
- lib/solid_objects/instrumentation.rb
|
|
469
|
+
- lib/solid_objects/json_compatibility.rb
|
|
467
470
|
- lib/solid_objects/lease.rb
|
|
468
471
|
- lib/solid_objects/lease_renewer.rb
|
|
469
472
|
- lib/solid_objects/log_subscriber.rb
|
|
@@ -569,6 +572,7 @@ files:
|
|
|
569
572
|
- sig/generated/lib/solid_objects/executor.rbs
|
|
570
573
|
- sig/generated/lib/solid_objects/instance_pruner.rbs
|
|
571
574
|
- sig/generated/lib/solid_objects/instrumentation.rbs
|
|
575
|
+
- sig/generated/lib/solid_objects/json_compatibility.rbs
|
|
572
576
|
- sig/generated/lib/solid_objects/lease.rbs
|
|
573
577
|
- sig/generated/lib/solid_objects/lease_renewer.rbs
|
|
574
578
|
- sig/generated/lib/solid_objects/log_subscriber.rbs
|
|
@@ -685,5 +689,5 @@ requirements: []
|
|
|
685
689
|
rubygems_version: 3.5.22
|
|
686
690
|
signing_key:
|
|
687
691
|
specification_version: 4
|
|
688
|
-
summary: SQL-backed virtual actors
|
|
692
|
+
summary: Fix race conditions on one record in Rails with SQL-backed virtual actors
|
|
689
693
|
test_files: []
|