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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +17 -0
  3. data/MIT-LICENSE +19 -0
  4. data/README.md +744 -0
  5. data/Rakefile +40 -0
  6. data/app/controllers/solid_objects/application_controller.rb +23 -0
  7. data/app/controllers/solid_objects/dead_letters_controller.rb +23 -0
  8. data/app/controllers/solid_objects/instances_controller.rb +29 -0
  9. data/app/helpers/solid_objects/actor_helper.rb +25 -0
  10. data/app/models/solid_objects/broadcast.rb +10 -0
  11. data/app/models/solid_objects/claimed_message.rb +14 -0
  12. data/app/models/solid_objects/dead_letter.rb +13 -0
  13. data/app/models/solid_objects/effect.rb +10 -0
  14. data/app/models/solid_objects/instance.rb +93 -0
  15. data/app/models/solid_objects/message.rb +53 -0
  16. data/app/models/solid_objects/process.rb +13 -0
  17. data/app/models/solid_objects/ready_message.rb +10 -0
  18. data/app/models/solid_objects/record.rb +17 -0
  19. data/app/models/solid_objects/reminder.rb +9 -0
  20. data/app/views/solid_objects/dead_letters/index.html.erb +26 -0
  21. data/app/views/solid_objects/instances/index.html.erb +24 -0
  22. data/app/views/solid_objects/instances/show.html.erb +35 -0
  23. data/benchmark/activation_cache.rb +5 -0
  24. data/benchmark/ask_latency.rb +5 -0
  25. data/benchmark/claim.rb +5 -0
  26. data/benchmark/cold_actors.rb +5 -0
  27. data/benchmark/concurrent_actors.rb +5 -0
  28. data/benchmark/enqueue.rb +5 -0
  29. data/benchmark/hot_actor.rb +5 -0
  30. data/benchmark/processing.rb +5 -0
  31. data/benchmark/query_count.rb +5 -0
  32. data/benchmark/support.rb +271 -0
  33. data/config/routes.rb +8 -0
  34. data/db/migrate/20260805000000_create_solid_objects_tables.rb +319 -0
  35. data/docs/adr/0001-postgresql-backend.md +21 -0
  36. data/docs/adr/0002-jsonb-actor-state.md +21 -0
  37. data/docs/adr/0003-mailbox-ordering.md +30 -0
  38. data/docs/adr/0004-activation-leasing.md +21 -0
  39. data/docs/adr/0005-fencing-tokens.md +25 -0
  40. data/docs/adr/0006-at-least-once-delivery.md +24 -0
  41. data/docs/adr/0007-transactional-outbox.md +21 -0
  42. data/docs/adr/0008-actor-communication.md +21 -0
  43. data/docs/adr/0009-realtime-updates.md +21 -0
  44. data/docs/adr/0010-state-versioning.md +29 -0
  45. data/docs/adr/0011-wake-up-strategy.md +34 -0
  46. data/docs/adr/0012-not-active-jobs.md +21 -0
  47. data/docs/adr/0013-database-adapters.md +48 -0
  48. data/docs/architecture.md +615 -0
  49. data/docs/benchmarks.md +26 -0
  50. data/docs/correctness.md +124 -0
  51. data/docs/database-schema.md +111 -0
  52. data/docs/development.md +87 -0
  53. data/docs/implementation-plan.md +518 -0
  54. data/docs/operations.md +123 -0
  55. data/docs/realtime.md +51 -0
  56. data/docs/research/solid_queue.md +545 -0
  57. data/docs/roadmap.md +53 -0
  58. data/docs/security.md +61 -0
  59. data/docs/state-migrations.md +46 -0
  60. data/examples/application/README.md +16 -0
  61. data/examples/application/app/actors/chat_room_actor.rb +34 -0
  62. data/examples/application/app/actors/shopping_cart_actor.rb +79 -0
  63. data/examples/application/app/controllers/cart_controller.rb +54 -0
  64. data/examples/application/app/controllers/chat_rooms_controller.rb +44 -0
  65. data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +8 -0
  66. data/examples/application/app/views/actors/shopping_cart_actor/_summary.html.erb +10 -0
  67. data/examples/application/app/views/cart/show.html.erb +13 -0
  68. data/examples/application/app/views/chat_rooms/show.html.erb +8 -0
  69. data/examples/application/config/initializers/solid_objects.rb +23 -0
  70. data/examples/application/config/routes.rb +20 -0
  71. data/exe/solid_objects +9 -0
  72. data/lib/generators/solid_objects/install_generator.rb +21 -0
  73. data/lib/generators/solid_objects/templates/solid_objects.rb +13 -0
  74. data/lib/solid_objects/action_cable_broadcast_adapter.rb +19 -0
  75. data/lib/solid_objects/activation.rb +183 -0
  76. data/lib/solid_objects/activation_manager.rb +102 -0
  77. data/lib/solid_objects/actor.rb +271 -0
  78. data/lib/solid_objects/actor_channel.rb +29 -0
  79. data/lib/solid_objects/actor_definition.rb +212 -0
  80. data/lib/solid_objects/actor_registry.rb +65 -0
  81. data/lib/solid_objects/actor_snapshot.rb +42 -0
  82. data/lib/solid_objects/actor_view.rb +117 -0
  83. data/lib/solid_objects/broadcast_executor.rb +162 -0
  84. data/lib/solid_objects/cli.rb +118 -0
  85. data/lib/solid_objects/client.rb +153 -0
  86. data/lib/solid_objects/configuration.rb +168 -0
  87. data/lib/solid_objects/context.rb +41 -0
  88. data/lib/solid_objects/database_adapter.rb +82 -0
  89. data/lib/solid_objects/database_adapters/mysql.rb +22 -0
  90. data/lib/solid_objects/database_adapters/postgresql.rb +17 -0
  91. data/lib/solid_objects/database_adapters/sqlite.rb +12 -0
  92. data/lib/solid_objects/dead_letter_manager.rb +47 -0
  93. data/lib/solid_objects/dom_identity.rb +38 -0
  94. data/lib/solid_objects/effect_executor.rb +235 -0
  95. data/lib/solid_objects/effect_registry.rb +34 -0
  96. data/lib/solid_objects/engine.rb +33 -0
  97. data/lib/solid_objects/errors.rb +65 -0
  98. data/lib/solid_objects/executor.rb +290 -0
  99. data/lib/solid_objects/instrumentation.rb +10 -0
  100. data/lib/solid_objects/lease.rb +172 -0
  101. data/lib/solid_objects/lease_renewer.rb +70 -0
  102. data/lib/solid_objects/log_subscriber.rb +29 -0
  103. data/lib/solid_objects/mailbox.rb +178 -0
  104. data/lib/solid_objects/message_reference.rb +52 -0
  105. data/lib/solid_objects/process_registry.rb +143 -0
  106. data/lib/solid_objects/reference.rb +96 -0
  107. data/lib/solid_objects/reminder_scheduler.rb +168 -0
  108. data/lib/solid_objects/serialization.rb +99 -0
  109. data/lib/solid_objects/state.rb +111 -0
  110. data/lib/solid_objects/stream_name.rb +29 -0
  111. data/lib/solid_objects/stream_token.rb +59 -0
  112. data/lib/solid_objects/supervisor.rb +87 -0
  113. data/lib/solid_objects/turbo_stream_renderer.rb +35 -0
  114. data/lib/solid_objects/version.rb +5 -0
  115. data/lib/solid_objects/wake_up.rb +28 -0
  116. data/lib/solid_objects/worker.rb +139 -0
  117. data/lib/solid_objects.rb +118 -0
  118. data/sig/generated/controllers/solid_objects/application_controller.rbs +10 -0
  119. data/sig/generated/controllers/solid_objects/dead_letters_controller.rbs +11 -0
  120. data/sig/generated/controllers/solid_objects/instances_controller.rbs +11 -0
  121. data/sig/generated/helpers/solid_objects/actor_helper.rbs +8 -0
  122. data/sig/generated/lib/generators/solid_objects/install_generator.rbs +13 -0
  123. data/sig/generated/lib/solid_objects/action_cable_broadcast_adapter.rbs +8 -0
  124. data/sig/generated/lib/solid_objects/activation.rbs +65 -0
  125. data/sig/generated/lib/solid_objects/activation_manager.rbs +36 -0
  126. data/sig/generated/lib/solid_objects/actor.rbs +183 -0
  127. data/sig/generated/lib/solid_objects/actor_channel.rbs +8 -0
  128. data/sig/generated/lib/solid_objects/actor_definition.rbs +117 -0
  129. data/sig/generated/lib/solid_objects/actor_registry.rbs +36 -0
  130. data/sig/generated/lib/solid_objects/actor_snapshot.rbs +28 -0
  131. data/sig/generated/lib/solid_objects/actor_view.rbs +56 -0
  132. data/sig/generated/lib/solid_objects/broadcast_executor.rbs +55 -0
  133. data/sig/generated/lib/solid_objects/cli.rbs +31 -0
  134. data/sig/generated/lib/solid_objects/client.rbs +35 -0
  135. data/sig/generated/lib/solid_objects/configuration.rbs +147 -0
  136. data/sig/generated/lib/solid_objects/context.rbs +56 -0
  137. data/sig/generated/lib/solid_objects/database_adapter.rbs +42 -0
  138. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +16 -0
  139. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +13 -0
  140. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +10 -0
  141. data/sig/generated/lib/solid_objects/dead_letter_manager.rbs +16 -0
  142. data/sig/generated/lib/solid_objects/dom_identity.rbs +20 -0
  143. data/sig/generated/lib/solid_objects/effect_executor.rbs +82 -0
  144. data/sig/generated/lib/solid_objects/effect_registry.rbs +24 -0
  145. data/sig/generated/lib/solid_objects/engine.rbs +7 -0
  146. data/sig/generated/lib/solid_objects/errors.rbs +64 -0
  147. data/sig/generated/lib/solid_objects/executor.rbs +60 -0
  148. data/sig/generated/lib/solid_objects/instrumentation.rbs +8 -0
  149. data/sig/generated/lib/solid_objects/lease.rbs +57 -0
  150. data/sig/generated/lib/solid_objects/lease_renewer.rbs +42 -0
  151. data/sig/generated/lib/solid_objects/log_subscriber.rbs +11 -0
  152. data/sig/generated/lib/solid_objects/mailbox.rbs +46 -0
  153. data/sig/generated/lib/solid_objects/message_reference.rbs +37 -0
  154. data/sig/generated/lib/solid_objects/process_registry.rbs +46 -0
  155. data/sig/generated/lib/solid_objects/reference.rbs +45 -0
  156. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +55 -0
  157. data/sig/generated/lib/solid_objects/serialization.rbs +31 -0
  158. data/sig/generated/lib/solid_objects/state.rbs +72 -0
  159. data/sig/generated/lib/solid_objects/stream_name.rbs +11 -0
  160. data/sig/generated/lib/solid_objects/stream_token.rbs +19 -0
  161. data/sig/generated/lib/solid_objects/supervisor.rbs +38 -0
  162. data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +14 -0
  163. data/sig/generated/lib/solid_objects/version.rbs +5 -0
  164. data/sig/generated/lib/solid_objects/wake_up.rbs +24 -0
  165. data/sig/generated/lib/solid_objects/worker.rbs +56 -0
  166. data/sig/generated/lib/solid_objects.rbs +41 -0
  167. data/sig/generated/models/solid_objects/broadcast.rbs +6 -0
  168. data/sig/generated/models/solid_objects/claimed_message.rbs +6 -0
  169. data/sig/generated/models/solid_objects/dead_letter.rbs +6 -0
  170. data/sig/generated/models/solid_objects/effect.rbs +6 -0
  171. data/sig/generated/models/solid_objects/instance.rbs +25 -0
  172. data/sig/generated/models/solid_objects/message.rbs +22 -0
  173. data/sig/generated/models/solid_objects/process.rbs +6 -0
  174. data/sig/generated/models/solid_objects/ready_message.rbs +6 -0
  175. data/sig/generated/models/solid_objects/record.rbs +8 -0
  176. data/sig/generated/models/solid_objects/reminder.rbs +6 -0
  177. data/sig/support/framework.rbs +37 -0
  178. metadata +467 -0
