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,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.
@@ -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.