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,21 @@
1
+ # ADR 0004: Renewable Database Activation Leases
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ One worker must process one actor at a time without holding a database connection for the actor's in-memory lifetime.
9
+
10
+ ## Decision
11
+
12
+ The actor instance row stores an activation owner, expiration, and generation. PostgreSQL and MySQL workers select an available or expired actor with `FOR UPDATE SKIP LOCKED`. SQLite workers serialize the short claim transaction with `BEGIN IMMEDIATE`. Every strategy changes the owner, advances the generation, and sets a database-time expiration before commit.
13
+
14
+ Lease renewal is a conditional update matching instance, owner, and generation. Graceful release uses the same predicate. Actor execution never holds a database transaction or checked-out connection across application code.
15
+
16
+ ## Consequences
17
+
18
+ - A paused process can lose an activation even if its process heartbeat remains current.
19
+ - Clock comparison uses the backend database's current time.
20
+ - Lease duration must exceed renewal interval and configuration validation enforces it.
21
+ - Expiration enables crash recovery but does not by itself reject stale writes; fencing does.
@@ -0,0 +1,25 @@
1
+ # ADR 0005: Every State Commit Is Fenced
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ An expired lease alone permits a paused worker to resume after a replacement worker has changed state.
9
+
10
+ ## Decision
11
+
12
+ Every activation acquisition monotonically increments `activation_generation`. State/message/outbox commit locks the instance and verifies:
13
+
14
+ - activation owner equals the committing worker
15
+ - activation generation equals the worker's token
16
+ - activation expiration is still in the future
17
+ - the message is owned by the same activation generation
18
+
19
+ If any predicate fails, the transaction raises `SolidObjects::LostActivation` and no state, completion, result, effect, reminder, or broadcast is persisted.
20
+
21
+ ## Consequences
22
+
23
+ - Fencing tokens, not wall-clock expiry alone, prevent stale state writes.
24
+ - Generation overflow is practically unreachable with a signed 64-bit positive counter and is guarded by a check constraint.
25
+ - Any future storage backend must implement an equivalent conditional commit.
@@ -0,0 +1,24 @@
1
+ # ADR 0006: At-Least-Once Mailbox Delivery
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ A worker can die before committing a message, after actor code ran, or after external code performed an irreversible effect. Exactly-once execution cannot be guaranteed by a database mailbox.
9
+
10
+ ## Decision
11
+
12
+ Mailbox delivery is at least once. State mutation, message completion, result persistence, and outbox insertion are atomic. An interrupted message whose commit is absent becomes eligible after lease recovery and may execute again.
13
+
14
+ Actor code receives message ID, request ID, attempt, enqueue time, and idempotency key. Documentation requires idempotency for effects outside the actor commit.
15
+
16
+ Message handlers themselves can run more than once. Sequential execution means one valid activation runs one turn at a time; it does not mean a handler runs once. Handlers for transitions such as `launch`, `checkout`, or `submit` must inspect durable actor state and return safely when the transition already happened. External calls belong in an outbox and still require downstream idempotency.
17
+
18
+ ## Consequences
19
+
20
+ - No exactly-once claim appears in the API or documentation.
21
+ - Pure state transitions are safe because failed transactions roll back.
22
+ - Application handlers need durable state guards for non-repeatable logical transitions.
23
+ - External systems require idempotency keys or deduplication.
24
+ - `ask` timing out does not cancel its durable message.
@@ -0,0 +1,21 @@
1
+ # ADR 0007: External Effects and Broadcasts Use Outboxes
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ Calling an external system or broadcasting before the actor transaction commits can create phantom effects. Calling it after commit without a durable record can lose effects.
9
+
10
+ ## Decision
11
+
12
+ `emit` stages an effect. Successful message commit inserts the effect in the same database transaction as state and message completion. Observable changes likewise insert broadcast rows in that transaction. Separate workers claim and deliver outbox rows at least once.
13
+
14
+ Effect handlers must accept a stable effect ID as their idempotency key. Optional success and failure messages are enqueued back to the actor after delivery outcome is durably recorded.
15
+
16
+ ## Consequences
17
+
18
+ - Slow network I/O never runs inside the actor-state transaction.
19
+ - Delivery may be duplicated after a worker crash.
20
+ - Consumers and effect handlers must be idempotent.
21
+ - Outbox retention and replay are operational concerns.
@@ -0,0 +1,21 @@
1
+ # ADR 0008: Actor Communication Is Asynchronous
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ If actor A synchronously waits for actor B while B waits for A, sequential actors deadlock. Waiting also extends activation occupation and database pressure.
9
+
10
+ ## Decision
11
+
12
+ Actor references support durable asynchronous `tell`. `ask` is a caller-facing request/response operation and must not be called from actor code. Actor code uses staged actor messages or request/result message pairs.
13
+
14
+ Messages staged during actor execution are delivered through a transactional outbox so they exist if and only if the source message commits.
15
+
16
+ ## Consequences
17
+
18
+ - Cyclic synchronous actor calls are rejected in actor context.
19
+ - Workflows spanning actors are explicit state machines or sagas.
20
+ - Cross-actor message order is not globally defined.
21
+ - Each target actor allocates its own mailbox sequence at delivery time.
@@ -0,0 +1,21 @@
1
+ # ADR 0009: Realtime Updates Use a Durable Broadcast Outbox
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ Action Cable broadcasts are online-only. A transaction can roll back, a broadcast process can crash, and a client can disconnect.
9
+
10
+ ## Decision
11
+
12
+ The executor evaluates declared observables before and after a successful message. Changed values create broadcast outbox records inside the message commit. A broadcast worker delivers Turbo Stream replacements after commit.
13
+
14
+ One `solid_object` block creates one signed Action Cable subscription and contains stable targets for multiple observables and components. Subscription authorization runs after token verification and before streaming. Reconnect refresh reads current actor state; the broadcast stream is an optimization, not state.
15
+
16
+ ## Consequences
17
+
18
+ - Disconnected clients may miss individual broadcasts but can converge by refresh.
19
+ - Broadcast delivery is at least once and replacements must be idempotent.
20
+ - Actor IDs and signed stream names are identifiers, not authorization.
21
+ - Realtime support is optional and loaded only when Action Cable and Turbo are present.
@@ -0,0 +1,29 @@
1
+ # ADR 0010: Explicit Actor State Versions
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ Persistent actors can outlive application releases. During a rolling deployment, old and new worker processes can briefly execute different actor code.
9
+
10
+ ## Decision
11
+
12
+ Every actor class declares a current integer state version. Migrations must form an unbroken, one-step chain from stored version to current version. The runtime must refuse to activate an actor whose stored state version is newer than the actor class declares.
13
+
14
+ Process metadata includes an application/runtime version. Deployments that change state shape must keep old code able to read the new representation or drain old workers before new-version messages can commit. Automatic downgrade is unsupported.
15
+
16
+ Operating rules are normative:
17
+
18
+ - Additive, backward-readable state changes do not require a version bump.
19
+ - Destructive or reinterpretive changes use an expand/contract deployment across at least two releases.
20
+ - A new migration may run only while every live worker can read the resulting representation, or after incompatible workers are drained.
21
+ - Actor state migrations are permanent history. They cannot be squashed like Rails schema migrations because an actor may reactivate from a blob written years earlier.
22
+
23
+ ## Consequences
24
+
25
+ - Rolling safety is an application responsibility and is never hidden.
26
+ - Additive, backward-readable changes are preferred.
27
+ - Destructive migrations require a coordinated drain or two-phase deployment.
28
+ - Published migration steps remain in the actor code or an equivalent archival migration registry.
29
+ - Migration errors fail the message without modifying stored state.
@@ -0,0 +1,34 @@
1
+ # ADR 0011: Pluggable Wake-Up Strategy
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ Polling adds latency and database queries. PostgreSQL notifications are transactional but transient, session-scoped, coalesced in some cases, and subject to listener startup races. MySQL has no equivalent notification primitive. Some applications already operate Redis and may choose it as a low-latency accelerator, but Redis is not required infrastructure and cannot become durable truth.
9
+
10
+ ## Decision
11
+
12
+ Database rows remain the only durable source of work and results. Wake-up adapters only tell workers and `ask` waiters to re-query those rows.
13
+
14
+ The interface supports:
15
+
16
+ - Adaptive polling on every backend as the required fallback.
17
+ - An in-process condition signal for same-process workers and waiters.
18
+ - PostgreSQL `LISTEN/NOTIFY`.
19
+ - Optional Redis Pub/Sub for applications that already operate Redis.
20
+
21
+ MySQL uses polling or optional Redis. SQLite uses polling plus the in-process signal; multi-host SQLite is outside its supported operating model.
22
+
23
+ The coordination-overhead latency budget, measured from durable enqueue or completion commit until a waiting worker or caller begins its confirming query, is p99 at or below 100 milliseconds when a healthy cross-process wake-up adapter is enabled. Polling-only deployments accept up to the configured polling interval on each wait leg. End-to-end `ask` latency additionally includes queueing and actor execution and cannot have a library-wide bound.
24
+
25
+ Polling-only `ask` is intended for background callers, scripts, and control paths. It is not recommended in latency-sensitive Rails request handlers. A request handler may use it only with an explicit timeout and an operationally verified wake-up adapter and actor latency budget.
26
+
27
+ ## Consequences
28
+
29
+ - Missing or duplicate notifications do not change correctness.
30
+ - PostgreSQL listeners need one dedicated connection while active.
31
+ - A reconnecting PostgreSQL listener must commit `LISTEN`, inspect current state, and then wait.
32
+ - Redis loss only increases latency and never loses durable work.
33
+ - Every adapter retains periodic polling to close startup, reconnect, and missed-message races.
34
+ - Notification payloads never contain actor arguments or results.
@@ -0,0 +1,21 @@
1
+ # ADR 0012: Actor Messages Are Not Active Jobs
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+
6
+ ## Context
7
+
8
+ Active Job and Solid Queue provide durable independent jobs, but the actor contract couples an ordered mailbox, one logical activation, mutable state, fencing, request results, and transactional outboxes.
9
+
10
+ ## Decision
11
+
12
+ Solid Objects implements its own mailbox and runtime rather than representing each actor message as an ordinary Active Job.
13
+
14
+ Active Job may be used by host applications around Solid Objects, but it is not part of actor claiming, ordering, retries, state commit, effects, or reminders.
15
+
16
+ ## Consequences
17
+
18
+ - Per-actor sequentiality is mandatory instead of opt-in concurrency control.
19
+ - Message retry and serialization semantics are owned by Solid Objects.
20
+ - State and message completion can be one database transaction.
21
+ - The gem has more runtime code than an Active Job wrapper, but its guarantees are explicit and enforceable.
@@ -0,0 +1,48 @@
1
+ # ADR 0013: MySQL, PostgreSQL, and SQLite Backends
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-08-05
5
+ - Supersedes: ADR 0001
6
+
7
+ ## Context
8
+
9
+ Solid Objects must work with the three databases Rails applications most commonly use without silently weakening mailbox ordering, activation ownership, or fencing.
10
+
11
+ The databases expose different coordination features:
12
+
13
+ - PostgreSQL and MySQL InnoDB support row locks and `FOR UPDATE SKIP LOCKED`.
14
+ - SQLite has no row-level `SKIP LOCKED`, permits one writer at a time, and supports `BEGIN IMMEDIATE`.
15
+ - PostgreSQL has JSONB, MySQL has JSON, and SQLite exposes Rails JSON serialization over its dynamic storage types.
16
+
17
+ ## Decision
18
+
19
+ Solid Objects supports:
20
+
21
+ - PostgreSQL 14 or newer
22
+ - MySQL 8.0 or newer using InnoDB
23
+ - SQLite 3.35 or newer
24
+
25
+ A database adapter capability object owns:
26
+
27
+ - Non-blocking claim strategy
28
+ - Database current-time expression
29
+ - JSON column migration type
30
+
31
+ Explicit lock/deadlock/busy retry classification remains a hardening milestone.
32
+
33
+ PostgreSQL and MySQL use bounded `FOR UPDATE SKIP LOCKED` claims in short transactions.
34
+
35
+ SQLite uses the immediate write transactions provided by Rails 8 for coordination writes. Its single writer serializes candidate selection and claim. SQLite is correct for the tested contract but intended for development and modest single-host workloads because write concurrency is database-wide; explicit busy-error classification and retry are not yet implemented.
36
+
37
+ All backends use the same conditional fenced commit predicate. Adapter differences may change throughput and lock granularity, never delivery semantics.
38
+
39
+ Ready and claimed work live in narrow membership tables with ordinary composite indexes. Completed history never accumulates in the polling index, and no backend-specific partial index is required.
40
+
41
+ ## Consequences
42
+
43
+ - The schema generator emits PostgreSQL JSONB, MySQL JSON, and SQLite JSON-compatible columns.
44
+ - PostgreSQL, MySQL, and SQLite use the same ready- and claimed-membership tables.
45
+ - MySQL tests require InnoDB and reject nontransactional table engines.
46
+ - SQLite uses the adapter's configured busy timeout; WAL mode is an application operating choice.
47
+ - Cross-backend integration tests exercise sequence allocation, claiming, lease renewal, fencing, atomic completion, and crash recovery.
48
+ - PostgreSQL remains the benchmark and high-concurrency reference backend.