@@ -0,0 +1,518 @@
1
+ # Solid Objects Implementation Plan
2
+
3
+ ## Test-driven workflow
4
+
5
+ Every behavior change starts with a focused failing Minitest, followed by the smallest implementation that makes it pass and a refactor while green. The suite follows Solid Queue's organization:
6
+
7
+ - `test/unit` for actor DSL and pure value objects
8
+ - `test/models` for records, constraints, and query objects
9
+ - `test/integration` for end-to-end runtime and process behavior
10
+ - `test/test_helpers` for deterministic barriers, process control, and cross-connection observation
11
+ - `test/dummy` for a real host Rails application and engine integration
12
+
13
+ Core coordination tests use real PostgreSQL, MySQL, and SQLite connections. Tests that require independent sessions do not use transactional wrapping and clean their records explicitly. Race tests use barriers, queues, condition variables, or child-process pipes rather than timing-based sleeps as their primary synchronization.
14
+
15
+ ## Milestone 0: Gem, inline RBS, and database test harness
16
+
17
+ ### Files
18
+
19
+ - `solid_objects.gemspec`
20
+ - `Gemfile`
21
+ - `Rakefile`
22
+ - `lib/solid_objects.rb`
23
+ - `lib/solid_objects/version.rb`
24
+ - `lib/solid_objects/engine.rb`
25
+ - `test/dummy/**`
26
+ - `test/test_helper.rb`
27
+ - `.github/workflows/ci.yml`
28
+ - `.standard.yml`
29
+ - `Steepfile`
30
+ - `sig/**`
31
+
32
+ ### Public API
33
+
34
+ `SolidObjects.configure` and `SolidObjects::VERSION`. All owned Ruby files use `# rbs_inline: enabled`, instance-variable declarations, and method signatures following `cardmagic/classifier`.
35
+
36
+ ### Database changes
37
+
38
+ None.
39
+
40
+ ### Tests
41
+
42
+ - Engine boots in the dummy application.
43
+ - PostgreSQL, MySQL InnoDB, and SQLite adapters and minimum server versions are validated.
44
+ - Zeitwerk eager loading succeeds.
45
+ - RBS::Inline generation, RBS validation, and Steep checking succeed.
46
+
47
+ ### Failure modes
48
+
49
+ - Rails or Ruby outside the supported range.
50
+ - Missing `pg` adapter.
51
+ - Missing `mysql2` or `sqlite3` adapter in its matrix job.
52
+ - Test database unavailable.
53
+
54
+ ### Completion criteria
55
+
56
+ Bundle installs, each dummy database boots, and the empty Minitest suite, Standard Ruby, generated RBS validation, and Steep pass.
57
+
58
+ ## Milestone 1: Registry, actor DSL, state, and serialization
59
+
60
+ ### Files
61
+
62
+ - `lib/solid_objects/actor.rb`
63
+ - `lib/solid_objects/actor_registry.rb`
64
+ - `lib/solid_objects/actor_definition.rb`
65
+ - `lib/solid_objects/state.rb`
66
+ - `lib/solid_objects/serialization.rb`
67
+ - `lib/solid_objects/context.rb`
68
+ - `lib/solid_objects/errors.rb`
69
+ - `test/unit/actor_registry_test.rb`
70
+ - `test/unit/actor_test.rb`
71
+ - `test/unit/state_test.rb`
72
+ - `test/unit/serialization_test.rb`
73
+
74
+ ### Public API
75
+
76
+ - `SolidObjects::Actor`
77
+ - `actor_type`
78
+ - `attribute` with actor instance readers and writers
79
+ - Public instance methods as messages
80
+ - `message` for explicit dynamic definitions
81
+ - `query`, `observable`
82
+ - `state_version`, `migrate_state`
83
+ - `on_activate`, `on_deactivate`
84
+ - `ActorClass.ref(actor_id)`
85
+
86
+ ### Database changes
87
+
88
+ None.
89
+
90
+ ### Tests
91
+
92
+ - Registration and duplicate rejection
93
+ - Public, private, query, and observable method lookup
94
+ - Defaults are not shared
95
+ - JSON normalization and unsafe value rejection
96
+ - Complete state migration chains and newer-state rejection
97
+
98
+ ### Failure modes
99
+
100
+ - Anonymous or duplicate actor types
101
+ - Undefined message names
102
+ - Mutable defaults shared across actors
103
+ - Unsafe serialization
104
+ - Missing migration step
105
+
106
+ ### Completion criteria
107
+
108
+ Actor definitions can be instantiated and executed in memory without persistence or constantization.
109
+
110
+ ## Milestone 2: Portable schema and internal records
111
+
112
+ ### Files
113
+
114
+ - `db/migrate/001_create_solid_objects_tables.rb`
115
+ - `app/models/solid_objects/record.rb`
116
+ - `app/models/solid_objects/instance.rb`
117
+ - `app/models/solid_objects/message.rb`
118
+ - `app/models/solid_objects/ready_message.rb`
119
+ - `app/models/solid_objects/claimed_message.rb`
120
+ - `app/models/solid_objects/reminder.rb`
121
+ - `app/models/solid_objects/effect.rb`
122
+ - `app/models/solid_objects/broadcast.rb`
123
+ - `app/models/solid_objects/dead_letter.rb`
124
+ - `app/models/solid_objects/process.rb`
125
+ - `lib/solid_objects/database_adapter.rb`
126
+ - `lib/solid_objects/database_adapters/postgresql.rb`
127
+ - `lib/solid_objects/database_adapters/mysql.rb`
128
+ - `lib/solid_objects/database_adapters/sqlite.rb`
129
+ - `docs/database-schema.md`
130
+ - `test/models/schema_constraints_test.rb`
131
+ - `test/models/instance_test.rb`
132
+ - `test/models/message_test.rb`
133
+
134
+ ### Public API
135
+
136
+ No new application API. Internal records are deliberately namespaced.
137
+
138
+ ### Database changes
139
+
140
+ Create the seven domain tables plus ready- and claimed-message membership tables. Add foreign keys, unique constraints, positive sequence/version checks, ordinary composite polling indexes, and cleanup indexes. Use JSONB on PostgreSQL, JSON on MySQL, and Rails JSON-compatible columns on SQLite. Do not use partial indexes or a message status column.
141
+
142
+ ### Tests
143
+
144
+ - Actor identity uniqueness
145
+ - Concurrent actor creation
146
+ - Counter and membership constraints
147
+ - Foreign-key cleanup behavior
148
+ - Ready and claimed hot-table index definitions
149
+ - Configured table prefix
150
+ - MySQL tables use InnoDB
151
+ - SQLite write transactions and busy retry
152
+
153
+ ### Failure modes
154
+
155
+ - Unsupported database or server version
156
+ - Prefix changed after migration
157
+ - A message accidentally represented in both ready and claimed tables
158
+ - Cascades deleting required diagnostic data
159
+
160
+ ### Completion criteria
161
+
162
+ The schema installs in PostgreSQL, MySQL, and SQLite dummy databases and database constraints reject invalid states independently of Rails validations.
163
+
164
+ ## Milestone 3: Durable enqueue, references, tell, and ask
165
+
166
+ ### Files
167
+
168
+ - `lib/solid_objects/reference.rb`
169
+ - `lib/solid_objects/client.rb`
170
+ - `lib/solid_objects/mailbox.rb`
171
+ - `lib/solid_objects/message_reference.rb`
172
+ - `lib/solid_objects/wake_up.rb`
173
+ - `test/integration/enqueue_test.rb`
174
+ - `test/integration/tell_test.rb`
175
+ - `test/integration/ask_test.rb`
176
+
177
+ ### Public API
178
+
179
+ - `Reference#tell`
180
+ - `Reference#ask`
181
+ - Method-style message, query, and read-only attribute dispatch
182
+ - `MessageReference#id`, `#status`, `#result`
183
+ - Authorization context and hooks
184
+
185
+ ### Database changes
186
+
187
+ No new tables. Use instance sequence and message request/idempotency columns.
188
+
189
+ ### Tests
190
+
191
+ - Per-actor sequence allocation under concurrent connections
192
+ - Independent sequences for different actors
193
+ - Idempotency key deduplication
194
+ - Tell return value
195
+ - Ask success, failure, and timeout
196
+ - Mailbox and payload limits
197
+ - Message/query authorization failure
198
+
199
+ ### Failure modes
200
+
201
+ - Concurrent first enqueue
202
+ - Lock timeout or deadlock
203
+ - Duplicate idempotency key with different payload
204
+ - Ask caller timeout
205
+ - Oversized payload or mailbox
206
+
207
+ ### Completion criteria
208
+
209
+ Messages and ready membership enqueue durably in strict per-actor sequence and `ask` can observe a manually completed result. Polling-only `ask` is documented as unsuitable for latency-sensitive request handlers.
210
+
211
+ ## Milestone 4: Fenced, runnable vertical slice
212
+
213
+ ### Files
214
+
215
+ - `lib/solid_objects/activation.rb`
216
+ - `lib/solid_objects/lease.rb`
217
+ - `lib/solid_objects/executor.rb`
218
+ - `lib/solid_objects/worker.rb`
219
+ - `lib/solid_objects/dispatcher.rb`
220
+ - `lib/solid_objects/process_registry.rb`
221
+ - `examples/shopping_cart_actor.rb`
222
+ - `test/integration/vertical_slice_test.rb`
223
+ - `test/integration/sequential_processing_test.rb`
224
+ - `test/integration/retry_test.rb`
225
+ - `test/integration/lease_test.rb`
226
+ - `test/integration/fencing_test.rb`
227
+ - `test/integration/crash_recovery_test.rb`
228
+
229
+ ### Public API
230
+
231
+ Runnable `SolidObjects::Worker`; current message context inside actors. A worker cannot process actor state without a registered process, renewable activation lease, and fencing generation.
232
+
233
+ ### Database changes
234
+
235
+ No new tables.
236
+
237
+ ### Tests
238
+
239
+ - Shopping cart tell and ask
240
+ - One actor processes messages sequentially
241
+ - Different actors can execute concurrently
242
+ - Lease acquire, renew, expire, and release
243
+ - Two workers cannot hold the same actor lease
244
+ - Deterministic stale-writer rejection
245
+ - Crash recovery and at-least-once redelivery
246
+ - State and completion are atomic
247
+ - Basic retry and strict head-of-mailbox blocking
248
+ - Handler-level duplicate-delivery guards
249
+ - Actor-to-actor tell outside actor context
250
+
251
+ ### Failure modes
252
+
253
+ - Actor exception
254
+ - Serialization failure after actor code
255
+ - Query mutates state
256
+ - Worker shutdown during a turn
257
+ - Process pause beyond lease expiry
258
+ - Lease renewal race
259
+
260
+ ### Completion criteria
261
+
262
+ The example actor runs end to end against all three databases and persists/reactivates state. Real multi-connection tests prove that generation A cannot write after generation B acquires and commits. The runnable worker always enforces leases and fencing; no unsafe single-worker mode exists.
263
+
264
+ ## Milestone 5: Supervision, heartbeats, and distributed hardening
265
+
266
+ ### Files
267
+
268
+ - `lib/solid_objects/supervisor.rb`
269
+ - `lib/solid_objects/activation_manager.rb`
270
+ - `lib/solid_objects/configuration.rb`
271
+ - `test/integration/process_lifecycle_test.rb`
272
+ - `test/integration/fairness_test.rb`
273
+
274
+ ### Public API
275
+
276
+ Process configuration and lifecycle hooks.
277
+
278
+ ### Database changes
279
+
280
+ Use process and activation columns already created. Add a migration only if query-plan evidence requires a new lease index.
281
+
282
+ ### Tests
283
+
284
+ - Heartbeats and stale process cleanup
285
+ - Graceful shutdown
286
+ - Max message and duration budgets
287
+ - Hot actor fairness
288
+
289
+ ### Failure modes
290
+
291
+ - Process pause rather than death
292
+ - Heartbeat task failure
293
+ - Database outage during release
294
+ - Child process boot or shutdown timeout
295
+
296
+ ### Completion criteria
297
+
298
+ Real process tests on PostgreSQL, MySQL, and SQLite demonstrate heartbeat cleanup, bounded fairness, and graceful shutdown. PostgreSQL and MySQL additionally prove `SKIP LOCKED`; SQLite proves serialized `BEGIN IMMEDIATE` claims and busy retry.
299
+
300
+ ## Milestone 6: Effects and actor-message outbox
301
+
302
+ ### Files
303
+
304
+ - `lib/solid_objects/effect_registry.rb`
305
+ - `lib/solid_objects/effect_executor.rb`
306
+ - `lib/solid_objects/outbox_dispatcher.rb`
307
+ - `test/integration/effects_test.rb`
308
+ - `test/integration/actor_communication_test.rb`
309
+
310
+ ### Public API
311
+
312
+ - `emit`
313
+ - `SolidObjects.register_effect`
314
+ - `send_to`
315
+
316
+ ### Database changes
317
+
318
+ Use the effects table. Add delivery-token or outcome columns only through a migration.
319
+
320
+ ### Tests
321
+
322
+ - Effect insert is atomic with state/message completion
323
+ - Rollback leaves no effect
324
+ - Delivery retry and dead effect
325
+ - Stable idempotency context
326
+ - Success/failure outcome messages
327
+ - Transactional actor-to-actor delivery
328
+ - `ask` rejected in actor context
329
+
330
+ ### Failure modes
331
+
332
+ - External success before local acknowledgement
333
+ - Handler missing after deploy
334
+ - Outcome payload too large
335
+ - Target actor message renamed
336
+
337
+ ### Completion criteria
338
+
339
+ Effects and actor messages are never delivered for a rolled-back actor turn and can be retried without losing their stable IDs.
340
+
341
+ ## Milestone 7: Durable reminders
342
+
343
+ ### Files
344
+
345
+ - `lib/solid_objects/reminder_scheduler.rb`
346
+ - `test/integration/reminders_test.rb`
347
+
348
+ ### Public API
349
+
350
+ - `schedule`
351
+ - Reminder cancellation and inspection API
352
+
353
+ ### Database changes
354
+
355
+ Use reminders plus mailbox idempotency. Add a unique occurrence index if not in the initial schema.
356
+
357
+ ### Tests
358
+
359
+ - One-shot reminder
360
+ - Recurring occurrence uniqueness with two schedulers
361
+ - Reminder reactivates idle actor
362
+ - Scheduler crash recovery
363
+ - Missed-occurrence policies
364
+ - Cancellation race
365
+
366
+ ### Failure modes
367
+
368
+ - Clock jumps
369
+ - Duplicate scheduler claims
370
+ - Long outage creates excessive catch-up
371
+ - Reminder callback removed in code
372
+
373
+ ### Completion criteria
374
+
375
+ Due reminders become ordinary mailbox messages exactly once per occurrence record while their eventual message execution remains at least once.
376
+
377
+ ## Milestone 8: Realtime integration
378
+
379
+ ### Files
380
+
381
+ - `app/channels/solid_objects/actor_channel.rb`
382
+ - `app/controllers/solid_objects/actor_states_controller.rb`
383
+ - `app/helpers/solid_objects/actors_helper.rb`
384
+ - `lib/solid_objects/stream_name.rb`
385
+ - `lib/solid_objects/broadcast_executor.rb`
386
+ - `config/routes.rb`
387
+ - `docs/realtime.md`
388
+ - `test/channels/solid_objects/actor_channel_test.rb`
389
+ - `test/helpers/solid_objects/actors_helper_test.rb`
390
+ - `test/integration/broadcasts_test.rb`
391
+
392
+ ### Public API
393
+
394
+ - `solid_object`
395
+ - Scope `value` and `component`
396
+ - Subscription and state-read authorization
397
+
398
+ ### Database changes
399
+
400
+ Use broadcast outbox rows and their retry fields.
401
+
402
+ ### Tests
403
+
404
+ - Initial server render
405
+ - Stable DOM IDs
406
+ - One actor subscription for multiple targets
407
+ - Signed token verification and authorization
408
+ - Changed observable detection
409
+ - Broadcast inserted with commit, never rollback
410
+ - Broadcast retry
411
+ - Reconnect refresh current state
412
+
413
+ ### Failure modes
414
+
415
+ - Action Cable or Turbo absent
416
+ - Disconnected client
417
+ - Duplicate replacement
418
+ - Authorization changes while connected
419
+ - Component renderer missing
420
+
421
+ ### Completion criteria
422
+
423
+ An authorized scope renders current values and converges after reconnect; observable broadcasts are durable and post-commit.
424
+
425
+ ## Milestone 9: Operations, instrumentation, and dead letters
426
+
427
+ ### Files
428
+
429
+ - `lib/solid_objects/cli.rb`
430
+ - `exe/solid_objects`
431
+ - `lib/solid_objects/log_subscriber.rb`
432
+ - `lib/solid_objects/instrumentation.rb`
433
+ - `lib/tasks/solid_objects_tasks.rake`
434
+ - `app/controllers/solid_objects/admin/**`
435
+ - `app/views/solid_objects/admin/**`
436
+ - `docs/operations.md`
437
+ - `docs/correctness.md`
438
+ - `docs/security.md`
439
+ - `test/unit/cli_test.rb`
440
+ - `test/integration/instrumentation_test.rb`
441
+ - `test/integration/dead_letters_test.rb`
442
+ - `test/models/instance_reconciliation_test.rb`
443
+
444
+ ### Public API
445
+
446
+ CLI start, check, status, dead-letter list/retry, and prune commands. Optional read-only admin engine. Batchable read-only actor relations: `.active`, `.without_pending_work`, and `.orphaned`.
447
+
448
+ ### Database changes
449
+
450
+ No expected changes.
451
+
452
+ ### Tests
453
+
454
+ - Required notification events and redacted payloads
455
+ - Structured log fields
456
+ - Dead-letter inspection and retry
457
+ - Admin authorization
458
+ - Pruning retention and bounded batches
459
+ - CLI exit statuses
460
+ - Lost-alarm and orphan discovery without direct state mutation
461
+
462
+ ### Failure modes
463
+
464
+ - Sensitive data in logs
465
+ - Unbounded admin queries
466
+ - Retrying wrong dead letter
467
+ - Cleanup racing with ask waiter
468
+ - Reconciliation code mutating actor state outside `tell`
469
+ - Reconciliation stampedes without delayed `available_at`
470
+
471
+ ### Completion criteria
472
+
473
+ Operators can inspect health and failures without direct SQL, locate lost alarms and orphaned actors, and observe every required transition without raw arguments. Documentation requires reconciliation repairs to use delayed `tell` rather than direct instance updates.
474
+
475
+ ## Milestone 10: Examples, benchmarks, documentation, and release hardening
476
+
477
+ ### Files
478
+
479
+ - `test/dummy/app/actors/shopping_cart_actor.rb`
480
+ - `test/dummy/app/actors/chat_room_actor.rb`
481
+ - `test/dummy/app/controllers/**`
482
+ - `test/dummy/app/views/**`
483
+ - `benchmark/enqueue.rb`
484
+ - `benchmark/claim.rb`
485
+ - `benchmark/processing.rb`
486
+ - `benchmark/workloads.rb`
487
+ - `README.md`
488
+ - `docs/development.md`
489
+ - `docs/state-migrations.md`
490
+ - `docs/roadmap.md`
491
+
492
+ ### Public API
493
+
494
+ Final documented v0.x API.
495
+
496
+ ### Database changes
497
+
498
+ Only evidence-driven index changes, each with query-plan tests and migration notes.
499
+
500
+ ### Tests
501
+
502
+ - Shopping cart and chat room end-to-end flows
503
+ - Per-backend query counts
504
+ - Rolling-version compatibility fixtures
505
+ - Full suite on supported Rails versions
506
+ - Standard Ruby and security audit
507
+ - Gem build and install smoke test
508
+
509
+ ### Failure modes
510
+
511
+ - Example-specific API design
512
+ - Benchmark environment mistaken for capacity guarantee
513
+ - Version matrix regressions
514
+ - Packaging omits engine files or migrations
515
+
516
+ ### Completion criteria
517
+
518
+ The gem builds, installs into the dummy app, passes all available database suites plus formatting, inline RBS, type, and security checks, and documents implemented, partial, and future behavior without a production-ready claim unsupported by evidence.
@@ -0,0 +1,123 @@
1
+ # Operations guide
2
+
3
+ ## Runtime
4
+
5
+ Start all configured roles:
6
+
7
+ ```bash
8
+ bundle exec solid_objects start
9
+ ```
10
+
11
+ Inspect process records and clean stale ownership:
12
+
13
+ ```bash
14
+ bundle exec solid_objects status
15
+ bundle exec solid_objects cleanup
16
+ ```
17
+
18
+ Process inspection, cleanup, dead-letter inspection, and retry all require an
19
+ administration policy that authorizes the CLI context:
20
+
21
+ ```bash
22
+ bundle exec solid_objects dead_letters
23
+ bundle exec solid_objects retry_dead_letter 123
24
+ ```
25
+
26
+ ## Configuration
27
+
28
+ Important controls include:
29
+
30
+ - `worker_count`
31
+ - `effect_worker_count`
32
+ - `broadcast_worker_count`
33
+ - `reminder_scheduler_count`
34
+ - `max_messages_per_activation_pass`
35
+ - `max_activation_duration`
36
+ - `idle_deactivation_timeout`
37
+ - `lease_duration`
38
+ - `lease_renewal_interval`
39
+ - `polling_interval`
40
+ - `max_mailbox_length`
41
+ - payload, state, and result byte limits
42
+ - retry attempts and delay
43
+ - heartbeat interval and alive threshold
44
+
45
+ Keep lease duration comfortably above renewal interval and expected database
46
+ pause time. A handler can exceed the pass-duration budget because Ruby code is
47
+ not safely preempted; alert on message duration and isolate untrusted work.
48
+
49
+ ## Graceful shutdown
50
+
51
+ The supervisor requests shutdown, stops new claims, lets active loops return,
52
+ releases cached actor leases, and marks process rows stopped. A hard kill is
53
+ safe: claimed messages become recoverable when the lease or process heartbeat
54
+ is stale.
55
+
56
+ Automatic replacement of failed runtime threads and periodic cleanup inside the
57
+ supervisor are not implemented yet. Run the cleanup command from a scheduled
58
+ operational task until that roadmap item lands.
59
+
60
+ ## Reconciliation
61
+
62
+ Self-scheduling actors need a daily or similarly low-frequency reconciliation
63
+ job because an application-level alarm can be lost. The reconciler reads state
64
+ but sends every repair through `tell`.
65
+
66
+ Use:
67
+
68
+ - `Instance.states_for(actor_type:, actor_ids:)`;
69
+ - `Instance.active(actor_type:)`;
70
+ - `Instance.without_pending_work(quiet_for:)`;
71
+ - `Instance.orphaned(actor_type:, owner:)`.
72
+
73
+ Spread large repairs with `available_at:`. Report at least bootstrapped,
74
+ reconfigured, revived, suspended, and orphaned counts. A nonzero revived count
75
+ is evidence that alarms are being lost.
76
+
77
+ Never bulk-update actor state. That bypasses lease ownership and fencing.
78
+
79
+ ## Actor destruction
80
+
81
+ Delete an actor only through its authorized reference:
82
+
83
+ ```ruby
84
+ Counter.ref("global").destroy(authorization_context: Current.user)
85
+ ```
86
+
87
+ Do not delete `solid_objects_instances` directly. The public operation locks
88
+ the identity, invalidates stale activations through the deleted incarnation
89
+ key, cascades through all actor-owned rows, emits
90
+ `solid_objects.actor.destroyed`, and wakes local waiters.
91
+
92
+ Destruction removes pending outboxes but cannot recall external I/O,
93
+ actor-to-actor delivery, or a broadcast that already started. Confirm
94
+ downstream idempotency and application retention requirements before deleting
95
+ an actor. Reusing the same actor type and ID creates a fresh incarnation.
96
+
97
+ ## Monitoring
98
+
99
+ Alert on:
100
+
101
+ - oldest ready-message age;
102
+ - ready and claimed membership counts;
103
+ - mailbox-full rejections;
104
+ - actor turn duration and failures;
105
+ - lost-activation rate;
106
+ - dead-letter creation;
107
+ - actor destruction rate;
108
+ - stale process heartbeats;
109
+ - effect and broadcast retry/dead counts;
110
+ - due-reminder lag;
111
+ - reconciliation drift;
112
+ - database lock waits, deadlocks, and SQLite busy errors.
113
+
114
+ ## Retention and backups
115
+
116
+ The schema has cleanup indexes, but automatic pruning commands are still
117
+ roadmap work. Until implemented, define application-owned bounded deletes that
118
+ preserve unfinished messages, dead letters under investigation, and ask results
119
+ for the promised lookup period.
120
+
121
+ Back up actor tables with the same consistency guarantees as application data.
122
+ Restoring only instances without their mailboxes/outboxes, or vice versa, can
123
+ violate application expectations.
data/docs/realtime.md ADDED
@@ -0,0 +1,51 @@
1
+ # Realtime integration
2
+
3
+ ## Rendering
4
+
5
+ `solid_object` performs initial server rendering and emits one
6
+ `turbo-cable-stream-source` for the actor:
7
+
8
+ ```erb
9
+ <%= solid_object ShoppingCartActor.ref(current_user.id) do |cart| %>
10
+ Items: <%= cart.items_count %>
11
+ <% end %>
12
+ ```
13
+
14
+ Every observable gets a stable opaque DOM ID. Multiple values share the one
15
+ actor subscription and Action Cable multiplexes actor subscriptions over the
16
+ browser's physical WebSocket.
17
+
18
+ `actor.component(:summary)` renders a host partial by convention at
19
+ `actors/<actor_class>/_summary`. Initial component rendering is implemented;
20
+ durable background component replacement is not yet implemented.
21
+
22
+ ## Authorization
23
+
24
+ The HTML contains a signed actor identity token. Signing prevents modification;
25
+ it does not grant access. `ActorChannel` verifies the token, resolves the actor
26
+ through the registry, calls `authorize_subscription`, and streams only after
27
+ approval.
28
+
29
+ Initial observable method reads separately call `authorize_query`. Never
30
+ authorize solely from actor ID, token possession, stream name, or DOM ID.
31
+
32
+ ## Broadcast durability
33
+
34
+ The actor's fenced commit compares observables before and after the turn and
35
+ inserts one broadcast row per changed value. A broadcast process later sends a
36
+ Turbo replacement and records delivery. No direct broadcast occurs inside the
37
+ actor transaction.
38
+
39
+ If Cable delivery is lost, reconnecting `ActorChannel` transmits replacements
40
+ from current actor state. The durable state row remains source of truth.
41
+
42
+ ## Deployment
43
+
44
+ The default adapter calls `ActionCable.server.broadcast`; configure Action
45
+ Cable's normal production pub/sub adapter for a multi-process Rails deployment.
46
+ Redis may therefore be used by Action Cable, but Solid Objects itself does not
47
+ require Redis.
48
+
49
+ Changing or removing observable names during a rolling deploy can strand old
50
+ broadcast rows or old DOM targets. Keep old names compatible until the outbox
51
+ and old pages have drained.