solid_objects 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d95ae7c1b6791ce00951e73eaf535a7b0b357c6708112509544de82eb59314d
4
- data.tar.gz: a6a9ef715c217e38959438d1ab549447e0814c621746967dece190ae46b3108b
3
+ metadata.gz: 654720ae19caa970f403b5cded61287e1f6a9eb9d731ff90fd9f4f103d55fa53
4
+ data.tar.gz: 256712423dd88685643a0345de44eb774efbca12188c78c9e89571ef884b59fc
5
5
  SHA512:
6
- metadata.gz: d604e195d678ae5f2ae24932fd0431a7076d0f8258b9e064bbc463042b911512edc7a6b613673b6731256d0b0603311708beb2158d79e421dc85b5febfb37f4d
7
- data.tar.gz: cd817466462c8d0c843fa9766df1f2190ed3b477466661e32a47859f7fc63349327af7cb077d75ab5a43b621bb3ee98e321363b007ff5ad19e311123d871e040
6
+ metadata.gz: f1e43da5f13c33558bdb187b52c55b55093786d3ec8af9f49d4ffc123ade274d2f5483dd8975c6a638152dfc9ae1c4cf416d8b1ea94cb1383ac30b33ef7548aa
7
+ data.tar.gz: f8cb10850b5b3748450ffaf741417866ed360437657c5d21836312018f0d6b27109cd69c4035f770f6cf643227ab086ae7eedc1b0a14245103cfe517a008f49c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 - 2026-08-06
4
+
5
+ - Add `solid_objects:doctor` for configuration, schema, policy, runtime, and
6
+ workerless synchronous round-trip verification.
7
+ - Add onboarding guidance for fit decisions, worker requirements,
8
+ authorization, performance and row growth, retention, Sorbet, RuboCop, and
9
+ migrations from existing state stores.
10
+ - Make the early-Action View engine boot regression explicit.
11
+
3
12
  ## 0.2.0 - 2026-08-06
4
13
 
5
14
  - Make direct actor methods synchronous Durable Object-style RPC.
data/README.md CHANGED
@@ -20,8 +20,13 @@ class Counter < SolidObjects::Actor
20
20
  end
21
21
  end
22
22
 
23
- # from anywhere in your app addressed by name:
24
- Counter.ref("global").increment(amount: 5)
23
+ # Synchronous caller-assisted RPC. No worker fleet is required.
24
+ counter = Counter.ref("global")
25
+ count = counter.increment(amount: 5)
26
+ current_count = counter.value
27
+
28
+ # Durable fire-and-forget delivery. A worker processes it later.
29
+ message = counter.async(:increment, amount: 5)
25
30
  ```
26
31
 
27
32
  `Counter / global` is a logical identity. Like a Durable Object named with
@@ -30,6 +35,23 @@ locating a Ruby object. Solid Objects activates it when work arrives, commits
30
35
  its ordered turns one at a time, persists its state, and deactivates it when
31
36
  idle. Different identities can run concurrently.
32
37
 
38
+ The invocation model is the first adoption decision:
39
+
40
+ | Call | Returns | Worker fleet required? |
41
+ | --- | --- | --- |
42
+ | `counter.increment(amount: 5)` | Committed handler result | No |
43
+ | `counter.sync(:increment, amount: 5)` | Committed handler result | No |
44
+ | `counter.value` | Deeply frozen state snapshot | No |
45
+ | `counter.async(:increment, amount: 5)` | `MessageReference` immediately | Yes |
46
+
47
+ Direct methods and `sync` durably enqueue the call, then the Rails caller helps
48
+ execute the actor through the same mailbox, lease, and fencing path as a
49
+ worker. `async` only enqueues; a runtime process handles it later.
50
+
51
+ Before adopting a latency-sensitive or high-volume surface, read
52
+ [Is Solid Objects a good fit?](docs/fit.md) and the
53
+ [measured performance and row-growth costs](docs/benchmarks.md).
54
+
33
55
  This is a port of the programming model, not Cloudflare's edge runtime or
34
56
  platform. Read the conceptual overview at [solidobjects.dev](https://solidobjects.dev/)
35
57
  and the exact Rails guarantees in [Correctness and delivery semantics](docs/correctness.md).
@@ -43,6 +65,7 @@ but the project does not yet claim production readiness. See
43
65
  - [Cloudflare Durable Objects for Rails](#cloudflare-durable-objects-for-rails)
44
66
  - [Reactive ERB](#reactive-erb)
45
67
  - [Installation](#installation)
68
+ - [Worker requirements](#worker-requirements)
46
69
  - [Defining an actor](#defining-an-actor)
47
70
  - [Actor identity](#actor-identity)
48
71
  - [Invoking an object](#invoking-an-object)
@@ -187,10 +210,17 @@ Add the gem, install its initializer and migration, then migrate:
187
210
  bundle add solid_objects
188
211
  bin/rails generate solid_objects:install
189
212
  bin/rails db:migrate
213
+ bin/rails solid_objects:doctor
190
214
  ```
191
215
 
192
- The generated initializer denies all externally initiated operations. Replace
193
- the policy blocks with application-specific authorization before sending
216
+ The doctor validates configuration and required schema shape, reports
217
+ authorization posture and live runtime roles, and completes a real synchronous
218
+ actor round-trip without a worker. It checks required tables and columns instead
219
+ of a copied migration timestamp, which the host application rewrites. It exits
220
+ unsuccessfully when configuration, schema, or the round-trip is broken.
221
+
222
+ The generated initializer is intentionally inert: all five policies deny by
223
+ default. Replace them with application-specific authorization before sending
194
224
  messages, querying state, destroying actors, subscribing to streams, or
195
225
  mounting administration routes:
196
226
 
