You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Authorize and linearize reference destruction on the actor instance row. Cascade actor-owned durable work, fence stale activations, and prevent claimed reminders from resurrecting a deleted incarnation. Cover deletion and in-flight races with deterministic Minitest integration tests.
Copy file name to clipboardExpand all lines: docs/architecture.md
+60-10Lines changed: 60 additions & 10 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,13 @@
2
2
3
3
## Purpose
4
4
5
-
Solid Objects is a Rails engine that provides database-backed virtual actors for MySQL, PostgreSQL, and SQLite. A virtual actor is a logical object addressed by type and ID whose in-memory activation is created on demand, processes one mailbox turn at a time, persists JSON state, and can disappear when idle without losing its identity or state.
5
+
Solid Objects ports the Cloudflare Durable Objects programming model to Rails.
6
+
It is a database-backed virtual actor runtime for MySQL, PostgreSQL, and
7
+
SQLite. A virtual actor is a logical object addressed by type and ID whose
8
+
in-memory activation is created on demand, processes one mailbox turn at a
9
+
time, persists JSON state, and can disappear when idle without losing its
10
+
identity or state. This ports the programming model, not Cloudflare's
11
+
serverless runtime, global placement, storage API, or platform guarantees.
6
12
7
13
The runtime contract is:
8
14
@@ -53,11 +59,12 @@ The registry maps a stable persisted actor type string to a Ruby actor class. Re
53
59
A reference contains actor type and normalized actor ID. It is cheap,
54
60
serializable as data, and does not imply an active Ruby object. Declared
55
61
message methods delegate to `tell`; declared query and attribute methods
56
-
delegate to `ask`. Both paths authorize and enqueue through the client.
62
+
delegate to `ask`. `destroy` is a reserved synchronous reference operation.
63
+
All three paths authorize through the client.
57
64
58
65
### Client and mailbox
59
66
60
-
The client finds or creates the actor instance and atomically allocates a sequence. It inserts one durable message-history row and one ready-membership row. It validates message names and JSON payloads before writing and enforces idempotency-key uniqueness, payload limits, and the per-actor mailbox cap. Distributed rate limiting and global admission control are not implemented.
67
+
The client finds or creates the actor instance and atomically allocates a sequence. It inserts one durable message-history row and one ready-membership row. It validates message names and JSON payloads before writing and enforces idempotency-key uniqueness, payload limits, and the per-actor mailbox cap. It also authorizes and coordinates actor destruction. Distributed rate limiting and global admission control are not implemented.
61
68
62
69
Message execution state is table membership, not a status column. The durable message remains for results, retention, and diagnostics. Only live work occupies `ready_messages` or `claimed_messages`, so completed history cannot inflate the polling index.
63
70
@@ -100,7 +107,11 @@ An effect worker claims due effect rows through the database coordination adapte
100
107
101
108
### Reminder scheduler
102
109
103
-
The scheduler claims due reminder definitions, enqueues ordinary actor messages, and advances recurring reminders or completes one-shot reminders. A unique occurrence key prevents two schedulers from producing two mailbox rows for the same reminder occurrence.
110
+
The scheduler claims due reminder definitions, locks the source actor instance,
111
+
then enqueues the ordinary actor message and advances or completes the reminder
112
+
in one transaction. A unique occurrence key prevents two schedulers from
113
+
producing two mailbox rows for the same reminder occurrence. Locking the source
114
+
instance first prevents a claimed reminder from recreating a destroyed actor.
104
115
105
116
### Broadcast worker
106
117
@@ -141,9 +152,9 @@ updates; it is not independently persisted.
141
152
Lifecycle hooks are deterministic local hooks:
142
153
143
154
-`on_activate` runs after state load and migration. State changes made there are included with the next successful message commit, not persisted on activation alone.
144
-
-`on_deactivate` runs only on graceful local deactivation. Its state changes are not persisted and it must not be used for durable work.
155
+
-`on_deactivate` runs only on graceful local deactivation. Its state changes are not persisted and it must not be used for durable work. Explicit destruction does not run lifecycle hooks.
145
156
146
-
Durable cleanup belongs in messages, reminders, or effects.
157
+
Durable application cleanup belongs in messages, reminders, or effects.
147
158
148
159
## Enqueue and sequence allocation
149
160
@@ -164,6 +175,37 @@ The increment and insert roll back together. The unique index on `(actor_type, a
164
175
165
176
Committed concurrent enqueues have one database-defined sequence order. No order is promised between transactions that have not committed.
166
177
178
+
## Actor destruction
179
+
180
+
`ActorClass.ref(actor_id).destroy` is a synchronous, idempotent runtime
181
+
operation. It is forbidden from actor context and has a separate
182
+
`authorize_destroy` policy that runs before actor existence is revealed.
183
+
184
+
Destruction uses one transaction:
185
+
186
+
1. Resolve the actor type through the registry.
187
+
2. Authorize the actor type and ID.
188
+
3. Lock the actor instance by logical identity.
189
+
4. Return `false` if it does not exist.
190
+
5. Delete the instance.
191
+
6. Let foreign-key cascades delete message history, ready and claimed
192
+
memberships, dead letters, reminders, effects, and broadcasts.
193
+
7. Commit, emit `solid_objects.actor.destroyed`, and wake local waiters.
194
+
195
+
The instance primary key is the actor-incarnation boundary. A worker holding an
196
+
old lease can continue running Ruby code, but its fenced transaction cannot
197
+
find the deleted instance and raises `LostActivation`. If the same logical
198
+
identity is referenced later, enqueue creates a new instance with default
199
+
state, state version, sequence 1, and a new primary key. An enqueue that loses
200
+
the instance between lookup and locking retries against the new incarnation.
201
+
202
+
A claimed reminder locks the source instance before enqueueing its occurrence,
203
+
so it either commits before destruction and is deleted by the cascade, or
204
+
observes the missing instance and does nothing. An already-running external
205
+
effect, actor-to-actor delivery, or broadcast may have crossed the database
206
+
boundary before destruction; it cannot be recalled. Its completion sees the
207
+
deleted outbox row and cannot enqueue a callback or recreate the source actor.
208
+
167
209
## Candidate selection and fairness
168
210
169
211
An actor is eligible when:
@@ -324,7 +366,10 @@ A reminder record contains actor identity, a reminder name, target message, JSON
When due, the scheduler creates a normal mailbox row with an idempotency key derived from reminder ID and occurrence. The mailbox provides sequential processing and ordinary retry behavior.
369
+
When due, the scheduler locks the source instance and creates a normal mailbox
370
+
row with an idempotency key derived from reminder ID and occurrence. The
371
+
mailbox insert and reminder advancement commit atomically. The mailbox provides
372
+
sequential processing and ordinary retry behavior.
328
373
329
374
Solid Objects persists each occurrence by its mailbox row. Unlike Orleans reminders, an outage does not intentionally discard a due occurrence. Recurring catch-up is configurable:
330
375
@@ -373,14 +418,18 @@ Each channel subscription transmits current observable replacements before strea
373
418
374
419
## Authorization
375
420
376
-
Configuration provides four explicit policies:
421
+
Configuration provides five explicit policies:
377
422
378
423
-`authorize_message`
379
424
-`authorize_query`
425
+
-`authorize_destroy`
380
426
-`authorize_subscription`
381
427
-`authorize_administration`
382
428
383
-
Each receives a request context, registered actor class, actor ID, and operation details. A host can set request context using an isolated execution-state carrier. Internal runtime deliveries carry a system context that is separately recognizable.
429
+
Each receives a request context, actor type, actor ID, and relevant operation
430
+
details. A host can set request context using an isolated execution-state
431
+
carrier. Internal runtime deliveries carry a system context that is separately
432
+
recognizable.
384
433
385
434
No controller, channel, or administrative command treats an actor ID, message ID, request ID, or signed stream name as authorization.
386
435
@@ -515,7 +564,8 @@ PostgreSQL transaction-level advisory locks may be used for optional singleton m
| Failed message attempt | Conditional error, claimed deletion, ready reinsertion or dead letter |
517
566
| Renew or release lease | Conditional instance update |
518
-
| Deliver reminder occurrence | Mailbox enqueue is one transaction; reminder advance is a second claim-checked transaction, bridged by a stable occurrence idempotency key |
567
+
| Destroy actor | Instance identity lock and cascading delete of state, mailbox, reminders, and outboxes |
568
+
| Deliver reminder occurrence | Source instance lock, mailbox enqueue, and reminder advance, with a stable occurrence idempotency key |
0 commit comments