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
|
@@ -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.
|
data/docs/operations.md
ADDED
|
@@ -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.
|