solid_objects 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +17 -0
- data/MIT-LICENSE +19 -0
- data/README.md +744 -0
- data/Rakefile +40 -0
- data/app/controllers/solid_objects/application_controller.rb +23 -0
- data/app/controllers/solid_objects/dead_letters_controller.rb +23 -0
- data/app/controllers/solid_objects/instances_controller.rb +29 -0
- data/app/helpers/solid_objects/actor_helper.rb +25 -0
- data/app/models/solid_objects/broadcast.rb +10 -0
- data/app/models/solid_objects/claimed_message.rb +14 -0
- data/app/models/solid_objects/dead_letter.rb +13 -0
- data/app/models/solid_objects/effect.rb +10 -0
- data/app/models/solid_objects/instance.rb +93 -0
- data/app/models/solid_objects/message.rb +53 -0
- data/app/models/solid_objects/process.rb +13 -0
- data/app/models/solid_objects/ready_message.rb +10 -0
- data/app/models/solid_objects/record.rb +17 -0
- data/app/models/solid_objects/reminder.rb +9 -0
- data/app/views/solid_objects/dead_letters/index.html.erb +26 -0
- data/app/views/solid_objects/instances/index.html.erb +24 -0
- data/app/views/solid_objects/instances/show.html.erb +35 -0
- data/benchmark/activation_cache.rb +5 -0
- data/benchmark/ask_latency.rb +5 -0
- data/benchmark/claim.rb +5 -0
- data/benchmark/cold_actors.rb +5 -0
- data/benchmark/concurrent_actors.rb +5 -0
- data/benchmark/enqueue.rb +5 -0
- data/benchmark/hot_actor.rb +5 -0
- data/benchmark/processing.rb +5 -0
- data/benchmark/query_count.rb +5 -0
- data/benchmark/support.rb +271 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20260805000000_create_solid_objects_tables.rb +319 -0
- data/docs/adr/0001-postgresql-backend.md +21 -0
- data/docs/adr/0002-jsonb-actor-state.md +21 -0
- data/docs/adr/0003-mailbox-ordering.md +30 -0
- data/docs/adr/0004-activation-leasing.md +21 -0
- data/docs/adr/0005-fencing-tokens.md +25 -0
- data/docs/adr/0006-at-least-once-delivery.md +24 -0
- data/docs/adr/0007-transactional-outbox.md +21 -0
- data/docs/adr/0008-actor-communication.md +21 -0
- data/docs/adr/0009-realtime-updates.md +21 -0
- data/docs/adr/0010-state-versioning.md +29 -0
- data/docs/adr/0011-wake-up-strategy.md +34 -0
- data/docs/adr/0012-not-active-jobs.md +21 -0
- data/docs/adr/0013-database-adapters.md +48 -0
- data/docs/architecture.md +615 -0
- data/docs/benchmarks.md +26 -0
- data/docs/correctness.md +124 -0
- data/docs/database-schema.md +111 -0
- data/docs/development.md +87 -0
- data/docs/implementation-plan.md +518 -0
- data/docs/operations.md +123 -0
- data/docs/realtime.md +51 -0
- data/docs/research/solid_queue.md +545 -0
- data/docs/roadmap.md +53 -0
- data/docs/security.md +61 -0
- data/docs/state-migrations.md +46 -0
- data/examples/application/README.md +16 -0
- data/examples/application/app/actors/chat_room_actor.rb +34 -0
- data/examples/application/app/actors/shopping_cart_actor.rb +79 -0
- data/examples/application/app/controllers/cart_controller.rb +54 -0
- data/examples/application/app/controllers/chat_rooms_controller.rb +44 -0
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +8 -0
- data/examples/application/app/views/actors/shopping_cart_actor/_summary.html.erb +10 -0
- data/examples/application/app/views/cart/show.html.erb +13 -0
- data/examples/application/app/views/chat_rooms/show.html.erb +8 -0
- data/examples/application/config/initializers/solid_objects.rb +23 -0
- data/examples/application/config/routes.rb +20 -0
- data/exe/solid_objects +9 -0
- data/lib/generators/solid_objects/install_generator.rb +21 -0
- data/lib/generators/solid_objects/templates/solid_objects.rb +13 -0
- data/lib/solid_objects/action_cable_broadcast_adapter.rb +19 -0
- data/lib/solid_objects/activation.rb +183 -0
- data/lib/solid_objects/activation_manager.rb +102 -0
- data/lib/solid_objects/actor.rb +271 -0
- data/lib/solid_objects/actor_channel.rb +29 -0
- data/lib/solid_objects/actor_definition.rb +212 -0
- data/lib/solid_objects/actor_registry.rb +65 -0
- data/lib/solid_objects/actor_snapshot.rb +42 -0
- data/lib/solid_objects/actor_view.rb +117 -0
- data/lib/solid_objects/broadcast_executor.rb +162 -0
- data/lib/solid_objects/cli.rb +118 -0
- data/lib/solid_objects/client.rb +153 -0
- data/lib/solid_objects/configuration.rb +168 -0
- data/lib/solid_objects/context.rb +41 -0
- data/lib/solid_objects/database_adapter.rb +82 -0
- data/lib/solid_objects/database_adapters/mysql.rb +22 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +17 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +12 -0
- data/lib/solid_objects/dead_letter_manager.rb +47 -0
- data/lib/solid_objects/dom_identity.rb +38 -0
- data/lib/solid_objects/effect_executor.rb +235 -0
- data/lib/solid_objects/effect_registry.rb +34 -0
- data/lib/solid_objects/engine.rb +33 -0
- data/lib/solid_objects/errors.rb +65 -0
- data/lib/solid_objects/executor.rb +290 -0
- data/lib/solid_objects/instrumentation.rb +10 -0
- data/lib/solid_objects/lease.rb +172 -0
- data/lib/solid_objects/lease_renewer.rb +70 -0
- data/lib/solid_objects/log_subscriber.rb +29 -0
- data/lib/solid_objects/mailbox.rb +178 -0
- data/lib/solid_objects/message_reference.rb +52 -0
- data/lib/solid_objects/process_registry.rb +143 -0
- data/lib/solid_objects/reference.rb +96 -0
- data/lib/solid_objects/reminder_scheduler.rb +168 -0
- data/lib/solid_objects/serialization.rb +99 -0
- data/lib/solid_objects/state.rb +111 -0
- data/lib/solid_objects/stream_name.rb +29 -0
- data/lib/solid_objects/stream_token.rb +59 -0
- data/lib/solid_objects/supervisor.rb +87 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +35 -0
- data/lib/solid_objects/version.rb +5 -0
- data/lib/solid_objects/wake_up.rb +28 -0
- data/lib/solid_objects/worker.rb +139 -0
- data/lib/solid_objects.rb +118 -0
- data/sig/generated/controllers/solid_objects/application_controller.rbs +10 -0
- data/sig/generated/controllers/solid_objects/dead_letters_controller.rbs +11 -0
- data/sig/generated/controllers/solid_objects/instances_controller.rbs +11 -0
- data/sig/generated/helpers/solid_objects/actor_helper.rbs +8 -0
- data/sig/generated/lib/generators/solid_objects/install_generator.rbs +13 -0
- data/sig/generated/lib/solid_objects/action_cable_broadcast_adapter.rbs +8 -0
- data/sig/generated/lib/solid_objects/activation.rbs +65 -0
- data/sig/generated/lib/solid_objects/activation_manager.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor.rbs +183 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +8 -0
- data/sig/generated/lib/solid_objects/actor_definition.rbs +117 -0
- data/sig/generated/lib/solid_objects/actor_registry.rbs +36 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +28 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +56 -0
- data/sig/generated/lib/solid_objects/broadcast_executor.rbs +55 -0
- data/sig/generated/lib/solid_objects/cli.rbs +31 -0
- data/sig/generated/lib/solid_objects/client.rbs +35 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +147 -0
- data/sig/generated/lib/solid_objects/context.rbs +56 -0
- data/sig/generated/lib/solid_objects/database_adapter.rbs +42 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +16 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +13 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +10 -0
- data/sig/generated/lib/solid_objects/dead_letter_manager.rbs +16 -0
- data/sig/generated/lib/solid_objects/dom_identity.rbs +20 -0
- data/sig/generated/lib/solid_objects/effect_executor.rbs +82 -0
- data/sig/generated/lib/solid_objects/effect_registry.rbs +24 -0
- data/sig/generated/lib/solid_objects/engine.rbs +7 -0
- data/sig/generated/lib/solid_objects/errors.rbs +64 -0
- data/sig/generated/lib/solid_objects/executor.rbs +60 -0
- data/sig/generated/lib/solid_objects/instrumentation.rbs +8 -0
- data/sig/generated/lib/solid_objects/lease.rbs +57 -0
- data/sig/generated/lib/solid_objects/lease_renewer.rbs +42 -0
- data/sig/generated/lib/solid_objects/log_subscriber.rbs +11 -0
- data/sig/generated/lib/solid_objects/mailbox.rbs +46 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +37 -0
- data/sig/generated/lib/solid_objects/process_registry.rbs +46 -0
- data/sig/generated/lib/solid_objects/reference.rbs +45 -0
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +55 -0
- data/sig/generated/lib/solid_objects/serialization.rbs +31 -0
- data/sig/generated/lib/solid_objects/state.rbs +72 -0
- data/sig/generated/lib/solid_objects/stream_name.rbs +11 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +19 -0
- data/sig/generated/lib/solid_objects/supervisor.rbs +38 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +14 -0
- data/sig/generated/lib/solid_objects/version.rbs +5 -0
- data/sig/generated/lib/solid_objects/wake_up.rbs +24 -0
- data/sig/generated/lib/solid_objects/worker.rbs +56 -0
- data/sig/generated/lib/solid_objects.rbs +41 -0
- data/sig/generated/models/solid_objects/broadcast.rbs +6 -0
- data/sig/generated/models/solid_objects/claimed_message.rbs +6 -0
- data/sig/generated/models/solid_objects/dead_letter.rbs +6 -0
- data/sig/generated/models/solid_objects/effect.rbs +6 -0
- data/sig/generated/models/solid_objects/instance.rbs +25 -0
- data/sig/generated/models/solid_objects/message.rbs +22 -0
- data/sig/generated/models/solid_objects/process.rbs +6 -0
- data/sig/generated/models/solid_objects/ready_message.rbs +6 -0
- data/sig/generated/models/solid_objects/record.rbs +8 -0
- data/sig/generated/models/solid_objects/reminder.rbs +6 -0
- data/sig/support/framework.rbs +37 -0
- metadata +467 -0
data/docs/correctness.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Correctness and delivery semantics
|
|
2
|
+
|
|
3
|
+
## Contract
|
|
4
|
+
|
|
5
|
+
Messages for one actor are durably enqueued and processed sequentially, at
|
|
6
|
+
least once, by at most one valid activation lease holder at a time. Different
|
|
7
|
+
actor identities may execute concurrently.
|
|
8
|
+
|
|
9
|
+
Exactly-once execution is not provided.
|
|
10
|
+
|
|
11
|
+
## Ordering
|
|
12
|
+
|
|
13
|
+
Enqueue locks the actor instance, allocates `next_message_sequence`, and inserts
|
|
14
|
+
the durable message and ready membership in one transaction. Unique indexes on
|
|
15
|
+
instance/sequence and actor identity/sequence enforce the final invariant.
|
|
16
|
+
|
|
17
|
+
The activation always claims the lowest live sequence for its actor. A
|
|
18
|
+
retryable failure returns that message to ready membership, so it blocks later
|
|
19
|
+
messages until success or dead-lettering.
|
|
20
|
+
|
|
21
|
+
## Ownership and fencing
|
|
22
|
+
|
|
23
|
+
Claiming an actor writes a process UUID, database-time expiration, and a
|
|
24
|
+
monotonically increasing generation. Every successful or failed message
|
|
25
|
+
finalization locks the instance and checks:
|
|
26
|
+
|
|
27
|
+
- owner UUID matches;
|
|
28
|
+
- generation matches;
|
|
29
|
+
- expiration is still in the future according to database time; and
|
|
30
|
+
- claimed-message membership names the same owner and generation.
|
|
31
|
+
|
|
32
|
+
A stale worker can continue running Ruby code, but it cannot commit state,
|
|
33
|
+
completion, or outboxes.
|
|
34
|
+
|
|
35
|
+
## Destruction
|
|
36
|
+
|
|
37
|
+
`reference.destroy` locks and deletes the actor instance in one transaction.
|
|
38
|
+
Cascading foreign keys delete its message history, ready and claimed
|
|
39
|
+
memberships, dead letters, reminders, effects, and broadcasts. The operation
|
|
40
|
+
returns `true` when it deletes an incarnation and `false` when no incarnation
|
|
41
|
+
exists.
|
|
42
|
+
|
|
43
|
+
The deleted instance primary key is also the fencing boundary. An activation
|
|
44
|
+
that was executing before destruction raises `LostActivation` when it attempts
|
|
45
|
+
to commit because its instance no longer exists. Reusing the logical actor
|
|
46
|
+
type and ID creates a fresh incarnation with a different primary key, default
|
|
47
|
+
state, and sequence 1; an old lease cannot address or commit into it.
|
|
48
|
+
|
|
49
|
+
An enqueue racing with destruction is ordered by the instance lock. Work that
|
|
50
|
+
commits first is deleted; work that observes the deletion retries against a new
|
|
51
|
+
incarnation. A claimed reminder cannot resurrect an old incarnation because
|
|
52
|
+
reminder delivery locks the source instance and atomically advances the
|
|
53
|
+
reminder with its mailbox insert.
|
|
54
|
+
|
|
55
|
+
Destruction removes pending and claimed outbox records, but it cannot recall
|
|
56
|
+
external I/O, actor-to-actor delivery, or a broadcast that began before the
|
|
57
|
+
delete. A stale outbox completion cannot record success, enqueue an actor
|
|
58
|
+
callback, or recreate the source actor. Applications must still make external
|
|
59
|
+
effect handlers idempotent.
|
|
60
|
+
|
|
61
|
+
Destruction is synchronous, forbidden from actor context, authorized by
|
|
62
|
+
`authorize_destroy`, and does not run `on_deactivate`.
|
|
63
|
+
|
|
64
|
+
## Crash matrix
|
|
65
|
+
|
|
66
|
+
| Failure point | Durable outcome |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Before message claim | Ready membership remains. |
|
|
69
|
+
| After claim, before handler | Claimed membership is recovered after lease loss. |
|
|
70
|
+
| During handler | In-memory work is lost and the message can run again. |
|
|
71
|
+
| During fenced commit | The database commits every state/message/outbox write or none. |
|
|
72
|
+
| After fenced commit | The message is complete; there is no separate acknowledgement to lose. |
|
|
73
|
+
| During external effect | The stable effect ID is reused, but the provider call can repeat. |
|
|
74
|
+
| During broadcast delivery | The durable row retries; reconnect refresh repairs client staleness. |
|
|
75
|
+
|
|
76
|
+
## Handler idempotency
|
|
77
|
+
|
|
78
|
+
Sequential does not mean once. A message such as `launch` still needs a durable
|
|
79
|
+
guard:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
def launch
|
|
83
|
+
return if status == "launched"
|
|
84
|
+
|
|
85
|
+
self.status = "launched"
|
|
86
|
+
emit :launch_vehicle, launch_id: actor_id
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The guard prevents a repeated state transition. The effect consumer still
|
|
91
|
+
deduplicates with `context.id`.
|
|
92
|
+
|
|
93
|
+
## Atomic boundaries
|
|
94
|
+
|
|
95
|
+
The following are atomic:
|
|
96
|
+
|
|
97
|
+
- actor creation, sequence allocation, durable message, and ready membership;
|
|
98
|
+
- activation owner, expiration, and generation acquisition;
|
|
99
|
+
- ready-to-claimed membership move and attempt increment;
|
|
100
|
+
- state, state version, message result/completion, claimed deletion, effects,
|
|
101
|
+
reminders, outbound actor messages, and observable broadcasts;
|
|
102
|
+
- failed-attempt record plus ready reinsertion or dead letter;
|
|
103
|
+
- effect completion plus its optional actor outcome message;
|
|
104
|
+
- reminder occurrence enqueue plus reminder advancement; and
|
|
105
|
+
- actor destruction plus cascading removal of all actor-owned rows.
|
|
106
|
+
|
|
107
|
+
Actor Ruby code and external I/O are never inside the actor-state transaction.
|
|
108
|
+
|
|
109
|
+
## Ask
|
|
110
|
+
|
|
111
|
+
`ask` is a durable message followed by result polling and wake-up hints. Timeout
|
|
112
|
+
does not cancel the message. The current cross-process fallback is polling;
|
|
113
|
+
therefore polling-only ask is not recommended in latency-sensitive HTTP paths.
|
|
114
|
+
Destroying the actor while an `ask` is waiting removes its message, wakes the
|
|
115
|
+
caller, and raises `SolidObjects::ActorDestroyed`.
|
|
116
|
+
|
|
117
|
+
## Database dependencies
|
|
118
|
+
|
|
119
|
+
PostgreSQL and MySQL use primary-key `FOR UPDATE SKIP LOCKED` attempts after a
|
|
120
|
+
read-only grouped scan of the narrow membership tables. SQLite relies on Active
|
|
121
|
+
Record's immediate write transactions and its one-writer serialization.
|
|
122
|
+
|
|
123
|
+
All three rely on transactional tables, foreign keys, unique constraints,
|
|
124
|
+
database time, and JSON-compatible columns. MySQL requires InnoDB.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Database schema
|
|
2
|
+
|
|
3
|
+
The default prefix is `solid_objects_`. It is configurable before models and
|
|
4
|
+
migrations load. No partial indexes are used.
|
|
5
|
+
|
|
6
|
+
## Tables
|
|
7
|
+
|
|
8
|
+
### `instances`
|
|
9
|
+
|
|
10
|
+
One row per `(actor_type, actor_id)`. Stores JSON state, state version,
|
|
11
|
+
next-message sequence, activation owner/expiration/generation, pause state, and
|
|
12
|
+
lifecycle timestamps.
|
|
13
|
+
|
|
14
|
+
Deleting an instance is the actor-incarnation boundary. Foreign keys cascade
|
|
15
|
+
the delete through messages, ready and claimed memberships, reminders, effects,
|
|
16
|
+
broadcasts, and dead letters. Reusing the logical identity creates a new
|
|
17
|
+
instance primary key with fresh state and message sequence.
|
|
18
|
+
|
|
19
|
+
Indexes:
|
|
20
|
+
|
|
21
|
+
- unique identity: enforces one logical actor;
|
|
22
|
+
- lease expiration/last claim/ID: operational lease scans and cleanup;
|
|
23
|
+
- owner: dead-process cleanup;
|
|
24
|
+
- last used/ID: retention and reconciliation.
|
|
25
|
+
|
|
26
|
+
### `messages`
|
|
27
|
+
|
|
28
|
+
Durable immutable invocation identity and arguments plus sequence, attempt
|
|
29
|
+
count, request/idempotency IDs, result/error, requested availability, and
|
|
30
|
+
execution timestamps. It intentionally has no status column.
|
|
31
|
+
|
|
32
|
+
Indexes:
|
|
33
|
+
|
|
34
|
+
- unique instance/sequence and actor identity/sequence: mailbox order;
|
|
35
|
+
- unique request ID: ask lookup;
|
|
36
|
+
- unique instance/idempotency key: deduplicated enqueue;
|
|
37
|
+
- completion/ID: bounded retention cleanup.
|
|
38
|
+
|
|
39
|
+
### `ready_messages`
|
|
40
|
+
|
|
41
|
+
Small hot membership table for due or future work. A row exists only while its
|
|
42
|
+
message is ready.
|
|
43
|
+
|
|
44
|
+
Indexes:
|
|
45
|
+
|
|
46
|
+
- unique message ID;
|
|
47
|
+
- unique instance/sequence;
|
|
48
|
+
- availability/instance/sequence polling index.
|
|
49
|
+
|
|
50
|
+
### `claimed_messages`
|
|
51
|
+
|
|
52
|
+
Small hot membership table for one message currently owned by an activation.
|
|
53
|
+
It records process UUID, activation generation, and claim time.
|
|
54
|
+
|
|
55
|
+
Indexes:
|
|
56
|
+
|
|
57
|
+
- unique message ID;
|
|
58
|
+
- unique instance ID, enforcing at most one claimed turn for an actor;
|
|
59
|
+
- process/claim time for crash cleanup.
|
|
60
|
+
|
|
61
|
+
### `reminders`
|
|
62
|
+
|
|
63
|
+
Durable one-shot or recurring alarm definitions. Unique instance/name makes
|
|
64
|
+
rescheduling an actor-owned reminder deterministic. Status/next-run/ID drives
|
|
65
|
+
the due scan. Claim ownership references the process registry, and constraints
|
|
66
|
+
limit recurrence intervals, missed-work policies, and statuses.
|
|
67
|
+
|
|
68
|
+
### `effects`
|
|
69
|
+
|
|
70
|
+
Transactional external-effect outbox. It stores stable effect UUID, arguments,
|
|
71
|
+
optional actor outcome messages, attempts, claim ownership, result/error, and
|
|
72
|
+
completion time. Claim ownership references the process registry.
|
|
73
|
+
Status/availability/ID drives delivery; completion/ID drives cleanup.
|
|
74
|
+
|
|
75
|
+
### `broadcasts`
|
|
76
|
+
|
|
77
|
+
Durable observable-change outbox. The unique message/observable key prevents
|
|
78
|
+
duplicate rows for one actor turn. Claim and delivery indexes support retries
|
|
79
|
+
and cleanup.
|
|
80
|
+
|
|
81
|
+
### `dead_letters`
|
|
82
|
+
|
|
83
|
+
Permanent message failures with original identity/arguments, attempts,
|
|
84
|
+
exception summary, bounded backtrace, failure times, and optional retried
|
|
85
|
+
message ID.
|
|
86
|
+
|
|
87
|
+
### `processes`
|
|
88
|
+
|
|
89
|
+
Worker/effect/reminder/broadcast process UUID, kind, hostname, PID, start and
|
|
90
|
+
heartbeat times, metadata, graceful shutdown state, and stop times.
|
|
91
|
+
|
|
92
|
+
## JSON types
|
|
93
|
+
|
|
94
|
+
PostgreSQL uses JSONB. MySQL uses native JSON. SQLite uses Active Record's JSON
|
|
95
|
+
type. Ruby `Marshal` is never used.
|
|
96
|
+
|
|
97
|
+
## Why membership tables
|
|
98
|
+
|
|
99
|
+
Completed messages remain useful history, but they never occupy the polling
|
|
100
|
+
index. Moving a message between ready and claimed tables makes executable state
|
|
101
|
+
physical table membership. The hot indexes stay proportional to live work,
|
|
102
|
+
avoid backend-specific partial indexes, and are portable across all supported
|
|
103
|
+
databases.
|
|
104
|
+
|
|
105
|
+
## Cascading destruction
|
|
106
|
+
|
|
107
|
+
The public `reference.destroy` operation locks the instance row before deleting
|
|
108
|
+
it. Every actor-owned table has a cascading foreign key either directly to the
|
|
109
|
+
instance or through its message row. No application-side bulk delete can leave
|
|
110
|
+
an executable orphan. Process registry rows are not actor-owned and remain
|
|
111
|
+
available for worker lifecycle accounting.
|
data/docs/development.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Development guide
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- Ruby 3.3 or newer
|
|
6
|
+
- Rails 8.0 or newer
|
|
7
|
+
- SQLite 3.35+, PostgreSQL 14+, and MySQL 8.0/InnoDB for the full matrix
|
|
8
|
+
|
|
9
|
+
Install dependencies:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Tests
|
|
16
|
+
|
|
17
|
+
The suite uses Minitest and follows Solid Queue's broad structure: unit tests,
|
|
18
|
+
model/schema tests, engine boot tests, and real database integration tests.
|
|
19
|
+
Concurrency tests use queues and notification barriers instead of timing-only
|
|
20
|
+
sleeps.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
bundle exec rake test
|
|
24
|
+
SOLID_OBJECTS_DATABASE_URL=postgresql://... bundle exec rake test
|
|
25
|
+
SOLID_OBJECTS_DATABASE_URL=mysql2://... bundle exec rake test
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each database run must start from an empty dedicated test database because the
|
|
29
|
+
test helper applies the engine migration.
|
|
30
|
+
|
|
31
|
+
## Inline RBS
|
|
32
|
+
|
|
33
|
+
Ruby source starts with:
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# rbs_inline: enabled
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Methods and instance variables use `# @rbs` annotations. Generate and validate
|
|
40
|
+
signatures with:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle exec rake rbs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This follows the inline convention used by `cardmagic/classifier`.
|
|
47
|
+
|
|
48
|
+
## Formatting and security
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle exec standardrb
|
|
52
|
+
bundle exec rubocop
|
|
53
|
+
bundle exec rake rbs steep
|
|
54
|
+
bundle exec brakeman --force --no-pager -q .
|
|
55
|
+
bundle exec rake
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`.rubocop.yml` is pinned to the policy shape in Solid Queue main at commit
|
|
59
|
+
`86f3d92f1dd68547ec0ebe960fc9933c203d9e51`: Rails Omakase, Ruby 3.3, and its
|
|
60
|
+
schema/template exclusions. Rails Omakase is canonical where the policies
|
|
61
|
+
conflict. Standard remains an additional gate with only its opposing
|
|
62
|
+
array/hash-bracket whitespace cops ignored.
|
|
63
|
+
|
|
64
|
+
Run a failing Minitest first for behavioral changes, implement the smallest
|
|
65
|
+
correct change, rerun the focused test, then the complete database matrix.
|
|
66
|
+
|
|
67
|
+
## Benchmarks
|
|
68
|
+
|
|
69
|
+
Scripts in `benchmark/` cover enqueue, claim, processing, cold actors, a hot
|
|
70
|
+
actor, concurrent actors, ask latency, cache reuse, and query counts. Results
|
|
71
|
+
describe one machine and database configuration; they are not universal
|
|
72
|
+
capacity guarantees.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/enqueue.rb
|
|
76
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/claim.rb
|
|
77
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/processing.rb
|
|
78
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/cold_actors.rb
|
|
79
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/hot_actor.rb
|
|
80
|
+
COUNT=500 CONCURRENCY=4 bundle exec ruby -Ilib benchmark/concurrent_actors.rb
|
|
81
|
+
COUNT=100 bundle exec ruby -Ilib benchmark/ask_latency.rb
|
|
82
|
+
COUNT=500 bundle exec ruby -Ilib benchmark/activation_cache.rb
|
|
83
|
+
bundle exec ruby -Ilib benchmark/query_count.rb
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
SQLite is the default. Set `SOLID_OBJECTS_DATABASE_URL` to benchmark a dedicated
|
|
87
|
+
empty PostgreSQL or MySQL database.
|