solid_objects 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +261 -31
- data/app/controllers/solid_objects/components_controller.rb +78 -0
- data/app/helpers/solid_objects/actor_helper.rb +13 -4
- data/config/routes.rb +1 -0
- data/db/migrate/20260806000000_add_state_revision_to_solid_objects_instances.rb +12 -0
- data/docs/architecture.md +116 -23
- data/docs/authorization.md +53 -3
- data/docs/benchmarks.md +6 -2
- data/docs/correctness.md +45 -1
- data/docs/database-schema.md +18 -5
- data/docs/development.md +40 -0
- data/docs/fit.md +10 -2
- data/docs/migrating-existing-state.md +8 -1
- data/docs/operations.md +74 -36
- data/docs/realtime.md +106 -12
- data/docs/roadmap.md +14 -7
- data/docs/security.md +25 -3
- data/docs/state-migrations.md +2 -0
- data/examples/application/README.md +5 -4
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +1 -1
- data/examples/application/app/views/chat_rooms/show.html.erb +2 -2
- data/examples/application/config/initializers/solid_objects.rb +9 -3
- data/lib/generators/solid_objects/templates/solid_objects.rb +33 -2
- data/lib/solid_objects/activation.rb +33 -4
- data/lib/solid_objects/actor.rb +58 -6
- data/lib/solid_objects/actor_channel.rb +73 -5
- data/lib/solid_objects/actor_definition.rb +2 -0
- data/lib/solid_objects/actor_snapshot.rb +35 -10
- data/lib/solid_objects/actor_view.rb +103 -10
- data/lib/solid_objects/application_write_guard.rb +24 -0
- data/lib/solid_objects/caller_process.rb +28 -0
- data/lib/solid_objects/cli.rb +44 -5
- data/lib/solid_objects/client.rb +98 -5
- data/lib/solid_objects/commit_action_registry.rb +42 -0
- data/lib/solid_objects/component_path_resolver.rb +27 -0
- data/lib/solid_objects/component_registration.rb +117 -0
- data/lib/solid_objects/component_renderer.rb +82 -0
- data/lib/solid_objects/component_subscriptions.rb +109 -0
- data/lib/solid_objects/component_token.rb +139 -0
- data/lib/solid_objects/component_view.rb +67 -0
- data/lib/solid_objects/configuration.rb +39 -1
- data/lib/solid_objects/database_adapter.rb +28 -1
- data/lib/solid_objects/database_adapters/mysql.rb +46 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +40 -0
- data/lib/solid_objects/errors.rb +156 -0
- data/lib/solid_objects/executor.rb +65 -4
- data/lib/solid_objects/instance_pruner.rb +97 -0
- data/lib/solid_objects/message_pruner.rb +97 -0
- data/lib/solid_objects/message_reference.rb +9 -0
- data/lib/solid_objects/process_pruner.rb +49 -0
- data/lib/solid_objects/reference.rb +5 -0
- data/lib/solid_objects/state_snapshot.rb +41 -0
- data/lib/solid_objects/stream_token.rb +32 -13
- data/lib/solid_objects/sync_deadline.rb +57 -0
- data/lib/solid_objects/sync_diagnostics.rb +133 -0
- data/lib/solid_objects/synchronous_invocation.rb +26 -7
- data/lib/solid_objects/test_helper.rb +78 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +45 -1
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/worker.rb +1 -1
- data/lib/solid_objects.rb +40 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +22 -0
- data/sig/generated/lib/solid_objects/activation.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor.rbs +29 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +20 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +17 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +31 -2
- data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
- data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
- data/sig/generated/lib/solid_objects/cli.rbs +11 -2
- data/sig/generated/lib/solid_objects/client.rbs +15 -0
- data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
- data/sig/generated/lib/solid_objects/component_path_resolver.rbs +13 -0
- data/sig/generated/lib/solid_objects/component_registration.rbs +51 -0
- data/sig/generated/lib/solid_objects/component_renderer.rbs +39 -0
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +40 -0
- data/sig/generated/lib/solid_objects/component_token.rbs +38 -0
- data/sig/generated/lib/solid_objects/component_view.rbs +43 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +35 -7
- data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +13 -0
- data/sig/generated/lib/solid_objects/errors.rbs +130 -0
- data/sig/generated/lib/solid_objects/executor.rbs +12 -0
- data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
- data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
- data/sig/generated/lib/solid_objects/reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +9 -4
- data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
- data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +10 -0
- data/sig/generated/lib/solid_objects.rbs +12 -0
- metadata +34 -1
data/docs/operations.md
CHANGED
|
@@ -34,6 +34,9 @@ Process inspection, cleanup, dead-letter inspection, and retry all require an
|
|
|
34
34
|
administration policy that authorizes the CLI context:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
+
bundle exec solid_objects prune_messages
|
|
38
|
+
bundle exec solid_objects prune_instances
|
|
39
|
+
bundle exec solid_objects prune_processes
|
|
37
40
|
bundle exec solid_objects dead_letters
|
|
38
41
|
bundle exec solid_objects retry_dead_letter 123
|
|
39
42
|
```
|
|
@@ -56,6 +59,9 @@ Important controls include:
|
|
|
56
59
|
- payload, state, and result byte limits
|
|
57
60
|
- retry attempts and delay
|
|
58
61
|
- heartbeat interval and alive threshold
|
|
62
|
+
- message retention and per-actor-type overrides
|
|
63
|
+
- opt-in actor-instance retention by actor type
|
|
64
|
+
- stopped-process retention and prune batch size
|
|
59
65
|
|
|
60
66
|
Keep lease duration comfortably above renewal interval and expected database
|
|
61
67
|
pause time. A handler can exceed the pass-duration budget because Ruby code is
|
|
@@ -126,49 +132,81 @@ Alert on:
|
|
|
126
132
|
- reconciliation drift;
|
|
127
133
|
- database lock waits, deadlocks, and SQLite busy errors.
|
|
128
134
|
|
|
135
|
+
## Instrumentation and logging
|
|
136
|
+
|
|
137
|
+
Active Support notifications use the `solid_objects.` prefix. Core events
|
|
138
|
+
include message enqueue/start/completion/failure/rejection, activation
|
|
139
|
+
claim/start/renew/release/deactivation failure, sync timeout/enqueue timeout/
|
|
140
|
+
transaction rejection, commit-action start/completion/failure, effect and
|
|
141
|
+
broadcast enqueue/completion, reminder enqueue, actor destruction/expiration,
|
|
142
|
+
retention pruning, process cleanup, and supervisor lifecycle.
|
|
143
|
+
|
|
144
|
+
Payloads contain stable runtime identifiers, actor identity, sequence,
|
|
145
|
+
attempts, ownership generations, and safe exception summaries where relevant.
|
|
146
|
+
Arguments, actor state, results, and outbox payloads are excluded. The bundled
|
|
147
|
+
log subscriber turns the same notifications into structured logger hashes.
|
|
148
|
+
|
|
129
149
|
## Retention and backups
|
|
130
150
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
volume, storage budget, audit needs, and the longest promised synchronous-result
|
|
135
|
-
lookup window.
|
|
151
|
+
Every actor call creates a durable message-history row, including queries and
|
|
152
|
+
attribute reads. The default retention policy keeps terminal message history
|
|
153
|
+
for 30 days and stopped process records for 7 days:
|
|
136
154
|
|
|
137
|
-
|
|
155
|
+
`reference.snapshot` is the explicit exception: it performs an authorized
|
|
156
|
+
current-state read without mailbox ordering or a message row.
|
|
138
157
|
|
|
139
158
|
```ruby
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
SolidObjects.configure do |configuration|
|
|
160
|
+
configuration.message_retention = 30.days
|
|
161
|
+
configuration.message_retention_by_actor_type = {
|
|
162
|
+
"AuditActor" => 365.days,
|
|
163
|
+
"EphemeralCounter" => 1.day
|
|
164
|
+
}
|
|
165
|
+
configuration.instance_retention_by_actor_type = {
|
|
166
|
+
"EphemeralCounter" => 30.days
|
|
167
|
+
}
|
|
168
|
+
configuration.process_retention = 7.days
|
|
169
|
+
configuration.prune_batch_size = 1_000
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Both pruning commands are dry-run previews by default:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
bundle exec solid_objects prune_messages
|
|
177
|
+
bundle exec solid_objects prune_instances
|
|
178
|
+
bundle exec solid_objects prune_processes
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
After reviewing the counts, execute bounded deletion:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bundle exec solid_objects prune_messages --execute
|
|
185
|
+
bundle exec solid_objects prune_instances --execute
|
|
186
|
+
bundle exec solid_objects prune_processes --execute
|
|
164
187
|
```
|
|
165
188
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
Message pruning keeps ready and claimed work, dead letters and their retry
|
|
190
|
+
links, messages with unfinished effects, and messages with undelivered
|
|
191
|
+
broadcasts. Deleting eligible history cascades to completed effects, delivered
|
|
192
|
+
broadcasts, and other message-owned rows. Choose a cutoff longer than every
|
|
193
|
+
`sync` timeout because a caller whose result row disappears can no longer
|
|
194
|
+
observe it.
|
|
195
|
+
|
|
196
|
+
Actor expiration is disabled by default. `prune_instances` considers only
|
|
197
|
+
actor types listed in `instance_retention_by_actor_type`, excludes active or
|
|
198
|
+
paused actors, and preserves ready/claimed mailbox work, scheduled reminders,
|
|
199
|
+
unfinished or dead outboxes, and dead letters. It locks and rechecks every
|
|
200
|
+
candidate before cascading deletion. Preview counts first, then schedule
|
|
201
|
+
`--execute` only after the application has accepted the loss of dormant state
|
|
202
|
+
and completed history.
|
|
203
|
+
|
|
204
|
+
Use authorized `reference.destroy` when deletion is an explicit application
|
|
205
|
+
operation rather than a retention policy.
|
|
206
|
+
|
|
207
|
+
Run stale-process `cleanup` before `prune_processes`. Normal caller processes
|
|
208
|
+
mark their registrations stopped at exit; hard kills remain recoverable through
|
|
209
|
+
heartbeat cleanup.
|
|
172
210
|
|
|
173
211
|
Back up actor tables with the same consistency guarantees as application data.
|
|
174
212
|
Restoring only instances without their mailboxes/outboxes, or vice versa, can
|
data/docs/realtime.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# Realtime integration
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Scalars and components
|
|
4
4
|
|
|
5
5
|
`solid_object` performs initial server rendering and emits one
|
|
6
6
|
`turbo-cable-stream-source` for the actor:
|
|
7
7
|
|
|
8
8
|
```erb
|
|
9
|
-
<%= solid_object ShoppingCartActor.ref(current_user.id)
|
|
9
|
+
<%= solid_object ShoppingCartActor.ref(current_user.id),
|
|
10
|
+
authorization_context: current_user do |cart| %>
|
|
10
11
|
Items: <%= cart.items_count %>
|
|
12
|
+
<%= cart.component :summary, observes: %i[items checkout_status] %>
|
|
11
13
|
<% end %>
|
|
12
14
|
```
|
|
13
15
|
|
|
@@ -15,9 +17,39 @@ Every observable gets a stable opaque DOM ID. Multiple values share the one
|
|
|
15
17
|
actor subscription and Action Cable multiplexes actor subscriptions over the
|
|
16
18
|
browser's physical WebSocket.
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
Scalar observable calls such as `cart.items_count` render stable `<span>`
|
|
21
|
+
targets. Their broadcast remains a direct escaped text replacement.
|
|
22
|
+
|
|
23
|
+
A reactive component declares one or more explicit observable dependencies.
|
|
24
|
+
`actor.component(:summary, observes: ...)` resolves the host partial by
|
|
25
|
+
convention at `actors/<actor_class>/_summary` and wraps it in a stable Turbo
|
|
26
|
+
Frame. Reactive components do not accept `partial:` because a client must
|
|
27
|
+
never influence partial resolution. The older
|
|
28
|
+
`actor.component(:summary, partial: "server/chosen/path")` form remains
|
|
29
|
+
available for initial-only static rendering.
|
|
30
|
+
|
|
31
|
+
The partial receives exactly two component locals:
|
|
32
|
+
|
|
33
|
+
- `actor`, which exposes the declared observables as deeply frozen ordinary
|
|
34
|
+
Ruby values plus `actor_id` and `reference`; and
|
|
35
|
+
- `authorization_context`, the context for this initial render or refresh.
|
|
36
|
+
|
|
37
|
+
`actor.state` is unavailable in reactive components. Reading an observable not
|
|
38
|
+
listed in `observes:` raises `UnknownComponentDependency`. This keeps
|
|
39
|
+
invalidation correct and prevents a partial from silently depending on state
|
|
40
|
+
that cannot wake it.
|
|
41
|
+
|
|
42
|
+
```erb
|
|
43
|
+
<ul>
|
|
44
|
+
<% actor.recent_messages.each do |message| %>
|
|
45
|
+
<li><%= message.fetch("body") %></li>
|
|
46
|
+
<% end %>
|
|
47
|
+
</ul>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Arrays, hashes, loops, conditionals, nested markup, and host helper output are
|
|
51
|
+
normal ERB. Escaping remains Action View's responsibility; Solid Objects never
|
|
52
|
+
marks actor strings as HTML safe.
|
|
21
53
|
|
|
22
54
|
## Authorization
|
|
23
55
|
|
|
@@ -26,18 +58,69 @@ it does not grant access. `ActorChannel` verifies the token, resolves the actor
|
|
|
26
58
|
through the registry, calls `authorize_subscription`, and streams only after
|
|
27
59
|
approval.
|
|
28
60
|
|
|
29
|
-
Initial
|
|
30
|
-
|
|
61
|
+
Initial scalar and component reads call `authorize_query` with the context
|
|
62
|
+
passed to `solid_object`. The refresh controller resolves a new request context
|
|
63
|
+
through `component_authorization_context`, then calls `authorize_query` again
|
|
64
|
+
for the component name and every declared dependency. The default resolver
|
|
65
|
+
supplies the engine controller; applications commonly resolve it to
|
|
66
|
+
`Current.user`:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
configuration.component_authorization_context = ->(controller:) { Current.user }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The three contexts are intentionally different:
|
|
73
|
+
|
|
74
|
+
| Boundary | Authorization context |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| Initial Action View render | Explicit `authorization_context:` passed to `solid_object` |
|
|
77
|
+
| Action Cable subscription | The authenticated Cable connection |
|
|
78
|
+
| Component refresh | Value returned by `component_authorization_context` for the engine controller request |
|
|
79
|
+
|
|
80
|
+
Do not substitute a signed token for any of them. Never authorize solely from
|
|
81
|
+
actor ID, token possession, stream name, component name, or DOM ID.
|
|
31
82
|
|
|
32
83
|
## Broadcast durability
|
|
33
84
|
|
|
34
85
|
The actor's fenced commit compares observables before and after the turn and
|
|
35
|
-
inserts one broadcast row per changed value.
|
|
36
|
-
|
|
37
|
-
|
|
86
|
+
inserts one broadcast row per changed value. The actor state, monotonic
|
|
87
|
+
`state_revision`, message completion, and broadcast rows commit atomically. A
|
|
88
|
+
rolled-back or fenced-out turn therefore cannot invalidate a component.
|
|
89
|
+
|
|
90
|
+
A broadcast process later sends small invalidation metadata and records
|
|
91
|
+
delivery. Its scalar Turbo replacement is transmitted only when that observable
|
|
92
|
+
target was rendered and signed into the scope's stream token. Component-only
|
|
93
|
+
dependencies therefore do not expose their serialized values over Cable. The
|
|
94
|
+
runtime never stores or broadcasts personalized component HTML. Each
|
|
95
|
+
authorized browser requests affected components through the engine endpoint
|
|
96
|
+
with its normal cookies. Responses are `private, no-store`.
|
|
97
|
+
|
|
98
|
+
Several changed dependencies from one message sequence produce one logical
|
|
99
|
+
refresh for a component. An unrelated observable does not refresh it. If a
|
|
100
|
+
newer invalidation arrives while a Turbo Frame request is in flight, the new
|
|
101
|
+
frame replaces the old frame element; the detached older response has no
|
|
102
|
+
current target.
|
|
38
103
|
|
|
39
104
|
If Cable delivery is lost, reconnecting `ActorChannel` transmits replacements
|
|
40
|
-
from current actor state.
|
|
105
|
+
from current actor state. It compares the signed component revision with the
|
|
106
|
+
latest `(instance_id, state_revision)` pair and refreshes stale components.
|
|
107
|
+
The incarnation ID handles destroy-and-recreate; the state revision handles
|
|
108
|
+
ordered commits within one incarnation. Out-of-order invalidations at or below
|
|
109
|
+
the last transmitted pair are ignored. The durable state row remains source of
|
|
110
|
+
truth.
|
|
111
|
+
|
|
112
|
+
The component endpoint rejects a requested revision newer than the committed
|
|
113
|
+
snapshot. This is a final server-side guard; browser safety primarily comes
|
|
114
|
+
from monotonic channel filtering and replacing the entire Turbo Frame
|
|
115
|
+
generation.
|
|
116
|
+
|
|
117
|
+
## Cost model
|
|
118
|
+
|
|
119
|
+
The durable row cost is unchanged: one broadcast row per changed observable,
|
|
120
|
+
containing its JSON value and the message/instance references needed to derive
|
|
121
|
+
invalidation metadata. No rendered document is stored. Each affected component
|
|
122
|
+
adds one authorized GET and one partial render per non-coalesced state
|
|
123
|
+
revision. Scalar observables remain the cheaper path for one text value.
|
|
41
124
|
|
|
42
125
|
## Deployment
|
|
43
126
|
|
|
@@ -48,4 +131,15 @@ require Redis.
|
|
|
48
131
|
|
|
49
132
|
Changing or removing observable names during a rolling deploy can strand old
|
|
50
133
|
broadcast rows or old DOM targets. Keep old names compatible until the outbox
|
|
51
|
-
and old pages have drained.
|
|
134
|
+
and old pages have drained. Keep component partial names and dependency
|
|
135
|
+
observables compatible across a rolling deploy for the same reason.
|
|
136
|
+
|
|
137
|
+
Reactive components require the engine mount because their signed refresh path
|
|
138
|
+
is generated from that mount. Applications with more than one engine mount can
|
|
139
|
+
set `component_path_resolver` to return the intended same-origin
|
|
140
|
+
`components_path`.
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# config/routes.rb
|
|
144
|
+
mount SolidObjects::Engine => "/solid_objects"
|
|
145
|
+
```
|
data/docs/roadmap.md
CHANGED
|
@@ -15,10 +15,17 @@
|
|
|
15
15
|
- Transactional effects with success/failure actor messages
|
|
16
16
|
- Actor-to-actor asynchronous outbox delivery
|
|
17
17
|
- One-shot and recurring reminders with `:latest` or `:all` catch-up
|
|
18
|
-
- Durable observable
|
|
18
|
+
- Durable observable invalidations, scalar Turbo replacement, and authorized
|
|
19
|
+
request-time ERB component refresh
|
|
19
20
|
- Reconciliation read APIs
|
|
20
21
|
- Installation doctor, authorization reference, fit guide, and legacy-state
|
|
21
22
|
migration cookbook
|
|
23
|
+
- Handler Active Record write isolation, same-database commit actions, ambient
|
|
24
|
+
transaction rejection, adapter lock/query deadlines, structured sync timeout
|
|
25
|
+
diagnostics, and result recovery
|
|
26
|
+
- Bounded message/process pruning, actor-type opt-in instance expiration,
|
|
27
|
+
graceful caller shutdown, committed state snapshots, and an opt-in Minitest
|
|
28
|
+
helper
|
|
22
29
|
- SQLite, PostgreSQL, and MySQL integration suites
|
|
23
30
|
- Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact
|
|
24
31
|
RuboCop policy, and a warning-free Brakeman scan
|
|
@@ -29,8 +36,9 @@
|
|
|
29
36
|
role or run periodic maintenance automatically.
|
|
30
37
|
- Wake-up strategy: in-process signaling plus durable polling and injection are
|
|
31
38
|
implemented; PostgreSQL `LISTEN/NOTIFY` and optional Redis adapters are not.
|
|
32
|
-
- Realtime:
|
|
33
|
-
|
|
39
|
+
- Realtime: scalar and dependency-driven ERB component replacement,
|
|
40
|
+
personalized refresh authorization, revision fencing, coalescing, and
|
|
41
|
+
reconnect convergence are implemented; Turbo append actions are not.
|
|
34
42
|
- Backpressure: mailbox/payload/state/result caps and fair yields exist;
|
|
35
43
|
distributed per-actor rate limits and global admission control do not.
|
|
36
44
|
- Administration: actor and dead-letter views plus policy hooks exist; richer
|
|
@@ -43,11 +51,10 @@
|
|
|
43
51
|
1. Add automatic supervisor role replacement and periodic dead-process cleanup.
|
|
44
52
|
2. Add PostgreSQL notification and optional Redis wake-up adapters with latency
|
|
45
53
|
benchmarks and polling-race tests.
|
|
46
|
-
3. Add
|
|
47
|
-
4. Add
|
|
54
|
+
3. Add result lookup by request ID and broader deadlock retry classification.
|
|
55
|
+
4. Add scheduled retention and stale-process maintenance.
|
|
48
56
|
5. Add database/server-version checks and MySQL InnoDB verification at boot.
|
|
49
|
-
6. Add
|
|
50
|
-
in a full browser.
|
|
57
|
+
6. Add Turbo append intents and expand reconnect coverage in a full browser.
|
|
51
58
|
7. Add distributed rate limits, global admission hooks, and cache-capacity
|
|
52
59
|
eviction.
|
|
53
60
|
8. Expand security scanning and run compatibility CI across supported Rails and
|
data/docs/security.md
CHANGED
|
@@ -13,9 +13,11 @@ risk for every hook and includes a tenant-aware policy example.
|
|
|
13
13
|
Method-style reference calls do not bypass these hooks. Public instance methods
|
|
14
14
|
declared on an actor are part of its remotely addressable message surface and
|
|
15
15
|
delegate to the authorized synchronous invocation path. Keep implementation
|
|
16
|
-
helpers private or protected. Query
|
|
17
|
-
query authorization policy. Explicit `async`
|
|
18
|
-
message authorization policy as direct calls.
|
|
16
|
+
helpers private or protected. Query, attribute, observable, and committed
|
|
17
|
+
`snapshot` reads use the separate query authorization policy. Explicit `async`
|
|
18
|
+
message delivery uses the same message authorization policy as direct calls.
|
|
19
|
+
Recovering a timed-out result through `MessageReference#wait` reauthorizes the
|
|
20
|
+
stored operation.
|
|
19
21
|
`reference.destroy` delegates to `authorize_destroy` before checking whether
|
|
20
22
|
the actor exists, so denial does not reveal actor existence.
|
|
21
23
|
|
|
@@ -54,10 +56,30 @@ host authentication and audit their use.
|
|
|
54
56
|
Instrumentation excludes arguments, state, results, and effect payloads by
|
|
55
57
|
default. Review custom logging and effect handlers for accidental disclosure.
|
|
56
58
|
|
|
59
|
+
## Handler database access
|
|
60
|
+
|
|
61
|
+
Handlers, observables, lifecycle hooks, and state migrations run with Active
|
|
62
|
+
Record writes prevented. They may query application records, but a direct
|
|
63
|
+
write becomes
|
|
64
|
+
`SolidObjects::ApplicationWriteForbidden` and dead-letters without retry.
|
|
65
|
+
This prevents application data from escaping a later actor failure or stale
|
|
66
|
+
fence.
|
|
67
|
+
|
|
68
|
+
Registered commit actions are privileged application code. They execute inside
|
|
69
|
+
the fenced actor transaction and receive stored JSON arguments, so register
|
|
70
|
+
only fixed names, validate record ownership again, and keep the block to
|
|
71
|
+
bounded database work. Never perform network I/O or authorize solely from a
|
|
72
|
+
record ID in commit-action arguments.
|
|
73
|
+
|
|
57
74
|
Actor destruction is not an administrative shortcut. Authorize tenancy and
|
|
58
75
|
ownership explicitly in `authorize_destroy`; knowledge of an actor ID is never
|
|
59
76
|
permission to delete its state or queued work.
|
|
60
77
|
|
|
78
|
+
Instance pruning is likewise destructive and requires administration
|
|
79
|
+
authorization. Only opt-in actor types are eligible, and live work is
|
|
80
|
+
preserved, but the host application must decide whether dormant state and
|
|
81
|
+
completed history may expire.
|
|
82
|
+
|
|
61
83
|
## Denial of service
|
|
62
84
|
|
|
63
85
|
Configure mailbox and byte limits. Add host rate limiting before public actor
|
data/docs/state-migrations.md
CHANGED
|
@@ -15,6 +15,8 @@ end
|
|
|
15
15
|
|
|
16
16
|
Migration runs in memory during activation. The new version is persisted only
|
|
17
17
|
with the next successful fenced message commit.
|
|
18
|
+
Migration blocks may read application records but cannot write them directly;
|
|
19
|
+
the same Active Record write guard used for handlers applies before activation.
|
|
18
20
|
|
|
19
21
|
## Runtime rules
|
|
20
22
|
|
|
@@ -10,7 +10,8 @@ key. The chat actor also gives every submitted chat message a caller-generated
|
|
|
10
10
|
message ID and checks that ID in durable actor state, because actor handlers may
|
|
11
11
|
be redelivered.
|
|
12
12
|
|
|
13
|
-
The views demonstrate
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Turbo append intents remain roadmap
|
|
13
|
+
The views demonstrate scalar observable replacement and a live chat-message
|
|
14
|
+
ERB component. The chat component receives `recent_messages` as an ordinary
|
|
15
|
+
Ruby array, rerenders its `<ol>` after committed changes, and refreshes through
|
|
16
|
+
the authenticated host request context. Turbo append intents remain roadmap
|
|
17
|
+
work.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<%= solid_object @room do |room| %>
|
|
1
|
+
<%= solid_object @room, authorization_context: current_user do |room| %>
|
|
2
2
|
<p>
|
|
3
3
|
Present:
|
|
4
4
|
<%= room.presence %>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
<%= room.component :messages %>
|
|
7
|
+
<%= room.component :messages, observes: :recent_messages %>
|
|
8
8
|
<% end %>
|
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
SolidObjects.configure do |configuration|
|
|
4
4
|
configuration.authorize_message = lambda do |actor_type:, actor_id:, authorization_context:, **|
|
|
5
|
-
|
|
5
|
+
user = if authorization_context.respond_to?(:current_user)
|
|
6
|
+
authorization_context.current_user
|
|
7
|
+
else
|
|
8
|
+
authorization_context
|
|
9
|
+
end
|
|
10
|
+
next false unless user
|
|
6
11
|
|
|
7
12
|
if actor_type == ShoppingCartActor.actor_type
|
|
8
|
-
|
|
13
|
+
user.id.to_s == actor_id
|
|
9
14
|
else
|
|
10
|
-
ChatRoomPolicy.new(
|
|
15
|
+
ChatRoomPolicy.new(user).access?(actor_id)
|
|
11
16
|
end
|
|
12
17
|
end
|
|
13
18
|
configuration.authorize_query = configuration.authorize_message
|
|
14
19
|
configuration.authorize_subscription = configuration.authorize_message
|
|
20
|
+
configuration.component_authorization_context = ->(controller:) { Current.user }
|
|
15
21
|
end
|
|
16
22
|
|
|
17
23
|
SolidObjects.register_effect(:charge_payment) do |arguments, context|
|
|
@@ -5,14 +5,35 @@ SolidObjects.configure do |configuration|
|
|
|
5
5
|
configuration.effect_worker_count = 1
|
|
6
6
|
configuration.broadcast_worker_count = 1
|
|
7
7
|
configuration.reminder_scheduler_count = 1
|
|
8
|
+
configuration.message_retention = 30.days
|
|
9
|
+
configuration.process_retention = 7.days
|
|
10
|
+
configuration.prune_batch_size = 1_000
|
|
11
|
+
|
|
12
|
+
# Override message retention only for actor types with different audit or
|
|
13
|
+
# privacy requirements:
|
|
14
|
+
#
|
|
15
|
+
# configuration.message_retention_by_actor_type = {
|
|
16
|
+
# "AuditActor" => 365.days,
|
|
17
|
+
# "EphemeralCounter" => 1.day
|
|
18
|
+
# }
|
|
19
|
+
#
|
|
20
|
+
# Actor instances never expire unless their type is listed here. Expiration
|
|
21
|
+
# removes idle state and completed history, so start with the preview command:
|
|
22
|
+
#
|
|
23
|
+
# configuration.instance_retention_by_actor_type = {
|
|
24
|
+
# "EphemeralCounter" => 30.days
|
|
25
|
+
# }
|
|
26
|
+
#
|
|
27
|
+
# bundle exec solid_objects prune_instances
|
|
8
28
|
|
|
9
29
|
# Every policy denies by default, so a fresh installation is intentionally
|
|
10
30
|
# inert. Replace these policies before invoking actors.
|
|
11
31
|
#
|
|
12
32
|
# Message and query policies gate direct calls, sync, async, and state reads.
|
|
13
33
|
# Destroy removes an actor and all of its durable work. Subscription gates
|
|
14
|
-
# Action Cable streams. Administration gates engine pages and
|
|
15
|
-
# commands. Keep the last three denied until their callers are
|
|
34
|
+
# Action Cable streams. Administration gates engine pages, pruning, and
|
|
35
|
+
# operational commands. Keep the last three denied until their callers are
|
|
36
|
+
# authenticated.
|
|
16
37
|
#
|
|
17
38
|
# Prefer policies that bind actor_type and actor_id to a trusted
|
|
18
39
|
# authorization_context. See:
|
|
@@ -27,4 +48,14 @@ SolidObjects.configure do |configuration|
|
|
|
27
48
|
configuration.authorize_destroy = ->(**) { false }
|
|
28
49
|
configuration.authorize_subscription = ->(**) { false }
|
|
29
50
|
configuration.authorize_administration = ->(**) { false }
|
|
51
|
+
|
|
52
|
+
# Configure component_authorization_context to return the authenticated
|
|
53
|
+
# principal used for reactive component refreshes.
|
|
54
|
+
|
|
55
|
+
# On hosts where shell access is already an authenticated administrative
|
|
56
|
+
# boundary, this enables only gem commands that pass the CLI context:
|
|
57
|
+
#
|
|
58
|
+
# configuration.authorize_administration = lambda do |authorization_context:, **|
|
|
59
|
+
# authorization_context.is_a?(Hash) && authorization_context[:source] == "cli"
|
|
60
|
+
# end
|
|
30
61
|
end
|
|
@@ -91,9 +91,25 @@ module SolidObjects
|
|
|
91
91
|
# @rbs () -> void
|
|
92
92
|
def deactivate
|
|
93
93
|
actor.deactivate
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
rescue => error
|
|
95
|
+
SolidObjects.instrument(
|
|
96
|
+
:"activation.deactivation_failed",
|
|
97
|
+
instance_id: lease.instance_id,
|
|
98
|
+
actor_type: actor.class.actor_type,
|
|
99
|
+
actor_id: actor.actor_id,
|
|
100
|
+
owner_id: lease.owner_id,
|
|
101
|
+
generation: lease.generation,
|
|
102
|
+
error_class: error.class.name,
|
|
103
|
+
error_message: error.message
|
|
104
|
+
)
|
|
105
|
+
SolidObjects.configuration.logger.error(
|
|
106
|
+
"SolidObjects activation deactivation failed " \
|
|
107
|
+
"actor_type=#{actor.class.actor_type.inspect} " \
|
|
108
|
+
"actor_id=#{actor.actor_id.inspect} " \
|
|
109
|
+
"error_class=#{error.class.name}"
|
|
110
|
+
)
|
|
111
|
+
ensure
|
|
112
|
+
release_lease
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
private
|
|
@@ -129,13 +145,26 @@ module SolidObjects
|
|
|
129
145
|
|
|
130
146
|
# @rbs (Instance) -> Actor
|
|
131
147
|
def build_actor(instance)
|
|
132
|
-
state_data =
|
|
148
|
+
state_data = ApplicationWriteGuard.call(
|
|
149
|
+
actor_type: instance.actor_type,
|
|
150
|
+
actor_id: instance.actor_id,
|
|
151
|
+
operation: "state_migration"
|
|
152
|
+
) do
|
|
153
|
+
actor_class.definition.migrate_state(instance.state_version, instance.state)
|
|
154
|
+
end
|
|
133
155
|
actor_class.new(
|
|
134
156
|
actor_id: instance.actor_id,
|
|
135
157
|
state: State.new(actor_class.definition.state_definition, state_data)
|
|
136
158
|
)
|
|
137
159
|
end
|
|
138
160
|
|
|
161
|
+
# @rbs () -> void
|
|
162
|
+
def release_lease
|
|
163
|
+
lease.release
|
|
164
|
+
rescue LostActivation
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
139
168
|
# @rbs () -> Message?
|
|
140
169
|
def claim_next_message
|
|
141
170
|
lease.fenced_transaction do |instance|
|