solid_objects 0.1.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 +7 -0
- data/CHANGELOG.md +17 -0
- data/MIT-LICENSE +19 -0
- data/README.md +744 -0
- data/Rakefile +40 -0
- data/app/controllers/solid_objects/application_controller.rb +23 -0
- data/app/controllers/solid_objects/dead_letters_controller.rb +23 -0
- data/app/controllers/solid_objects/instances_controller.rb +29 -0
- data/app/helpers/solid_objects/actor_helper.rb +25 -0
- data/app/models/solid_objects/broadcast.rb +10 -0
- data/app/models/solid_objects/claimed_message.rb +14 -0
- data/app/models/solid_objects/dead_letter.rb +13 -0
- data/app/models/solid_objects/effect.rb +10 -0
- data/app/models/solid_objects/instance.rb +93 -0
- data/app/models/solid_objects/message.rb +53 -0
- data/app/models/solid_objects/process.rb +13 -0
- data/app/models/solid_objects/ready_message.rb +10 -0
- data/app/models/solid_objects/record.rb +17 -0
- data/app/models/solid_objects/reminder.rb +9 -0
- data/app/views/solid_objects/dead_letters/index.html.erb +26 -0
- data/app/views/solid_objects/instances/index.html.erb +24 -0
- data/app/views/solid_objects/instances/show.html.erb +35 -0
- data/benchmark/activation_cache.rb +5 -0
- data/benchmark/ask_latency.rb +5 -0
- data/benchmark/claim.rb +5 -0
- data/benchmark/cold_actors.rb +5 -0
- data/benchmark/concurrent_actors.rb +5 -0
- data/benchmark/enqueue.rb +5 -0
- data/benchmark/hot_actor.rb +5 -0
- data/benchmark/processing.rb +5 -0
- data/benchmark/query_count.rb +5 -0
- data/benchmark/support.rb +271 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20260805000000_create_solid_objects_tables.rb +319 -0
- data/docs/adr/0001-postgresql-backend.md +21 -0
- data/docs/adr/0002-jsonb-actor-state.md +21 -0
- data/docs/adr/0003-mailbox-ordering.md +30 -0
- data/docs/adr/0004-activation-leasing.md +21 -0
- data/docs/adr/0005-fencing-tokens.md +25 -0
- data/docs/adr/0006-at-least-once-delivery.md +24 -0
- data/docs/adr/0007-transactional-outbox.md +21 -0
- data/docs/adr/0008-actor-communication.md +21 -0
- data/docs/adr/0009-realtime-updates.md +21 -0
- data/docs/adr/0010-state-versioning.md +29 -0
- data/docs/adr/0011-wake-up-strategy.md +34 -0
- data/docs/adr/0012-not-active-jobs.md +21 -0
- data/docs/adr/0013-database-adapters.md +48 -0
- data/docs/architecture.md +615 -0
- data/docs/benchmarks.md +26 -0
- data/docs/correctness.md +124 -0
- data/docs/database-schema.md +111 -0
- data/docs/development.md +87 -0
- data/docs/implementation-plan.md +518 -0
- data/docs/operations.md +123 -0
- data/docs/realtime.md +51 -0
- data/docs/research/solid_queue.md +545 -0
- data/docs/roadmap.md +53 -0
- data/docs/security.md +61 -0
- data/docs/state-migrations.md +46 -0
- data/examples/application/README.md +16 -0
- data/examples/application/app/actors/chat_room_actor.rb +34 -0
- data/examples/application/app/actors/shopping_cart_actor.rb +79 -0
- data/examples/application/app/controllers/cart_controller.rb +54 -0
- data/examples/application/app/controllers/chat_rooms_controller.rb +44 -0
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +8 -0
- data/examples/application/app/views/actors/shopping_cart_actor/_summary.html.erb +10 -0
- data/examples/application/app/views/cart/show.html.erb +13 -0
- data/examples/application/app/views/chat_rooms/show.html.erb +8 -0
- data/examples/application/config/initializers/solid_objects.rb +23 -0
- data/examples/application/config/routes.rb +20 -0
- data/exe/solid_objects +9 -0
- data/lib/generators/solid_objects/install_generator.rb +21 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +13 -0
- data/lib/solid_objects/action_cable_broadcast_adapter.rb +19 -0
- data/lib/solid_objects/activation.rb +183 -0
- data/lib/solid_objects/activation_manager.rb +102 -0
- data/lib/solid_objects/actor.rb +271 -0
- data/lib/solid_objects/actor_channel.rb +29 -0
- data/lib/solid_objects/actor_definition.rb +212 -0
- data/lib/solid_objects/actor_registry.rb +65 -0
- data/lib/solid_objects/actor_snapshot.rb +42 -0
- data/lib/solid_objects/actor_view.rb +117 -0
- data/lib/solid_objects/broadcast_executor.rb +162 -0
- data/lib/solid_objects/cli.rb +118 -0
- data/lib/solid_objects/client.rb +153 -0
- data/lib/solid_objects/configuration.rb +168 -0
- data/lib/solid_objects/context.rb +41 -0
- data/lib/solid_objects/database_adapter.rb +82 -0
- data/lib/solid_objects/database_adapters/mysql.rb +22 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +17 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +12 -0
- data/lib/solid_objects/dead_letter_manager.rb +47 -0
- data/lib/solid_objects/dom_identity.rb +38 -0
- data/lib/solid_objects/effect_executor.rb +235 -0
- data/lib/solid_objects/effect_registry.rb +34 -0
- data/lib/solid_objects/engine.rb +33 -0
- data/lib/solid_objects/errors.rb +65 -0
- data/lib/solid_objects/executor.rb +290 -0
- data/lib/solid_objects/instrumentation.rb +10 -0
- data/lib/solid_objects/lease.rb +172 -0
- data/lib/solid_objects/lease_renewer.rb +70 -0
- data/lib/solid_objects/log_subscriber.rb +29 -0
- data/lib/solid_objects/mailbox.rb +178 -0
- data/lib/solid_objects/message_reference.rb +52 -0
- data/lib/solid_objects/process_registry.rb +143 -0
- data/lib/solid_objects/reference.rb +96 -0
- data/lib/solid_objects/reminder_scheduler.rb +168 -0
- data/lib/solid_objects/serialization.rb +99 -0
- data/lib/solid_objects/state.rb +111 -0
- data/lib/solid_objects/stream_name.rb +29 -0
- data/lib/solid_objects/stream_token.rb +59 -0
- data/lib/solid_objects/supervisor.rb +87 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +35 -0
- data/lib/solid_objects/version.rb +5 -0
- data/lib/solid_objects/wake_up.rb +28 -0
- data/lib/solid_objects/worker.rb +139 -0
- data/lib/solid_objects.rb +118 -0
- data/sig/generated/controllers/solid_objects/application_controller.rbs +10 -0
- data/sig/generated/controllers/solid_objects/dead_letters_controller.rbs +11 -0
- data/sig/generated/controllers/solid_objects/instances_controller.rbs +11 -0
- data/sig/generated/helpers/solid_objects/actor_helper.rbs +8 -0
- data/sig/generated/lib/generators/solid_objects/install_generator.rbs +13 -0
- data/sig/generated/lib/solid_objects/action_cable_broadcast_adapter.rbs +8 -0
- data/sig/generated/lib/solid_objects/activation.rbs +65 -0
- data/sig/generated/lib/solid_objects/activation_manager.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor.rbs +183 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +8 -0
- data/sig/generated/lib/solid_objects/actor_definition.rbs +117 -0
- data/sig/generated/lib/solid_objects/actor_registry.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +28 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +56 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +55 -0
- data/sig/generated/lib/solid_objects/cli.rbs +31 -0
- data/sig/generated/lib/solid_objects/client.rbs +35 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +147 -0
- data/sig/generated/lib/solid_objects/context.rbs +56 -0
- data/sig/generated/lib/solid_objects/database_adapter.rbs +42 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +16 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +13 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +10 -0
- data/sig/generated/lib/solid_objects/dead_letter_manager.rbs +16 -0
- data/sig/generated/lib/solid_objects/dom_identity.rbs +20 -0
- data/sig/generated/lib/solid_objects/effect_executor.rbs +82 -0
- data/sig/generated/lib/solid_objects/effect_registry.rbs +24 -0
- data/sig/generated/lib/solid_objects/engine.rbs +7 -0
- data/sig/generated/lib/solid_objects/errors.rbs +64 -0
- data/sig/generated/lib/solid_objects/executor.rbs +60 -0
- data/sig/generated/lib/solid_objects/instrumentation.rbs +8 -0
- data/sig/generated/lib/solid_objects/lease.rbs +57 -0
- data/sig/generated/lib/solid_objects/lease_renewer.rbs +42 -0
- data/sig/generated/lib/solid_objects/log_subscriber.rbs +11 -0
- data/sig/generated/lib/solid_objects/mailbox.rbs +46 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +37 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +46 -0
- data/sig/generated/lib/solid_objects/reference.rbs +45 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +55 -0
- data/sig/generated/lib/solid_objects/serialization.rbs +31 -0
- data/sig/generated/lib/solid_objects/state.rbs +72 -0
- data/sig/generated/lib/solid_objects/stream_name.rbs +11 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +19 -0
- data/sig/generated/lib/solid_objects/supervisor.rbs +38 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +14 -0
- data/sig/generated/lib/solid_objects/version.rbs +5 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +24 -0
- data/sig/generated/lib/solid_objects/worker.rbs +56 -0
- data/sig/generated/lib/solid_objects.rbs +41 -0
- data/sig/generated/models/solid_objects/broadcast.rbs +6 -0
- data/sig/generated/models/solid_objects/claimed_message.rbs +6 -0
- data/sig/generated/models/solid_objects/dead_letter.rbs +6 -0
- data/sig/generated/models/solid_objects/effect.rbs +6 -0
- data/sig/generated/models/solid_objects/instance.rbs +25 -0
- data/sig/generated/models/solid_objects/message.rbs +22 -0
- data/sig/generated/models/solid_objects/process.rbs +6 -0
- data/sig/generated/models/solid_objects/ready_message.rbs +6 -0
- data/sig/generated/models/solid_objects/record.rbs +8 -0
- data/sig/generated/models/solid_objects/reminder.rbs +6 -0
- data/sig/support/framework.rbs +37 -0
- metadata +467 -0
data/README.md
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
# Solid Objects
|
|
2
|
+
|
|
3
|
+
[](https://github.com/cardmagic/solid_objects/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
**Cloudflare Durable Objects, ported to Rails.**
|
|
6
|
+
|
|
7
|
+
Solid Objects brings the Durable Objects programming model—addressable objects,
|
|
8
|
+
durable state, serialized turns, alarms, and live clients—to ordinary Rails
|
|
9
|
+
applications. It runs on the MySQL, PostgreSQL, or SQLite database the
|
|
10
|
+
application already has, following the database-backed operating model of the
|
|
11
|
+
Solid family. No Redis, Cloudflare account, or separate actor service is
|
|
12
|
+
required.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
class Counter < SolidObjects::Actor
|
|
16
|
+
attribute :value, default: 0
|
|
17
|
+
|
|
18
|
+
def increment(amount: 1)
|
|
19
|
+
self.value += amount
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# from anywhere in your app — addressed by name:
|
|
24
|
+
Counter.ref("global").increment(amount: 5)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`Counter / global` is a logical identity. Like a Durable Object named with
|
|
28
|
+
`idFromName`, it can be addressed from anywhere without first creating or
|
|
29
|
+
locating a Ruby object. Solid Objects activates it when work arrives, commits
|
|
30
|
+
its ordered turns one at a time, persists its state, and deactivates it when
|
|
31
|
+
idle. Different identities can run concurrently.
|
|
32
|
+
|
|
33
|
+
This is a port of the programming model, not Cloudflare's edge runtime or
|
|
34
|
+
platform. Read the conceptual overview at [solidobjects.dev](https://solidobjects.dev/)
|
|
35
|
+
and the exact Rails guarantees in [Correctness and delivery semantics](docs/correctness.md).
|
|
36
|
+
|
|
37
|
+
Version 0.1 is an early release. Its correctness core is implemented and tested,
|
|
38
|
+
but the project does not yet claim production readiness. See
|
|
39
|
+
[Status](#status) and the [roadmap](docs/roadmap.md).
|
|
40
|
+
|
|
41
|
+
## Table of contents
|
|
42
|
+
|
|
43
|
+
- [Cloudflare Durable Objects for Rails](#cloudflare-durable-objects-for-rails)
|
|
44
|
+
- [Reactive ERB](#reactive-erb)
|
|
45
|
+
- [Installation](#installation)
|
|
46
|
+
- [Defining an actor](#defining-an-actor)
|
|
47
|
+
- [Actor identity](#actor-identity)
|
|
48
|
+
- [Messages and queries](#messages-and-queries)
|
|
49
|
+
- [Effects](#effects)
|
|
50
|
+
- [Reminders](#reminders)
|
|
51
|
+
- [Destroying an object](#destroying-an-object)
|
|
52
|
+
- [State migrations](#state-migrations)
|
|
53
|
+
- [Configuration](#configuration)
|
|
54
|
+
- [Workers and operations](#workers-and-operations)
|
|
55
|
+
- [Database support](#database-support)
|
|
56
|
+
- [Guarantees](#guarantees)
|
|
57
|
+
- [When to use it](#when-to-use-it)
|
|
58
|
+
- [Comparisons](#comparisons)
|
|
59
|
+
- [Development](#development)
|
|
60
|
+
- [Status](#status)
|
|
61
|
+
- [License](#license)
|
|
62
|
+
|
|
63
|
+
## Cloudflare Durable Objects for Rails
|
|
64
|
+
|
|
65
|
+
Cloudflare Durable Objects combine a name, durable storage, serialized
|
|
66
|
+
execution, alarms, and live connections in one stateful object. Solid Objects
|
|
67
|
+
maps those ideas into Rails:
|
|
68
|
+
|
|
69
|
+
| Cloudflare Durable Objects | Solid Objects |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| Namespace plus `idFromName("id")` | Actor class plus `.ref("id")` |
|
|
72
|
+
| RPC method on a stub | Public Ruby method on a reference |
|
|
73
|
+
| Per-object transactional storage | Declared attributes in native JSON |
|
|
74
|
+
| Single-threaded input handling | Ordered mailbox plus fenced activation |
|
|
75
|
+
| Alarms API | Per-object `schedule` |
|
|
76
|
+
| WebSockets | Reactive ERB over Action Cable and Turbo Streams |
|
|
77
|
+
| Hibernation when idle | Idle activation deactivation |
|
|
78
|
+
| Storage deletion | Authorized `reference.destroy` |
|
|
79
|
+
| Cloudflare Workers platform | Your Rails processes and SQL database |
|
|
80
|
+
|
|
81
|
+
Rails already has excellent tools for jobs, records, and realtime transport.
|
|
82
|
+
None of those primitives alone provides this complete stateful-object shape.
|
|
83
|
+
Solid Objects adds five capabilities:
|
|
84
|
+
|
|
85
|
+
### Ordered delivery per identity
|
|
86
|
+
|
|
87
|
+
Every enqueue locks the actor instance and allocates an explicit, monotonically
|
|
88
|
+
increasing sequence number. An activation always takes the lowest live sequence
|
|
89
|
+
for that actor. A retryable failure keeps later messages blocked until the
|
|
90
|
+
failed message succeeds or reaches its dead letter.
|
|
91
|
+
|
|
92
|
+
This is stronger than a concurrency limit. Solid Queue's
|
|
93
|
+
[`limits_concurrency`](https://github.com/rails/solid_queue#concurrency-controls)
|
|
94
|
+
caps simultaneous executions sharing a key, but explicitly does not guarantee
|
|
95
|
+
their execution order. Solid Objects turns each actor identity into an ordered
|
|
96
|
+
mailbox.
|
|
97
|
+
|
|
98
|
+
### Fenced activation
|
|
99
|
+
|
|
100
|
+
A lease expiration by itself cannot stop a paused worker from resuming with
|
|
101
|
+
stale state. Solid Objects combines the lease owner with a monotonically
|
|
102
|
+
increasing activation generation. Every state commit verifies the current
|
|
103
|
+
owner, generation, unexpired database-time lease, and claimed-message
|
|
104
|
+
membership.
|
|
105
|
+
|
|
106
|
+
A stale worker may finish running Ruby code, but it cannot commit stale state,
|
|
107
|
+
complete the message, or publish outbox entries.
|
|
108
|
+
|
|
109
|
+
### Addressable objects with durable state
|
|
110
|
+
|
|
111
|
+
An actor is addressed by `(actor_type, actor_id)`, not by a process, thread, or
|
|
112
|
+
database row ID. Code anywhere in the application can refer to the same logical
|
|
113
|
+
cart, room, device, or workflow. Its JSON state survives worker restarts and
|
|
114
|
+
idle deactivation.
|
|
115
|
+
|
|
116
|
+
### Per-object alarms
|
|
117
|
+
|
|
118
|
+
Cloudflare Durable Objects give each object an alarm. Rails recurring schedules
|
|
119
|
+
are normally global task definitions. Solid Objects ports per-object alarms as
|
|
120
|
+
durable reminders owned by one logical identity:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
def schedule_expiration
|
|
124
|
+
schedule :expire, at: 30.minutes.from_now, arguments: {}
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
When due, a reminder becomes an ordinary mailbox message and follows the same
|
|
129
|
+
ordering, retry, lease, and fencing rules as every other turn.
|
|
130
|
+
|
|
131
|
+
### Durable Objects that render themselves
|
|
132
|
+
|
|
133
|
+
Cloudflare Durable Objects can coordinate WebSocket clients. Solid Objects adds
|
|
134
|
+
a Rails-native extension: an actor observable becomes a live Turbo target with
|
|
135
|
+
one helper call. The actor commit and durable broadcast outbox are atomic, so a
|
|
136
|
+
rolled-back state change cannot leak into the page.
|
|
137
|
+
|
|
138
|
+
## Reactive ERB
|
|
139
|
+
|
|
140
|
+
Define an observable:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
class ShoppingCart < SolidObjects::Actor
|
|
144
|
+
attribute :items, default: -> { [] }
|
|
145
|
+
|
|
146
|
+
observable :items_count do
|
|
147
|
+
items.sum { |item| item.fetch("quantity") }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Render it:
|
|
153
|
+
|
|
154
|
+
```erb
|
|
155
|
+
<%= solid_object current_cart do |cart| %>
|
|
156
|
+
Cart items: <%= cart.items_count %>
|
|
157
|
+
<% end %>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
That template provides initial server rendering, a stable opaque DOM target,
|
|
161
|
+
and live Turbo replacements after committed actor turns. One `solid_object`
|
|
162
|
+
block makes one Action Cable subscription for all values inside it, and Action
|
|
163
|
+
Cable multiplexes subscriptions over the browser's WebSocket.
|
|
164
|
+
|
|
165
|
+
No client-side state store, custom Stimulus controller, channel class, manual
|
|
166
|
+
broadcast, or one-WebSocket-per-value setup is required. Signed stream tokens
|
|
167
|
+
protect integrity, an application policy authorizes every subscription,
|
|
168
|
+
broadcasts are delivered from a durable outbox, and reconnecting clients
|
|
169
|
+
refresh from current actor state.
|
|
170
|
+
|
|
171
|
+
`cart.component(:summary)` supports initial rendering of
|
|
172
|
+
`actors/shopping_cart/_summary`. Durable live component replacement and
|
|
173
|
+
Turbo append actions are roadmap work; observable replacement is the live path
|
|
174
|
+
implemented in 0.1.
|
|
175
|
+
|
|
176
|
+
Reactive views require `turbo-rails` and a working Action Cable adapter in the
|
|
177
|
+
host application. They are optional; the actor runtime itself does not depend
|
|
178
|
+
on Turbo.
|
|
179
|
+
|
|
180
|
+
## Installation
|
|
181
|
+
|
|
182
|
+
Solid Objects requires Ruby 3.3 or newer and Rails 8.0 or newer.
|
|
183
|
+
|
|
184
|
+
Add the gem, install its initializer and migration, then migrate:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
bundle add solid_objects
|
|
188
|
+
bin/rails generate solid_objects:install
|
|
189
|
+
bin/rails db:migrate
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The generated initializer denies all externally initiated operations. Replace
|
|
193
|
+
the policy blocks with application-specific authorization before sending
|
|
194
|
+
messages, querying state, destroying actors, subscribing to streams, or
|
|
195
|
+
mounting administration routes:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
SolidObjects.configure do |configuration|
|
|
199
|
+
configuration.authorize_message = ->(**) { false }
|
|
200
|
+
configuration.authorize_query = ->(**) { false }
|
|
201
|
+
configuration.authorize_destroy = ->(**) { false }
|
|
202
|
+
configuration.authorize_subscription = ->(**) { false }
|
|
203
|
+
configuration.authorize_administration = ->(**) { false }
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Knowledge of an actor ID or signed stream token is never authorization.
|
|
208
|
+
|
|
209
|
+
Start the runtime:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
bundle exec solid_objects start
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
The engine uses the application's primary Active Record connection by default.
|
|
216
|
+
See [Database support](#database-support) for a separate database configuration.
|
|
217
|
+
|
|
218
|
+
## Defining an actor
|
|
219
|
+
|
|
220
|
+
The Durable Object class becomes an ordinary Ruby class:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
class ShoppingCart < SolidObjects::Actor
|
|
224
|
+
attribute :items, default: -> { [] }
|
|
225
|
+
attribute :checkout_status, default: "open"
|
|
226
|
+
|
|
227
|
+
def add_item(product_id:, quantity: 1)
|
|
228
|
+
item = items.find do |candidate|
|
|
229
|
+
candidate.fetch("product_id") == product_id
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if item
|
|
233
|
+
item["quantity"] += quantity
|
|
234
|
+
else
|
|
235
|
+
items << {
|
|
236
|
+
"product_id" => product_id,
|
|
237
|
+
"quantity" => quantity
|
|
238
|
+
}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
observable :items_count do
|
|
243
|
+
items.sum { |item| item.fetch("quantity") }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Class-level `attribute` declarations are the per-object durable storage schema
|
|
249
|
+
and generate actor instance readers and writers. Public instance methods
|
|
250
|
+
declared on the actor are durable message handlers. They can use `items`,
|
|
251
|
+
`self.checkout_status = "pending"`, or the lower-level `state` object. Declare
|
|
252
|
+
helper methods as private or protected so they are not exposed as messages.
|
|
253
|
+
|
|
254
|
+
Attributes also become ordered read queries on a reference. Declared messages
|
|
255
|
+
become asynchronous methods:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
cart = ShoppingCart.ref("alice")
|
|
259
|
+
message = cart.add_item(product_id: "shirt-123", quantity: 2)
|
|
260
|
+
items = cart.items
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
`message` is a `SolidObjects::MessageReference`. `items` is a deeply frozen
|
|
264
|
+
JSON snapshot; mutating it cannot bypass the actor mailbox. State changes must
|
|
265
|
+
go through public actor message methods.
|
|
266
|
+
|
|
267
|
+
State, arguments, results, effects, and reminder arguments accept
|
|
268
|
+
JSON-compatible values. Solid Objects never deserializes Ruby `Marshal` data.
|
|
269
|
+
|
|
270
|
+
Lifecycle hooks are also available:
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
class DeviceActor < SolidObjects::Actor
|
|
274
|
+
on_activate do
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
on_deactivate do
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Hooks should be deterministic and must not perform slow network I/O. See the
|
|
283
|
+
[architecture](docs/architecture.md) for their persistence semantics.
|
|
284
|
+
|
|
285
|
+
## Actor identity
|
|
286
|
+
|
|
287
|
+
The durable identity is:
|
|
288
|
+
|
|
289
|
+
```text
|
|
290
|
+
actor_type + actor_id
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`actor_type` is inferred from the Ruby class name, so the normal API needs no
|
|
294
|
+
declaration. The pair plays the role of a Durable Objects namespace and object
|
|
295
|
+
name:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
ShoppingCart.ref("alice")
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Use an explicit stable type when the persisted name should be independent of a
|
|
302
|
+
future Ruby constant rename:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
class ShoppingCart < SolidObjects::Actor
|
|
306
|
+
actor_type "shopping_cart"
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Actor types resolve only through the explicit registry. Solid Objects never
|
|
311
|
+
constantizes a type supplied by a client.
|
|
312
|
+
|
|
313
|
+
## Messages and queries
|
|
314
|
+
|
|
315
|
+
As with a Durable Object stub, declared actor operations are available directly
|
|
316
|
+
on a reference:
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
class Counter < SolidObjects::Actor
|
|
320
|
+
attribute :value, default: 0
|
|
321
|
+
|
|
322
|
+
def increment(amount: 1)
|
|
323
|
+
self.value += amount
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
counter = Counter.ref("global")
|
|
328
|
+
message = counter.increment(amount: 5)
|
|
329
|
+
value = counter.value
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Public actor methods are messages and become asynchronous syntax over `tell`,
|
|
333
|
+
returning a durable `MessageReference`. Declared queries and attribute readers
|
|
334
|
+
are synchronous syntax over `ask`. Unlike Cloudflare RPC, the initial `ask`
|
|
335
|
+
implementation waits by polling the durable database row.
|
|
336
|
+
The `message(:name) { ... }` DSL remains available for dynamic definitions.
|
|
337
|
+
The explicit `tell` and `ask` forms remain available for dynamic operation
|
|
338
|
+
names or names that collide with Ruby or reference methods.
|
|
339
|
+
|
|
340
|
+
### `tell`
|
|
341
|
+
|
|
342
|
+
`tell` durably enqueues work and immediately returns a message reference:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
message = order.tell(
|
|
346
|
+
:submit,
|
|
347
|
+
idempotency_key: "submit-order-123"
|
|
348
|
+
)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Use `available_at:` to spread bulk work or delay one message:
|
|
352
|
+
|
|
353
|
+
```ruby
|
|
354
|
+
order.tell(:evaluate, available_at: 10.minutes.from_now)
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### `ask`
|
|
358
|
+
|
|
359
|
+
`ask` durably enqueues a query or message and waits for its result:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
status = order.ask(:status, timeout: 5.seconds)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The initial cross-process implementation polls the durable message row. It is
|
|
366
|
+
intended for background callers and control paths, not latency-sensitive HTTP
|
|
367
|
+
request handling. A timeout does not cancel the actor message.
|
|
368
|
+
|
|
369
|
+
Actor code cannot call `ask`; synchronous actor-to-actor waits can deadlock in
|
|
370
|
+
cycles. Use `tell` or `send_to` and a result message.
|
|
371
|
+
|
|
372
|
+
### Redelivery
|
|
373
|
+
|
|
374
|
+
Sequential does not mean once. A handler can run again after a process crash or
|
|
375
|
+
lease loss, so guard logical transitions in durable actor state:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
def launch
|
|
379
|
+
return if status == "launched"
|
|
380
|
+
|
|
381
|
+
self.status = "launched"
|
|
382
|
+
emit :launch_vehicle, launch_id: actor_id
|
|
383
|
+
end
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
External systems must also deduplicate effects using the stable effect ID.
|
|
387
|
+
|
|
388
|
+
## Effects
|
|
389
|
+
|
|
390
|
+
Cloudflare Durable Objects can call external services directly. Solid Objects
|
|
391
|
+
does not hold a Rails database transaction across slow external I/O. `emit`
|
|
392
|
+
creates a transactional outbox entry alongside state and message completion:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
def checkout(payment_id:, amount_cents:)
|
|
396
|
+
return unless checkout_status == "open"
|
|
397
|
+
|
|
398
|
+
self.checkout_status = "pending"
|
|
399
|
+
emit(
|
|
400
|
+
:charge_payment,
|
|
401
|
+
payment_id:,
|
|
402
|
+
amount_cents:,
|
|
403
|
+
on_success: :payment_succeeded,
|
|
404
|
+
on_failure: :payment_failed
|
|
405
|
+
)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def payment_succeeded(effect_id:, result:)
|
|
409
|
+
self.checkout_status = "paid"
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def payment_failed(effect_id:, error:)
|
|
413
|
+
self.checkout_status = "failed"
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Register an effect handler during application boot:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
SolidObjects.register_effect(:charge_payment) do |arguments, context|
|
|
421
|
+
Payments.charge(
|
|
422
|
+
idempotency_key: context.id,
|
|
423
|
+
payment_id: arguments.fetch("payment_id"),
|
|
424
|
+
amount_cents: arguments.fetch("amount_cents")
|
|
425
|
+
)
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The provider call can repeat if a process dies after external success but
|
|
430
|
+
before recording completion. The stable effect ID is the idempotency key.
|
|
431
|
+
|
|
432
|
+
## Reminders
|
|
433
|
+
|
|
434
|
+
Reminders are Solid Objects' durable equivalent of the Durable Objects Alarms
|
|
435
|
+
API. One-shot and recurring alarms are actor-owned database records:
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
def schedule_evaluation
|
|
439
|
+
schedule :evaluate, at: 1.hour.from_now, every: 1.hour, missed: :latest
|
|
440
|
+
end
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Use `missed: :latest` to coalesce missed occurrences or `missed: :all` to
|
|
444
|
+
enqueue each one.
|
|
445
|
+
|
|
446
|
+
Self-scheduling actors should also have a low-frequency application reconciler.
|
|
447
|
+
It may read `SolidObjects::Instance.states_for`, `.without_pending_work`, and
|
|
448
|
+
`.orphaned`, but every repair must go through `tell`. Never bulk-update actor
|
|
449
|
+
state around the lease and fencing checks.
|
|
450
|
+
|
|
451
|
+
## Destroying an object
|
|
452
|
+
|
|
453
|
+
Destroy an actor incarnation through its reference:
|
|
454
|
+
|
|
455
|
+
```ruby
|
|
456
|
+
Counter.ref("global").destroy
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`destroy` is synchronous and idempotent. It returns `true` when it deletes an
|
|
460
|
+
existing incarnation and `false` when none exists. In one transaction it locks
|
|
461
|
+
and deletes the actor instance; cascading foreign keys remove state, message
|
|
462
|
+
history, ready and claimed mailbox rows, dead letters, reminders, effects, and
|
|
463
|
+
broadcasts.
|
|
464
|
+
|
|
465
|
+
Destruction has its own deny-by-default `authorize_destroy` policy and cannot be
|
|
466
|
+
called synchronously from actor code. It does not run `on_deactivate`. A stale
|
|
467
|
+
activation cannot commit after deletion because its fenced write targets the
|
|
468
|
+
deleted instance primary key. Addressing the same type and ID later creates a
|
|
469
|
+
fresh incarnation with default state and message sequence 1.
|
|
470
|
+
|
|
471
|
+
Pending outboxes are deleted. An external effect, actor-to-actor delivery, or
|
|
472
|
+
broadcast that already started cannot be recalled, but its stale completion
|
|
473
|
+
cannot enqueue a callback or recreate the source actor. See
|
|
474
|
+
[destruction semantics](docs/correctness.md#destruction) before using deletion
|
|
475
|
+
as application workflow.
|
|
476
|
+
|
|
477
|
+
## State migrations
|
|
478
|
+
|
|
479
|
+
Actor state has an independent schema version:
|
|
480
|
+
|
|
481
|
+
```ruby
|
|
482
|
+
class ShoppingCart < SolidObjects::Actor
|
|
483
|
+
state_version 2
|
|
484
|
+
|
|
485
|
+
migrate_state from: 1, to: 2 do |state|
|
|
486
|
+
state["currency"] ||= "USD"
|
|
487
|
+
state
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
An actor refuses activation when stored state is newer than the running code.
|
|
493
|
+
Published migration blocks cannot be squashed because a long-idle actor may
|
|
494
|
+
still hold an old representation. Destructive changes need an expand/contract
|
|
495
|
+
rolling deployment. Read the [state migration guide](docs/state-migrations.md)
|
|
496
|
+
before changing persisted state.
|
|
497
|
+
|
|
498
|
+
## Configuration
|
|
499
|
+
|
|
500
|
+
Configure Solid Objects in `config/initializers/solid_objects.rb`:
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
SolidObjects.configure do |configuration|
|
|
504
|
+
configuration.worker_count = 4
|
|
505
|
+
configuration.lease_duration = 30.seconds
|
|
506
|
+
configuration.lease_renewal_interval = 10.seconds
|
|
507
|
+
configuration.max_messages_per_activation_pass = 50
|
|
508
|
+
configuration.max_activation_duration = 5.seconds
|
|
509
|
+
end
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Important defaults:
|
|
513
|
+
|
|
514
|
+
| Setting | Default |
|
|
515
|
+
| --- | ---: |
|
|
516
|
+
| `polling_interval` | 0.1 seconds |
|
|
517
|
+
| `ask_polling_interval` | 0.05 seconds |
|
|
518
|
+
| `lease_duration` | 30 seconds |
|
|
519
|
+
| `lease_renewal_interval` | 10 seconds |
|
|
520
|
+
| `idle_deactivation_timeout` | 30 seconds |
|
|
521
|
+
| `max_messages_per_activation_pass` | 50 |
|
|
522
|
+
| `max_activation_duration` | 5 seconds |
|
|
523
|
+
| `max_mailbox_length` | 10,000 |
|
|
524
|
+
| `max_attempts` | 5 |
|
|
525
|
+
| `process_heartbeat_interval` | 15 seconds |
|
|
526
|
+
| `process_alive_threshold` | 60 seconds |
|
|
527
|
+
| `worker_count` | 1 |
|
|
528
|
+
| `effect_worker_count` | 1 |
|
|
529
|
+
| `broadcast_worker_count` | 1 |
|
|
530
|
+
| `reminder_scheduler_count` | 1 |
|
|
531
|
+
|
|
532
|
+
Payload, state, and result limits; retry delay; table prefix; logging; wake-up;
|
|
533
|
+
broadcast; database; and authorization adapters are also configurable. Invalid
|
|
534
|
+
lease intervals, component counts, and size limits fail fast at boot.
|
|
535
|
+
|
|
536
|
+
## Workers and operations
|
|
537
|
+
|
|
538
|
+
`solid_objects start` runs actor, effect, reminder, and broadcast roles under
|
|
539
|
+
one supervisor:
|
|
540
|
+
|
|
541
|
+
```bash
|
|
542
|
+
bundle exec solid_objects start
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
Worker and outbox counts can be overridden:
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
bundle exec solid_objects start \
|
|
549
|
+
--workers 4 \
|
|
550
|
+
--effect-workers 2 \
|
|
551
|
+
--broadcast-workers 2 \
|
|
552
|
+
--reminder-schedulers 1
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Administration commands require the administration policy:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
bundle exec solid_objects status
|
|
559
|
+
bundle exec solid_objects cleanup
|
|
560
|
+
bundle exec solid_objects dead_letters
|
|
561
|
+
bundle exec solid_objects retry_dead_letter 123
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
The supervisor stops new claims, drains active loops, releases cached leases,
|
|
565
|
+
and marks process rows stopped on graceful shutdown. A hard-killed worker's
|
|
566
|
+
claimed turn is recovered after its process heartbeat or activation lease
|
|
567
|
+
becomes stale.
|
|
568
|
+
|
|
569
|
+
See the [operations guide](docs/operations.md) for monitoring, reconciliation,
|
|
570
|
+
shutdown, retention, and backup guidance.
|
|
571
|
+
|
|
572
|
+
## Database support
|
|
573
|
+
|
|
574
|
+
Solid Objects supports:
|
|
575
|
+
|
|
576
|
+
- PostgreSQL 14 or newer
|
|
577
|
+
- MySQL 8.0 or newer using InnoDB
|
|
578
|
+
- SQLite 3.35 or newer
|
|
579
|
+
|
|
580
|
+
PostgreSQL and MySQL use `FOR UPDATE SKIP LOCKED` when claiming hot-table rows.
|
|
581
|
+
SQLite uses its serialized writer behavior. All three adapters run the same
|
|
582
|
+
locking, fencing, mailbox, outbox, and engine integration test suite.
|
|
583
|
+
|
|
584
|
+
No Redis or Kafka service is required.
|
|
585
|
+
|
|
586
|
+
By default, actor tables use the application's Active Record connection. A
|
|
587
|
+
separate database role is optional:
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
SolidObjects.configure do |configuration|
|
|
591
|
+
configuration.connects_to = {
|
|
592
|
+
database: {
|
|
593
|
+
writing: :actors,
|
|
594
|
+
reading: :actors
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
end
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Every table participating in an actor commit must share one database.
|
|
601
|
+
|
|
602
|
+
Completed message history lives in `solid_objects_messages`, while ready and
|
|
603
|
+
claimed work lives in small membership tables. Polling indexes stay
|
|
604
|
+
proportional to live work, and no partial indexes are required.
|
|
605
|
+
|
|
606
|
+
## Guarantees
|
|
607
|
+
|
|
608
|
+
For one actor identity, messages are:
|
|
609
|
+
|
|
610
|
+
- durably enqueued with explicit sequence numbers;
|
|
611
|
+
- processed sequentially in sequence order;
|
|
612
|
+
- delivered at least once; and
|
|
613
|
+
- committed by at most one valid activation owner and fencing generation.
|
|
614
|
+
|
|
615
|
+
Different actor identities may execute concurrently.
|
|
616
|
+
|
|
617
|
+
Actor destruction is authorized, synchronous, and linearized by the instance
|
|
618
|
+
row lock. It removes the current incarnation and all actor-owned durable rows.
|
|
619
|
+
A later message may create a fresh incarnation of the same logical identity.
|
|
620
|
+
|
|
621
|
+
The following writes are atomic for one successful turn:
|
|
622
|
+
|
|
623
|
+
- actor state and state version;
|
|
624
|
+
- message result and completion;
|
|
625
|
+
- effect outbox entries;
|
|
626
|
+
- reminder changes;
|
|
627
|
+
- actor-to-actor messages; and
|
|
628
|
+
- observable broadcast entries.
|
|
629
|
+
|
|
630
|
+
Solid Objects does not promise:
|
|
631
|
+
|
|
632
|
+
- exactly-once handler or effect execution;
|
|
633
|
+
- global order across actors;
|
|
634
|
+
- distributed transactions;
|
|
635
|
+
- bounded end-to-end latency;
|
|
636
|
+
- cancellation when an `ask` caller times out; or
|
|
637
|
+
- that a lease prevents stale Ruby code from continuing to run.
|
|
638
|
+
|
|
639
|
+
The fencing generation prevents stale code from committing.
|
|
640
|
+
|
|
641
|
+
Read [Correctness and delivery semantics](docs/correctness.md) for the full
|
|
642
|
+
contract and crash matrix.
|
|
643
|
+
|
|
644
|
+
## When to use it
|
|
645
|
+
|
|
646
|
+
Solid Objects fits the same coordination-heavy domains that lead developers to
|
|
647
|
+
Cloudflare Durable Objects, when the application belongs in Rails and its
|
|
648
|
+
existing database:
|
|
649
|
+
|
|
650
|
+
- shopping carts;
|
|
651
|
+
- chat rooms and presence;
|
|
652
|
+
- device twins;
|
|
653
|
+
- user-specific schedules;
|
|
654
|
+
- long-lived workflows;
|
|
655
|
+
- collaborative sessions; and
|
|
656
|
+
- game rooms.
|
|
657
|
+
|
|
658
|
+
Do not use it for stateless work, bulk pipelines, CPU-heavy computation,
|
|
659
|
+
cross-actor transactions, slow network calls inside handlers, or domains that
|
|
660
|
+
are clearer as normalized Active Record models and direct service objects.
|
|
661
|
+
|
|
662
|
+
## Comparisons
|
|
663
|
+
|
|
664
|
+
| Tool | What Solid Objects adds or changes |
|
|
665
|
+
| --- | --- |
|
|
666
|
+
| Cloudflare Durable Objects | Solid Objects ports the named, stateful, serialized-object model to Ruby and Rails. It uses your SQL database and Rails workers rather than Cloudflare's globally distributed serverless runtime, placement, and storage APIs. |
|
|
667
|
+
| Active Job | Jobs are independent work units. Solid Objects adds addressable identity, durable state, explicit per-identity order, activation leases, and fencing. |
|
|
668
|
+
| Solid Queue | Solid Queue is an excellent database backend for Active Job. Its concurrency controls cap overlap but do not guarantee order. Solid Objects provides actor mailboxes, state, fencing, per-identity reminders, and state-driven views. |
|
|
669
|
+
| Action Cable | Cable transports transient realtime messages. Solid Objects owns durable state and work; Cable is an optional delivery path for committed observable projections. |
|
|
670
|
+
| Orleans | Orleans provides the virtual-actor lineage behind the model, with grains, reminders, and activation lifecycle. Solid Objects is a smaller Rails-native runtime and does not match Orleans clustering or placement breadth. |
|
|
671
|
+
| Active Record service object | A service object runs directly against records. Solid Objects adds durable asynchronous ordering, retries, activation fencing, reminders, and outboxes at greater operational cost. |
|
|
672
|
+
|
|
673
|
+
## Development
|
|
674
|
+
|
|
675
|
+
Solid Objects uses Minitest and follows Solid Queue's test organization and
|
|
676
|
+
RuboCop policy. Ruby source carries inline RBS annotations.
|
|
677
|
+
|
|
678
|
+
Run the full SQLite suite and static checks:
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
bundle install
|
|
682
|
+
bundle exec rake
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Run the database integration suite against PostgreSQL or MySQL:
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
SOLID_OBJECTS_DATABASE_URL=postgresql://localhost/solid_objects_test \
|
|
689
|
+
bundle exec rake test
|
|
690
|
+
|
|
691
|
+
SOLID_OBJECTS_DATABASE_URL=mysql2://localhost/solid_objects_test \
|
|
692
|
+
bundle exec rake test
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
Concurrency tests use real database locks and deterministic synchronization,
|
|
696
|
+
not mocked locking behavior.
|
|
697
|
+
|
|
698
|
+
See the [development guide](docs/development.md) and
|
|
699
|
+
[local benchmarks](docs/benchmarks.md).
|
|
700
|
+
|
|
701
|
+
## Status
|
|
702
|
+
|
|
703
|
+
Implemented and tested in 0.1:
|
|
704
|
+
|
|
705
|
+
- Rails engine, install generator, migrations, and `solid_objects` executable;
|
|
706
|
+
- actor registry, references, JSON state, and state migrations;
|
|
707
|
+
- durable message history plus ready and claimed membership tables;
|
|
708
|
+
- concurrent sequence allocation and actor creation;
|
|
709
|
+
- activation leases, renewal, fencing generations, and stale-write rejection;
|
|
710
|
+
- bounded activation passes, idle activation cache, and hot-actor fairness;
|
|
711
|
+
- retries, strict poison ordering, dead letters, and retry tooling;
|
|
712
|
+
- transactional effects and asynchronous actor-to-actor messages;
|
|
713
|
+
- one-shot and recurring per-actor reminders;
|
|
714
|
+
- authorized actor destruction with fenced stale-write rejection and cascading
|
|
715
|
+
durable-work cleanup;
|
|
716
|
+
- durable observable broadcasts and authorized Action Cable refresh;
|
|
717
|
+
- process registration, heartbeats, cleanup, and graceful shutdown; and
|
|
718
|
+
- SQLite, PostgreSQL, and MySQL integration tests.
|
|
719
|
+
|
|
720
|
+
Partially implemented:
|
|
721
|
+
|
|
722
|
+
- the supervisor starts and drains roles but does not replace a crashed role or
|
|
723
|
+
run periodic maintenance automatically;
|
|
724
|
+
- cross-process wake-up uses polling; PostgreSQL notifications and optional
|
|
725
|
+
Redis acceleration are not implemented;
|
|
726
|
+
- live observable replacement works, while live component replacement and
|
|
727
|
+
Turbo append actions remain future work;
|
|
728
|
+
- local admission limits exist, but distributed rate limits and global
|
|
729
|
+
admission control do not; and
|
|
730
|
+
- administration views exist, but retention automation and richer audit tools
|
|
731
|
+
do not.
|
|
732
|
+
|
|
733
|
+
Production readiness requires hardening and operational soak evidence. The
|
|
734
|
+
[roadmap](docs/roadmap.md) tracks that work.
|
|
735
|
+
|
|
736
|
+
## License
|
|
737
|
+
|
|
738
|
+
Solid Objects is MIT Licensed by Lucas Carlson. See
|
|
739
|
+
[MIT-LICENSE](MIT-LICENSE).
|
|
740
|
+
|
|
741
|
+
Solid Objects is an independent open-source project. It is not affiliated with,
|
|
742
|
+
sponsored by, or endorsed by Cloudflare, Inc. “Cloudflare” and “Durable
|
|
743
|
+
Objects” are trademarks of Cloudflare, Inc. and are used here to identify the
|
|
744
|
+
programming model this gem ports to Rails.
|