@@ -205,16 +235,62 @@ end
205
235
  ```
206
236
 
207
237
  Knowledge of an actor ID or signed stream token is never authorization.
238
+ Read the [policy reference and tenant-aware example](docs/authorization.md)
239
+ before opening a policy. Unconditionally allowing message and query calls is
240
+ reasonable only for a controlled server-side pilot. Keep destroy,
241
+ subscription, and administration denied until each has an authenticated
242
+ caller.
208
243
 
209
- Start the runtime for asynchronous messages, effects, reminders, and
210
- broadcasts:
244
+ The engine uses the application's primary Active Record connection by default.
245
+ See [Database support](#database-support) for a separate database configuration.
246
+
247
+ ### Host application tooling
248
+
249
+ Installed engine migrations are copied as
250
+ `db/migrate/*_create_solid_objects_tables.solid_objects.rb`. If the host enables
251
+ `Rails/CreateTableWithTimestamps`, exclude engine-owned migrations rather than
252
+ editing their intentionally specialized hot tables:
253
+
254
+ ```yaml
255
+ Rails/CreateTableWithTimestamps:
256
+ Exclude:
257
+ - "db/migrate/*.solid_objects.rb"
258
+ ```
259
+
260
+ Solid Objects ships inline RBS signatures, not RBI files. Sorbet applications
261
+ can generate the gem RBI with:
262
+
263
+ ```bash
264
+ bundle exec tapioca gem solid_objects
265
+ ```
266
+
267
+ ## Worker requirements
268
+
269
+ Synchronous actors can be adopted without adding a long-running process. Start
270
+ the runtime when the feature introduces asynchronous delivery or outboxes:
271
+
272
+ | Feature | Runtime roles required |
273
+ | --- | --- |
274
+ | Direct actor method or explicit `sync` | None; the caller executes it |
275
+ | Attribute or declared query read | None; the caller executes it |
276
+ | `destroy` | None |
277
+ | `async` including delayed delivery | Actor worker |
278
+ | One-shot or recurring `schedule` | Reminder scheduler and actor worker |
279
+ | `emit` without an actor callback | Effect worker |
280
+ | `emit` with success or failure callback | Effect worker and actor worker |
281
+ | Actor-to-actor `async` or `send_to` | Effect worker and actor worker |
282
+ | Observable Turbo updates | Broadcast worker, Action Cable, and the actor execution path |
283
+ | Initial `solid_object` server render | No Solid Objects worker; normal Rails rendering |
284
+
285
+ One command starts every Solid Objects role:
211
286
 
212
287
  ```bash
213
288
  bundle exec solid_objects start
214
289
  ```
215
290
 
216
- The engine uses the application's primary Active Record connection by default.
217
- See [Database support](#database-support) for a separate database configuration.
291
+ Deploy and monitor that process before enabling any feature marked as requiring
292
+ a runtime role. A missing worker never makes a durable `async` message
293
+ disappear, but it leaves the message pending indefinitely.
218
294
 
219
295
  ## Defining an actor
220
296
 
@@ -396,6 +472,10 @@ The caller receives `SolidObjects::Rejected` with a stable code, message, and
396
472
  JSON-compatible details. The rejected message remains durable for audit, actor
397
473
  state is rolled back, and no later mailbox turn is blocked.
398
474
 
475
+ `Rejected#code` is a `String`, even when `reject` receives a symbol. Codes must
476
+ match `\A[a-z][a-z0-9_]*\z`; invalid codes raise `ArgumentError` when the
477
+ handler calls `reject`.
478
+
399
479
  ### Redelivery
400
480
 
401
481
  Sequential does not mean once. A handler can run again after a process crash or
@@ -686,6 +766,13 @@ Do not use it for stateless work, bulk pipelines, CPU-heavy computation,
686
766
  cross-actor transactions, slow network calls inside handlers, or domains that
687
767
  are clearer as normalized Active Record models and direct service objects.
688
768
 
769
+ High-QPS request reads, rate-limit counters, impression pipelines, large JSON
770
+ documents, and latency budgets that cannot tolerate several coordination
771
+ transactions are explicit anti-patterns. Read the full
772
+ [fit and anti-pattern guide](docs/fit.md) before migrating an existing
773
+ surface, and use the [legacy-state migration cookbook](docs/migrating-existing-state.md)
774
+ for staged cutovers.
775
+
689
776
  ## Comparisons
690
777
 
691
778
  | Tool | What Solid Objects adds or changes |
@@ -0,0 +1,5 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative "support"
4
+
5
+ SolidObjectsBenchmark.adoption_latency
data/benchmark/support.rb CHANGED
@@ -145,6 +145,32 @@ module SolidObjectsBenchmark
145
145
  "p99=#{milliseconds(percentile(sorted, 0.99))}ms"
146
146
  end
147
147
 
148
+ # @rbs () -> void
149
+ def adoption_latency
150
+ instance_count = SolidObjects::Instance.count
151
+ message_count = SolidObjects::Message.count
152
+
153
+ cold_elapsed = Benchmark.realtime do
154
+ CounterActor.ref("adoption-cold").increment
155
+ end
156
+
157
+ reference = CounterActor.ref("adoption-warm")
158
+ reference.increment
159
+ write_samples = Array.new(count) do
160
+ Benchmark.realtime { reference.increment }
161
+ end
162
+ read_samples = Array.new(count) do
163
+ Benchmark.realtime { reference.count }
164
+ end
165
+
166
+ puts "first cold call: #{milliseconds(cold_elapsed)}ms"
167
+ puts latency_summary("warm writes", write_samples)
168
+ puts latency_summary("ordered reads", read_samples)
169
+ puts "durable row growth: " \
170
+ "instances=+#{SolidObjects::Instance.count - instance_count}, " \
171
+ "messages=+#{SolidObjects::Message.count - message_count}"
172
+ end
173
+
148
174
  # @rbs () -> void
149
175
  def activation_cache
150
176
  reference = CounterActor.ref("cache")
@@ -255,6 +281,15 @@ module SolidObjectsBenchmark
255
281
  samples.fetch(((samples.length - 1) * fraction).ceil)
256
282
  end
257
283
 
284
+ # @rbs (String, Array[Float]) -> String
285
+ def latency_summary(name, samples)
286
+ sorted = samples.sort
287
+ "#{name} #{samples.length} calls: " \
288
+ "median=#{milliseconds(percentile(sorted, 0.50))}ms " \
289
+ "min=#{milliseconds(sorted.first)}ms " \
290
+ "max=#{milliseconds(sorted.last)}ms"
291
+ end
292
+
258
293
  # @rbs (Float) -> String
259
294
  def milliseconds(seconds)
260
295
  format("%.1f", seconds * 1_000)
@@ -0,0 +1,85 @@
1
+ # Authorization policies
2
+
3
+ Solid Objects treats actor identities as identifiers, never capabilities.
4
+ Knowing an actor ID, message ID, or signed stream token grants no permission.
5
+ All five policies deny by default, so a generated installation is
6
+ intentionally inert until the host application defines its trust boundary.
7
+
8
+ ## Policy reference
9
+
10
+ | Policy | Gates | Caller context | Risk if opened globally |
11
+ | --- | --- | --- | --- |
12
+ | `authorize_message` | Direct actor methods, explicit `sync` messages, and public `async` enqueue | Value passed as `authorization_context:`; often a user, service principal, or trusted internal marker | Anyone reaching the call site can mutate any known actor identity |
13
+ | `authorize_query` | Attribute reads, declared queries, observable reads, and component reads | Explicit call context or the Rails view context supplied by `solid_object` | Actor state can leak across users or tenants |
14
+ | `authorize_destroy` | `reference.destroy` | Value passed as `authorization_context:` | Complete actor state, mailbox, reminders, and pending outboxes can be deleted |
15
+ | `authorize_subscription` | Action Cable subscription to one actor stream | The `ActionCable::Connection` object | Clients can receive future observable updates for other actors |
16
+ | `authorize_administration` | Engine administration controllers, process inspection/cleanup, and dead-letter inspection/retry | Rails controller or `{ source: "cli" }` | Operational metadata, arguments, errors, and retries become exposed or mutable |
17
+
18
+ Internal reminder, effect-callback, and actor-to-actor deliveries come from
19
+ already committed runtime rows and do not re-enter the public client policy.
20
+
21
+ ## A tenant-aware policy
22
+
23
+ Pass the authenticated user as the call context:
24
+
25
+ ```ruby
26
+ cart = ShoppingCart.ref(Current.user.id)
27
+ cart.add_item(
28
+ product_id: "shirt-123",
29
+ authorization_context: Current.user
30
+ )
31
+ ```
32
+
33
+ Authorize only the matching user and actor type:
34
+
35
+ ```ruby
36
+ SolidObjects.configure do |configuration|
37
+ owns_actor = lambda do |actor_type:, actor_id:, authorization_context:, **|
38
+ user = authorization_context
39
+
40
+ actor_type == "ShoppingCart" &&
41
+ user.present? &&
42
+ actor_id == user.id.to_s
43
+ end
44
+
45
+ configuration.authorize_message = owns_actor
46
+ configuration.authorize_query = owns_actor
47
+ configuration.authorize_destroy = owns_actor
48
+
49
+ configuration.authorize_subscription = lambda do |actor_type:, actor_id:, authorization_context:|
50
+ connection = authorization_context
51
+
52
+ actor_type == "ShoppingCart" &&
53
+ connection.current_user.present? &&
54
+ actor_id == connection.current_user.id.to_s
55
+ end
56
+
57
+ configuration.authorize_administration = lambda do |authorization_context:, **|
58
+ context = authorization_context
59
+ user = context.respond_to?(:current_user) ? context.current_user : nil
60
+
61
+ user&.administrator?
62
+ end
63
+ end
64
+ ```
65
+
66
+ The policy receives normalized actor type and ID strings, message name and
67
+ arguments where relevant, and the context supplied by the caller. Avoid
68
+ authorizing from arguments alone; bind the actor identity to the authenticated
69
+ principal and tenant.
70
+
71
+ ## Server-side-only pilots
72
+
73
+ Allowing `authorize_message` and `authorize_query` unconditionally can be a
74
+ reasonable short-lived pilot only when every call site is trusted server code,
75
+ actor IDs cannot come from an unauthorized request, and the feature is not
76
+ exposed through Action Cable or administration routes.
77
+
78
+ Keep `authorize_destroy`, `authorize_subscription`, and
79
+ `authorize_administration` denied until each feature has an explicit policy.
80
+ Replace unconditional policies before exposing actor IDs to controllers, API
81
+ clients, MCP tools, jobs carrying user input, or browser subscriptions.
82
+
83
+ Run `bin/rails solid_objects:doctor` after configuration. Its neutral policy
84
+ probe is deliberately conservative: a context-aware policy may correctly warn
85
+ because it denies a `nil` context.
data/docs/benchmarks.md CHANGED
@@ -1,10 +1,31 @@
1
- # Local benchmarks
1
+ # Performance and storage costs
2
2
 
3
3
  These numbers are development measurements, not universal capacity guarantees.
4
4
  They include the runtime's Active Record and database query overhead and will
5
5
  vary with hardware, schema size, connection pools, durability settings, and
6
6
  contention.
7
7
 
8
+ ## Production-shaped adoption measurement
9
+
10
+ An adoption evaluation measured Solid Objects 0.2.0 from a macOS Rails process
11
+ against Docker MySQL 8 over a published TCP port. The host used Rails 8.1, Ruby
12
+ 4.0.5, roughly 165 gems, and an approximately 2,200-line schema.
13
+
14
+ | Operation | Existing key-value row | Solid Objects |
15
+ | --- | ---: | ---: |
16
+ | Write | median 4.7 ms | median 60 ms, minimum 33 ms, maximum 163 ms |
17
+ | Read | median 0.2 ms | median 28 ms |
18
+ | First call for a cold identity | approximately 5 ms | 315 ms |
19
+
20
+ This is not a controlled cross-database benchmark and no sample count was
21
+ recorded. It is still useful adoption evidence: a synchronous actor call is not
22
+ a substitute for a direct indexed row read when single-digit-millisecond
23
+ latency is the requirement. The first call includes actor-instance creation,
24
+ caller-process registration, message enqueue, activation claim, handler
25
+ execution, fenced commit, and activation release.
26
+
27
+ ## Project development benchmark
28
+
8
29
  Measured 2026-08-06 on an Apple M5 with 24 GB RAM, Ruby 4.0.5, Rails 8.1.3.1,
9
30
  and SQLite 3.51.0. Each throughput scenario used 200 operations; the concurrent
10
31
  scenario used four worker threads.
@@ -21,6 +42,55 @@ scenario used four worker threads.
21
42
  | Activation reuse | 98.0%, four activations for 200 messages |
22
43
  | Queries for one message turn | 29 |
23
44
 
45
+ The difference between the SQLite development result and the MySQL adoption
46
+ result is why Solid Objects does not publish one latency promise. Network
47
+ topology, adapter behavior, host schema, logging, callbacks, and contention all
48
+ matter.
49
+
50
+ ## Durable row growth
51
+
52
+ The storage cost is deterministic even when latency is not:
53
+
54
+ - the first call for one actor identity inserts one
55
+ `solid_objects_instances` row;
56
+ - every direct, `sync`, query, attribute read, or `async` call inserts one
57
+ permanent `solid_objects_messages` row;
58
+ - ready and claimed membership rows exist only while the call is pending or
59
+ executing;
60
+ - one caller process row is registered per application process that performs
61
+ synchronous calls;
62
+ - effects and observable changes add outbox rows; and
63
+ - reminders add one row per named actor reminder.
64
+
65
+ Attribute reads are therefore not free snapshots from the instance row. They
66
+ are ordered durable query messages and grow message history exactly like
67
+ writes.
68
+
69
+ Version 0.2 has cleanup indexes but no built-in pruning command. Budget message
70
+ growth as:
71
+
72
+ ```text
73
+ daily durable messages = daily actor writes + daily actor reads + daily callbacks
74
+ ```
75
+
76
+ Review the [retention requirements](operations.md#retention-and-backups) before
77
+ adopting a high-volume surface.
78
+
79
+ ## Measure the host application
80
+
81
+ Run the adoption benchmark against a dedicated empty database with the same
82
+ adapter and topology as production:
83
+
84
+ ```bash
85
+ COUNT=25 \
86
+ SOLID_OBJECTS_DATABASE_URL=mysql2://localhost/solid_objects_benchmark \
87
+ bundle exec ruby -Ilib benchmark/adoption_latency.rb
88
+ ```
89
+
90
+ It reports the first cold call, warm synchronous writes, ordered reads, and
91
+ durable instance/message growth. Run it near the application process, with
92
+ production-like TLS and network boundaries where applicable.
93
+
24
94
  The scripts and invocation examples are in the
25
95
  [development guide](development.md#benchmarks). PostgreSQL and MySQL should be
26
96
  benchmarked independently before selecting production capacity.
data/docs/development.md CHANGED
@@ -66,12 +66,13 @@ correct change, rerun the focused test, then the complete database matrix.
66
66
 
67
67
  ## Benchmarks
68
68
 
69
- Scripts in `benchmark/` cover enqueue, claim, processing, cold actors, a hot
70
- actor, concurrent actors, synchronous latency, cache reuse, and query counts.
71
- Results describe one machine and database configuration; they are not universal
72
- capacity guarantees.
69
+ Scripts in `benchmark/` cover adoption latency and durable row growth, enqueue,
70
+ claim, processing, cold actors, a hot actor, concurrent actors, synchronous
71
+ latency, cache reuse, and query counts. Results describe one machine and
72
+ database configuration; they are not universal capacity guarantees.
73
73
 
74
74
  ```bash
75
+ COUNT=25 bundle exec ruby -Ilib benchmark/adoption_latency.rb
75
76
  COUNT=500 bundle exec ruby -Ilib benchmark/enqueue.rb
76
77
  COUNT=500 bundle exec ruby -Ilib benchmark/claim.rb
77
78
  COUNT=500 bundle exec ruby -Ilib benchmark/processing.rb
data/docs/fit.md ADDED
@@ -0,0 +1,90 @@
1
+ # Is Solid Objects a good fit?
2
+
3
+ Solid Objects trades database work and retained message history for one strong
4
+ property: all committed turns for one durable identity execute in order behind
5
+ a fenced activation. Adopt it when that coordination property removes
6
+ application-level locking, recovery, and scheduling code that would otherwise
7
+ be difficult to make correct.
8
+
9
+ ## Strong fit signals
10
+
11
+ Solid Objects is a good candidate when most of these are true:
12
+
13
+ - State belongs to one durable identity such as a cart, room, device, session,
14
+ workflow, or user-specific schedule.
15
+ - Writes for that identity must be serialized.
16
+ - The state is naturally a bounded JSON document.
17
+ - The object needs per-identity reminders, transactional external effects, or
18
+ reactive Rails views.
19
+ - Different identities should run concurrently while one hot identity remains
20
+ deliberately sequential.
21
+ - A durable mailbox and at-least-once retry are more valuable than minimum
22
+ request latency.
23
+ - The application can operate and monitor additional database tables and, for
24
+ asynchronous features, a Solid Objects runtime process.
25
+
26
+ Typical fits include checkout state machines, collaborative rooms, device
27
+ twins, durable assessments, approval workflows, and user-specific scheduling.
28
+
29
+ ## Poor fit and anti-patterns
30
+
31
+ Prefer ordinary Active Record, cache storage, Active Job, or an event pipeline
32
+ when any of these dominate:
33
+
34
+ - High-QPS request-path reads. Every actor attribute read is an ordered durable
35
+ message, not a direct `SELECT`, and retains a message-history row.
36
+ - Hot counters such as abuse limits, impressions, page views, or metrics. One
37
+ identity is a serialization point and cannot gain throughput by adding
38
+ workers.
39
+ - High-volume append workloads. Actor state rewrites a JSON document and the
40
+ mailbox retains one durable message per call.
41
+ - Latency budgets where tens of milliseconds are already unacceptable.
42
+ - Large, relational, or query-heavy state. Keep that data normalized in
43
+ application tables.
44
+ - CPU-heavy work or slow network I/O inside a handler.
45
+ - Cross-actor transactions or synchronous actor-to-actor call graphs.
46
+ - State that is clearer as a normal record with database constraints and direct
47
+ service methods.
48
+
49
+ A rate limiter is usually a poor actor: it is hot, request-critical, and often
50
+ expires rather than requiring permanent message history. An impressions
51
+ pipeline is also a poor actor: its value is high-throughput append and
52
+ aggregation, not serialized mutable state.
53
+
54
+ ## Cost model
55
+
56
+ Every synchronous or asynchronous invocation:
57
+
58
+ - inserts one permanent `solid_objects_messages` row;
59
+ - briefly occupies one ready or claimed membership row;
60
+ - performs several short coordination transactions; and
61
+ - may add effect, broadcast, or reminder records.
62
+
63
+ The first call for an identity also inserts one `solid_objects_instances` row.
64
+ Each application process that performs synchronous calls registers one caller
65
+ process row. Actor state is rewritten as a JSON value on each successful
66
+ mutation.
67
+
68
+ There is no built-in retention command in 0.2. Completed message history grows
69
+ until the host application implements a reviewed retention policy. See
70
+ [performance measurements](benchmarks.md) and
71
+ [retention guidance](operations.md#retention-and-backups).
72
+
73
+ ## Decision checklist
74
+
75
+ Before adopting an actor, answer:
76
+
77
+ 1. What exact race or lifecycle problem requires serialized per-identity turns?
78
+ 2. What is the canonical actor identity?
79
+ 3. How hot can one identity become?
80
+ 4. Can the request path tolerate the measured cold and warm latency?
81
+ 5. How many calls and durable rows will this surface create per day?
82
+ 6. Which calls can be asynchronous?
83
+ 7. Which effects need downstream idempotency?
84
+ 8. Which runtime roles and operational alerts will the feature require?
85
+ 9. How will completed messages and outbox history be retained?
86
+ 10. How will existing state be cut over and rolled back?
87
+
88
+ Benchmark the actual host database and deployment topology before committing a
89
+ latency-sensitive surface. Local benchmark results are evidence about query
90
+ shape, not universal capacity guarantees.
@@ -0,0 +1,133 @@
1
+ # Migrating existing state
2
+
3
+ Moving an existing Redis, cache, or key-value state machine into Solid Objects
4
+ is a data migration and a coordination cutover. Treat it as a staged production
5
+ change, not a rewrite that switches storage in one deploy.
6
+
7
+ ## 1. Write down the existing contract
8
+
9
+ Inventory:
10
+
11
+ - every read and write path;
12
+ - the current canonical and secondary keys;
13
+ - expiration and cleanup behavior;
14
+ - concurrency guards and idempotency keys;
15
+ - external effects;
16
+ - expected request latency and volume; and
17
+ - rollback requirements.
18
+
19
+ Run the [fit checklist](fit.md#decision-checklist) before migrating. A hot
20
+ counter or append pipeline may be better left in its existing store.
21
+
22
+ ## 2. Choose one canonical identity
23
+
24
+ Solid Objects addresses an actor with one `(actor_type, actor_id)` pair. Do not
25
+ hide two competing identities inside actor code or reintroduce a scan.
26
+
27
+ When existing state is written by `(user_id, assessment_short)` but read by
28
+ `session_id`, create a normalized lookup record:
29
+
30
+ ```ruby
31
+ class AssessmentSession < ApplicationRecord
32
+ validates :session_id, uniqueness: true
33
+ validates :assessment_short, uniqueness: { scope: :user_id }
34
+
35
+ def actor
36
+ Assessment.ref(id)
37
+ end
38
+ end
39
+ ```
40
+
41
+ The lookup row gives both old keys one stable primary key. The actor ID is the
42
+ lookup record ID, and ordinary indexed Active Record queries resolve either
43
+ external key. This is clearer and safer than delimiter-joining composite values
44
+ or preserving a `LIKE` scan.
45
+
46
+ Create and backfill the lookup table before actor traffic begins. Enforce every
47
+ identity invariant with unique database indexes.
48
+
49
+ ## 3. Add an idempotent bootstrap message
50
+
51
+ Never bulk-update `solid_objects_instances.state`. Direct writes bypass actor
52
+ ordering, state migrations, observables, activation ownership, and fencing.
53
+
54
+ Import through a normal actor message:
55
+
56
+ ```ruby
57
+ class Assessment < SolidObjects::Actor
58
+ attribute :imported, default: false
59
+ attribute :answers, default: -> { [] }
60
+
61
+ def bootstrap(answers:)
62
+ return if imported
63
+
64
+ self.answers = answers
65
+ self.imported = true
66
+ end
67
+ end
68
+ ```
69
+
70
+ Give every bootstrap call an idempotency key derived from the legacy record:
71
+
72
+ ```ruby
73
+ session.actor.async(
74
+ :bootstrap,
75
+ answers: legacy.answers,
76
+ idempotency_key: "legacy-assessment:#{legacy.id}",
77
+ available_at: jittered_time
78
+ )
79
+ ```
80
+
81
+ Spread large backfills over a dispatch window and monitor mailbox age,
82
+ failures, and dead letters. Asynchronous backfill requires the worker runtime.
83
+
84
+ ## 4. Prefer shadow comparison over blind dual writes
85
+
86
+ Two independent stores cannot be updated atomically without a shared
87
+ transaction or outbox. A controller that writes Redis and an actor in sequence
88
+ can leave them divergent after a timeout or crash.
89
+
90
+ A safer rollout:
91
+
92
+ 1. Keep the legacy store authoritative.
93
+ 2. Bootstrap the actor from a consistent legacy snapshot.
94
+ 3. Mirror new changes to the actor with stable idempotency keys.
95
+ 4. Read both stores in a background comparison path.
96
+ 5. Record divergence counts without changing the user response.
97
+ 6. Repair through actor messages, never direct actor-state SQL.
98
+ 7. Cut reads over only after divergence remains acceptably low.
99
+
100
+ If the actor becomes authoritative before the legacy system is retired, emit a
101
+ transactional effect that updates the legacy store. The effect is at least once,
102
+ so the legacy write still needs idempotency.
103
+
104
+ ## 5. Cut over in reversible stages
105
+
106
+ A typical zero-downtime sequence is:
107
+
108
+ 1. Deploy the lookup table and dual-key resolution.
109
+ 2. Deploy actor code and policies with reads still on the legacy store.
110
+ 3. Start the required runtime roles.
111
+ 4. Backfill actors in bounded batches.
112
+ 5. Enable shadow comparison and reconcile drift.
113
+ 6. Move a small cohort of reads to actors.
114
+ 7. Expand the cohort while watching latency, database growth, retries, and
115
+ divergence.
116
+ 8. Move writes to the actor.
117
+ 9. Retain the legacy state through an explicit rollback window.
118
+ 10. Remove dual writes and legacy data only after the rollback window closes.
119
+
120
+ Use a feature flag whose rollback restores legacy reads and writes without
121
+ requiring actor deletion. Do not assume a timed-out synchronous actor call did
122
+ not commit; query the durable result or use an idempotency key before retrying.
123
+
124
+ ## 6. Plan for dormant state and future changes
125
+
126
+ Actor state migrations and legacy-store migration solve different problems:
127
+
128
+ - this cookbook moves ownership from another store into an actor;
129
+ - `state_version` evolves actor JSON after that ownership exists.
130
+
131
+ Keep every published actor migration step. A dormant actor can reactivate years
132
+ later with an old state representation. See the
133
+ [state migration guide](state-migrations.md) for rolling-deployment rules.
data/docs/operations.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Operations guide
2
2
 
3
+ ## Installation verification
4
+
5
+ Run the installation doctor after generating the initializer and migrating:
6
+
7
+ ```bash
8
+ bin/rails solid_objects:doctor
9
+ ```
10
+
11
+ It validates runtime configuration, required tables and columns, neutral policy
12
+ posture, live runtime roles, and a real workerless synchronous actor round-trip.
13
+ Engine migration timestamps are rewritten when copied into a host application,
14
+ so the schema check compares the required shape instead of a fixed timestamp.
15
+ Warnings such as an all-deny neutral policy do not fail the command because a
16
+ context-aware production policy may correctly deny the probe.
17
+
3
18
  ## Runtime
4
19
 
5
20
  Start all configured roles:
@@ -114,9 +129,46 @@ Alert on:
114
129
  ## Retention and backups
115
130
 
116
131
  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 synchronous
119
- results for the promised lookup period.
132
+ roadmap work. Every actor call creates a durable message-history row, including
133
+ queries and attribute reads. Choose a retention period from measured call
134
+ volume, storage budget, audit needs, and the longest promised synchronous-result
135
+ lookup window.
136
+
137
+ An application-owned pruning job can start from this conservative relation:
138
+
139
+ ```ruby
140
+ cutoff = 30.days.ago
141
+
142
+ prunable_messages = SolidObjects::Message
143
+ .where(completed_at: ...cutoff)
144
+ .where.not(id: SolidObjects::ReadyMessage.select(:message_id))
145
+ .where.not(id: SolidObjects::ClaimedMessage.select(:message_id))
146
+ .where.not(id: SolidObjects::DeadLetter.select(:message_id))
147
+ .where.not(
148
+ id: SolidObjects::DeadLetter
149
+ .where.not(retried_message_id: nil)
150
+ .select(:retried_message_id)
151
+ )
152
+ .where.not(
153
+ id: SolidObjects::Effect
154
+ .where.not(status: "completed")
155
+ .select(:message_id)
156
+ )
157
+ .where.not(
158
+ id: SolidObjects::Broadcast
159
+ .where.not(status: "delivered")
160
+ .select(:message_id)
161
+ )
162
+
163
+ prunable_messages.in_batches(of: 1_000).delete_all
164
+ ```
165
+
166
+ Deleting a message cascades to its completed effects, delivered broadcasts, and
167
+ other message-owned records. Test the exact relation against a restored
168
+ production snapshot before scheduling it. Keep source and retried messages for
169
+ dead letters under investigation, and never prune pending, processing, ready, or
170
+ claimed work. Choose a cutoff longer than every `sync` timeout because a caller
171
+ whose result row disappears can no longer observe that result.
120
172
 
121
173
  Back up actor tables with the same consistency guarantees as application data.
122
174
  Restoring only instances without their mailboxes/outboxes, or vice versa, can
data/docs/roadmap.md CHANGED
@@ -17,6 +17,8 @@
17
17
  - One-shot and recurring reminders with `:latest` or `:all` catch-up
18
18
  - Durable observable broadcast outbox and authorized Action Cable refresh
19
19
  - Reconciliation read APIs
20
+ - Installation doctor, authorization reference, fit guide, and legacy-state
21
+ migration cookbook
20
22
  - SQLite, PostgreSQL, and MySQL integration suites
21
23
  - Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact
22
24
  RuboCop policy, and a warning-free Brakeman scan
@@ -50,7 +52,8 @@
50
52
  eviction.
51
53
  8. Expand security scanning and run compatibility CI across supported Rails and
52
54
  Ruby versions.
53
- 9. Benchmark all workloads under documented hardware/database settings.
55
+ 9. Benchmark all workloads under documented hardware/database settings and
56
+ publish adapter-specific adoption measurements.
54
57
 
55
58
  No production-ready claim should be made until these hardening milestones have
56
59
  operational soak evidence.
data/docs/security.md CHANGED
@@ -7,6 +7,9 @@ destroying actors, subscribing to actor streams, and administration. The host
7
7
  application supplies the authenticated request or connection as
8
8
  `authorization_context`. All five hooks deny by default.
9
9
 
10
+ The [authorization reference](authorization.md) lists the caller context and
11
+ risk for every hook and includes a tenant-aware policy example.
12
+
10
13
  Method-style reference calls do not bypass these hooks. Public instance methods
11
14
  declared on an actor are part of its remotely addressable message surface and
12
15
  delegate to the authorized synchronous invocation path. Keep implementation
@@ -44,3 +44,7 @@ A safe destructive rollout normally uses:
44
44
 
45
45
  Never update actor JSON in a bulk SQL migration. Use actor messages so fencing,
46
46
  ordering, observables, and outboxes remain intact.
47
+
48
+ This guide covers evolution after state belongs to Solid Objects. For moving
49
+ existing Redis, key-value, or relational state into actors without downtime,
50
+ use the [legacy-state migration cookbook](migrating-existing-state.md).
@@ -5,6 +5,23 @@ SolidObjects.configure do |configuration|
5
5
  configuration.effect_worker_count = 1
6
6
  configuration.broadcast_worker_count = 1
7
7
  configuration.reminder_scheduler_count = 1
8
+
9
+ # Every policy denies by default, so a fresh installation is intentionally
10
+ # inert. Replace these policies before invoking actors.
11
+ #
12
+ # Message and query policies gate direct calls, sync, async, and state reads.
13
+ # Destroy removes an actor and all of its durable work. Subscription gates
14
+ # Action Cable streams. Administration gates engine pages and operational
15
+ # commands. Keep the last three denied until their callers are authenticated.
16
+ #
17
+ # Prefer policies that bind actor_type and actor_id to a trusted
18
+ # authorization_context. See:
19
+ # https://github.com/cardmagic/solid_objects/blob/main/docs/authorization.md
20
+ # and run:
21
+ #
22
+ # bin/rails solid_objects:doctor
23
+ #
24
+ # after configuring the application.
8
25
  configuration.authorize_message = ->(**) { false }
9
26
  configuration.authorize_query = ->(**) { false }
10
27
  configuration.authorize_destroy = ->(**) { false }
@@ -0,0 +1,311 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "solid_objects/mailbox"
4
+ require "solid_objects/synchronous_invocation"
5
+
6
+ module SolidObjects
7
+ class Doctor
8
+ class Check
9
+ # @rbs @name: Symbol
10
+ # @rbs @status: Symbol
11
+ # @rbs @message: String
12
+
13
+ attr_reader :name, :status, :message
14
+
15
+ # @rbs (name: Symbol, status: Symbol, message: String) -> void
16
+ def initialize(name:, status:, message:)
17
+ @name = name
18
+ @status = status
19
+ @message = message
20
+ freeze
21
+ end
22
+
23
+ # @rbs () -> bool
24
+ def failed?
25
+ status == :fail
26
+ end
27
+ end
28
+
29
+ class Report
30
+ # @rbs @checks: Array[Check]
31
+
32
+ attr_reader :checks
33
+
34
+ # @rbs (checks: Array[Check]) -> void
35
+ def initialize(checks:)
36
+ @checks = checks.freeze
37
+ freeze
38
+ end
39
+
40
+ # @rbs () -> bool
41
+ def healthy?
42
+ checks.none?(&:failed?)
43
+ end
44
+
45
+ # @rbs (Symbol) -> Check
46
+ def check(name)
47
+ checks.find { |candidate| candidate.name == name } ||
48
+ raise(KeyError, "unknown doctor check #{name.inspect}")
49
+ end
50
+
51
+ # @rbs () -> String
52
+ def to_s
53
+ lines = [ "Solid Objects doctor #{SolidObjects::VERSION}" ]
54
+ lines.concat(
55
+ checks.map do |check|
56
+ "#{check.status.to_s.upcase.ljust(4)} #{check.name}: #{check.message}"
57
+ end
58
+ )
59
+ lines.join("\n")
60
+ end
61
+ end
62
+
63
+ EXPECTED_COLUMNS = {
64
+ processes: %w[id kind hostname pid last_heartbeat_at shutdown_state],
65
+ instances: %w[
66
+ id actor_type actor_id state state_version next_message_sequence
67
+ activation_owner_id activation_token activation_expires_at
68
+ activation_generation
69
+ ],
70
+ messages: %w[
71
+ id instance_id message_kind arguments sequence attempt_count request_id
72
+ result error rejection completed_at rejected_at
73
+ ],
74
+ ready_messages: %w[id message_id instance_id sequence available_at],
75
+ claimed_messages: %w[
76
+ id message_id instance_id process_id activation_token
77
+ activation_generation claimed_at
78
+ ],
79
+ reminders: %w[id instance_id message_name next_run_at status],
80
+ effects: %w[id message_id instance_id effect_id status available_at],
81
+ broadcasts: %w[id message_id instance_id broadcast_id status available_at],
82
+ dead_letters: %w[id message_id instance_id actor_type actor_id attempts]
83
+ }.freeze
84
+
85
+ class ProbeActor < Actor
86
+ actor_type "solid_objects_doctor"
87
+
88
+ # @rbs (value: String) -> String
89
+ def ping(value:)
90
+ value
91
+ end
92
+ end
93
+
94
+ # @rbs @connection: untyped
95
+ # @rbs @configuration: Configuration
96
+
97
+ # @rbs (?connection: untyped, ?configuration: Configuration) -> void
98
+ def initialize(
99
+ connection: SolidObjects::Record.connection,
100
+ configuration: SolidObjects.configuration
101
+ )
102
+ @connection = connection
103
+ @configuration = configuration
104
+ end
105
+
106
+ # @rbs () -> Report
107
+ def call
108
+ configuration_check = check_configuration
109
+ schema_check = check_schema
110
+ checks = [
111
+ configuration_check,
112
+ schema_check,
113
+ check_authorization,
114
+ schema_check.failed? ? skipped_runtime : check_runtime,
115
+ ready_for_round_trip?(configuration_check, schema_check) ?
116
+ check_sync_round_trip :
117
+ skipped_round_trip
118
+ ]
119
+ Report.new(checks:)
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :connection, :configuration
125
+
126
+ # @rbs () -> Check
127
+ def check_configuration
128
+ configuration.validate!
129
+ pass(:configuration, "configuration is valid")
130
+ rescue => error
131
+ fail_check(:configuration, "#{error.class}: #{error.message}")
132
+ end
133
+
134
+ # @rbs () -> Check
135
+ def check_schema
136
+ missing_tables = expected_table_names - connection.data_sources
137
+ unless missing_tables.empty?
138
+ return fail_check(:schema, "missing tables: #{missing_tables.join(", ")}")
139
+ end
140
+
141
+ missing_columns = EXPECTED_COLUMNS.each_with_object([]) do |(name, expected), missing|
142
+ table_name = SolidObjects.table_name(name)
143
+ actual = connection.columns(table_name).map(&:name)
144
+ (expected - actual).each { |column| missing << "#{table_name}.#{column}" }
145
+ end
146
+ unless missing_columns.empty?
147
+ return fail_check(:schema, "missing columns: #{missing_columns.join(", ")}")
148
+ end
149
+
150
+ pass(:schema, "schema matches the #{SolidObjects::VERSION} runtime")
151
+ rescue => error
152
+ fail_check(:schema, "#{error.class}: #{error.message}")
153
+ end
154
+
155
+ # @rbs () -> Check
156
+ def check_authorization
157
+ outcomes = policy_probes.to_h do |name, arguments|
158
+ outcome = configuration.public_send(name).call(**arguments) ? :allow : :deny
159
+ [ name, outcome ]
160
+ rescue
161
+ [ name, :unknown ]
162
+ end
163
+ allowed = outcomes.select { |_, outcome| outcome == :allow }.keys
164
+ unknown = outcomes.select { |_, outcome| outcome == :unknown }.keys
165
+
166
+ if allowed.empty? && unknown.empty?
167
+ return warn_check(
168
+ :authorization,
169
+ "all five policies denied a neutral context; review the generated initializer before use"
170
+ )
171
+ end
172
+ risky = allowed & %i[
173
+ authorize_destroy
174
+ authorize_subscription
175
+ authorize_administration
176
+ ]
177
+ unless risky.empty?
178
+ return warn_check(
179
+ :authorization,
180
+ "sensitive policies allowed a neutral context: #{risky.join(", ")}"
181
+ )
182
+ end
183
+ unless unknown.empty?
184
+ return warn_check(
185
+ :authorization,
186
+ "#{allowed.length} of 5 policies allowed a neutral context; " \
187
+ "#{unknown.join(", ")} could not evaluate without application context"
188
+ )
189
+ end
190
+
191
+ pass(:authorization, "#{allowed.length} of 5 policies allowed a neutral context")
192
+ end
193
+
194
+ # @rbs () -> Check
195
+ def check_runtime
196
+ cutoff = SolidObjects.database_adapter.database_now -
197
+ configuration.process_alive_threshold
198
+ counts = Process
199
+ .where(shutdown_state: "running", last_heartbeat_at: cutoff..)
200
+ .group(:kind)
201
+ .count
202
+ if counts.empty?
203
+ return info(
204
+ :runtime,
205
+ "no live runtime roles; workerless synchronous calls are available, asynchronous features are not"
206
+ )
207
+ end
208
+
209
+ summary = counts.sort.map { |kind, count| "#{kind}=#{count}" }.join(", ")
210
+ pass(:runtime, "live runtime roles: #{summary}")
211
+ rescue => error
212
+ fail_check(:runtime, "#{error.class}: #{error.message}")
213
+ end
214
+
215
+ # @rbs () -> Check
216
+ def check_sync_round_trip
217
+ actor_id = SecureRandom.uuid
218
+ value = SecureRandom.hex(8)
219
+ process_registry = SolidObjects.caller_process.process_registry
220
+ reference = ProbeActor.ref(actor_id)
221
+ message_reference = Mailbox.new.enqueue(
222
+ reference,
223
+ :ping,
224
+ { value: },
225
+ kind: "sync"
226
+ )
227
+ result = SynchronousInvocation.new.call(message_reference, timeout: 5.seconds)
228
+ raise Error, "unexpected round-trip result" unless result == value
229
+
230
+ pass(:sync_round_trip, "durable synchronous actor call completed without a worker")
231
+ rescue => error
232
+ fail_check(:sync_round_trip, "#{error.class}: #{error.message}")
233
+ ensure
234
+ Instance.where(actor_type: ProbeActor.actor_type, actor_id:).delete_all if actor_id
235
+ process_registry&.stop
236
+ process_registry&.process_record&.delete
237
+ end
238
+
239
+ # @rbs (Check, Check) -> bool
240
+ def ready_for_round_trip?(configuration_check, schema_check)
241
+ !configuration_check.failed? && !schema_check.failed?
242
+ end
243
+
244
+ # @rbs () -> Check
245
+ def skipped_runtime
246
+ skip(:runtime, "schema check failed")
247
+ end
248
+
249
+ # @rbs () -> Check
250
+ def skipped_round_trip
251
+ skip(:sync_round_trip, "configuration or schema check failed")
252
+ end
253
+
254
+ # @rbs () -> Hash[Symbol, Hash[Symbol, untyped]]
255
+ def policy_probes
256
+ actor_arguments = {
257
+ actor_type: ProbeActor.actor_type,
258
+ actor_id: "doctor",
259
+ authorization_context: nil
260
+ }
261
+ {
262
+ authorize_message: actor_arguments.merge(
263
+ message_name: "ping",
264
+ arguments: { "value" => "doctor" }
265
+ ),
266
+ authorize_query: actor_arguments.merge(
267
+ message_name: "value",
268
+ arguments: {}
269
+ ),
270
+ authorize_destroy: actor_arguments,
271
+ authorize_subscription: actor_arguments,
272
+ authorize_administration: {
273
+ action: "doctor",
274
+ resource: "runtime",
275
+ resource_id: nil,
276
+ authorization_context: nil
277
+ }
278
+ }
279
+ end
280
+
281
+ # @rbs () -> Array[String]
282
+ def expected_table_names
283
+ EXPECTED_COLUMNS.keys.map { |name| SolidObjects.table_name(name) }
284
+ end
285
+
286
+ # @rbs (Symbol, String) -> Check
287
+ def pass(name, message)
288
+ Check.new(name:, status: :pass, message:)
289
+ end
290
+
291
+ # @rbs (Symbol, String) -> Check
292
+ def info(name, message)
293
+ Check.new(name:, status: :info, message:)
294
+ end
295
+
296
+ # @rbs (Symbol, String) -> Check
297
+ def warn_check(name, message)
298
+ Check.new(name:, status: :warn, message:)
299
+ end
300
+
301
+ # @rbs (Symbol, String) -> Check
302
+ def fail_check(name, message)
303
+ Check.new(name:, status: :fail, message:)
304
+ end
305
+
306
+ # @rbs (Symbol, String) -> Check
307
+ def skip(name, message)
308
+ Check.new(name:, status: :skip, message:)
309
+ end
310
+ end
311
+ end
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -0,0 +1,10 @@
1
+ require "solid_objects/doctor"
2
+
3
+ namespace :solid_objects do
4
+ desc "Verify the Solid Objects installation"
5
+ task doctor: :environment do
6
+ report = SolidObjects::Doctor.new.call
7
+ puts report
8
+ abort "Solid Objects doctor failed" unless report.healthy?
9
+ end
10
+ end
@@ -0,0 +1,111 @@
1
+ # Generated from lib/solid_objects/doctor.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class Doctor
5
+ class Check
6
+ @name: Symbol
7
+
8
+ @status: Symbol
9
+
10
+ @message: String
11
+
12
+ attr_reader name: untyped
13
+
14
+ attr_reader status: untyped
15
+
16
+ attr_reader message: untyped
17
+
18
+ # @rbs (name: Symbol, status: Symbol, message: String) -> void
19
+ def initialize: (name: Symbol, status: Symbol, message: String) -> void
20
+
21
+ # @rbs () -> bool
22
+ def failed?: () -> bool
23
+ end
24
+
25
+ class Report
26
+ @checks: Array[Check]
27
+
28
+ attr_reader checks: untyped
29
+
30
+ # @rbs (checks: Array[Check]) -> void
31
+ def initialize: (checks: Array[Check]) -> void
32
+
33
+ # @rbs () -> bool
34
+ def healthy?: () -> bool
35
+
36
+ # @rbs (Symbol) -> Check
37
+ def check: (Symbol) -> Check
38
+
39
+ # @rbs () -> String
40
+ def to_s: () -> String
41
+ end
42
+
43
+ EXPECTED_COLUMNS: untyped
44
+
45
+ class ProbeActor < Actor
46
+ # @rbs (value: String) -> String
47
+ def ping: (value: String) -> String
48
+ end
49
+
50
+ @configuration: Configuration
51
+
52
+ @connection: untyped
53
+
54
+ # @rbs (?connection: untyped, ?configuration: Configuration) -> void
55
+ def initialize: (?connection: untyped, ?configuration: Configuration) -> void
56
+
57
+ # @rbs () -> Report
58
+ def call: () -> Report
59
+
60
+ private
61
+
62
+ attr_reader connection: untyped
63
+
64
+ attr_reader configuration: untyped
65
+
66
+ # @rbs () -> Check
67
+ def check_configuration: () -> Check
68
+
69
+ # @rbs () -> Check
70
+ def check_schema: () -> Check
71
+
72
+ # @rbs () -> Check
73
+ def check_authorization: () -> Check
74
+
75
+ # @rbs () -> Check
76
+ def check_runtime: () -> Check
77
+
78
+ # @rbs () -> Check
79
+ def check_sync_round_trip: () -> Check
80
+
81
+ # @rbs (Check, Check) -> bool
82
+ def ready_for_round_trip?: (Check, Check) -> bool
83
+
84
+ # @rbs () -> Check
85
+ def skipped_runtime: () -> Check
86
+
87
+ # @rbs () -> Check
88
+ def skipped_round_trip: () -> Check
89
+
90
+ # @rbs () -> Hash[Symbol, Hash[Symbol, untyped]]
91
+ def policy_probes: () -> Hash[Symbol, Hash[Symbol, untyped]]
92
+
93
+ # @rbs () -> Array[String]
94
+ def expected_table_names: () -> Array[String]
95
+
96
+ # @rbs (Symbol, String) -> Check
97
+ def pass: (Symbol, String) -> Check
98
+
99
+ # @rbs (Symbol, String) -> Check
100
+ def info: (Symbol, String) -> Check
101
+
102
+ # @rbs (Symbol, String) -> Check
103
+ def warn_check: (Symbol, String) -> Check
104
+
105
+ # @rbs (Symbol, String) -> Check
106
+ def fail_check: (Symbol, String) -> Check
107
+
108
+ # @rbs (Symbol, String) -> Check
109
+ def skip: (Symbol, String) -> Check
110
+ end
111
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson
@@ -278,6 +278,7 @@ files:
278
278
  - app/views/solid_objects/instances/index.html.erb
279
279
  - app/views/solid_objects/instances/show.html.erb
280
280
  - benchmark/activation_cache.rb
281
+ - benchmark/adoption_latency.rb
281
282
  - benchmark/claim.rb
282
283
  - benchmark/cold_actors.rb
283
284
  - benchmark/concurrent_actors.rb
@@ -303,11 +304,14 @@ files:
303
304
  - docs/adr/0012-not-active-jobs.md
304
305
  - docs/adr/0013-database-adapters.md
305
306
  - docs/architecture.md
307
+ - docs/authorization.md
306
308
  - docs/benchmarks.md
307
309
  - docs/correctness.md
308
310
  - docs/database-schema.md
309
311
  - docs/development.md
312
+ - docs/fit.md
310
313
  - docs/implementation-plan.md
314
+ - docs/migrating-existing-state.md
311
315
  - docs/operations.md
312
316
  - docs/realtime.md
313
317
  - docs/research/solid_queue.md
@@ -349,6 +353,7 @@ files:
349
353
  - lib/solid_objects/database_adapters/postgresql.rb
350
354
  - lib/solid_objects/database_adapters/sqlite.rb
351
355
  - lib/solid_objects/dead_letter_manager.rb
356
+ - lib/solid_objects/doctor.rb
352
357
  - lib/solid_objects/dom_identity.rb
353
358
  - lib/solid_objects/effect_executor.rb
354
359
  - lib/solid_objects/effect_registry.rb
@@ -374,6 +379,7 @@ files:
374
379
  - lib/solid_objects/version.rb
375
380
  - lib/solid_objects/wake_up.rb
376
381
  - lib/solid_objects/worker.rb
382
+ - lib/tasks/solid_objects_tasks.rake
377
383
  - sig/generated/controllers/solid_objects/application_controller.rbs
378
384
  - sig/generated/controllers/solid_objects/dead_letters_controller.rbs
379
385
  - sig/generated/controllers/solid_objects/instances_controller.rbs
@@ -400,6 +406,7 @@ files:
400
406
  - sig/generated/lib/solid_objects/database_adapters/postgresql.rbs
401
407
  - sig/generated/lib/solid_objects/database_adapters/sqlite.rbs
402
408
  - sig/generated/lib/solid_objects/dead_letter_manager.rbs
409
+ - sig/generated/lib/solid_objects/doctor.rbs
403
410
  - sig/generated/lib/solid_objects/dom_identity.rbs
404
411
  - sig/generated/lib/solid_objects/effect_executor.rbs
405
412
  - sig/generated/lib/solid_objects/effect_registry.rbs