Skip to content

Commit 5244a0e

Browse files
cardmagicclaude
andcommitted
docs: correct what the docs claim about wake-up
Three documents still described the old opt-in behaviour. The realtime guide told readers to assign `WakeUpAdapters.for` and said Redis is never selected, the operations guide said the warning fires when no adapter is configured, and ADR 0011 recorded no decision about choosing one. Each now describes the setting, automatic selection, the probe, the downgrade that says why, and where to read the installed capability. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fa7351f commit 5244a0e

3 files changed

Lines changed: 64 additions & 34 deletions

File tree

‎docs/adr/0011-wake-up-strategy.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ The interface supports:
2525

2626
MySQL uses polling or optional Redis. SQLite uses polling plus the in-process signal; multi-host SQLite is outside its supported operating model.
2727

28+
Selection is automatic. `wake_up_adapter` takes a name or an adapter and
29+
defaults to `:automatic`, which prefers a configured Redis URL, then PostgreSQL
30+
notifications, then polling. PostgreSQL is chosen only after a probe
31+
notification arrives, because `LISTEN` does not survive a transaction pooler. A
32+
requested adapter that the environment cannot provide polls and records why,
33+
rather than claim a wake-up it cannot deliver. `SolidObjects.wake_up.capability`
34+
reports the choice, and the doctor reports the same record.
35+
2836
The synchronous caller first attempts to claim and execute the actor locally,
2937
so the normal path has no worker polling leg. When another process owns the
3038
activation, coordination overhead from completion commit until the caller's
@@ -45,5 +53,7 @@ Timeout does not cancel durable work.
4553
- Redis loss only increases latency and never loses durable work.
4654
- Every adapter retains periodic polling to close startup, reconnect, and missed-message races.
4755
- A process that returns `false` from a timed wait participates in backoff; an older custom adapter that returns `nil` keeps the fast cadence.
48-
- A multi-process deployment without an adapter trades idle database load for up to the current idle polling interval of notification latency and logs that topology once.
56+
- A multi-process deployment whose installed adapter cannot cross processes trades idle database load for up to the current idle polling interval of notification latency and logs that topology once.
57+
- A PostgreSQL deployment that configures nothing now pays one `NOTIFY` per enqueue after the commit and one listening connection per waiting thread, outside the pool.
58+
- Selection runs once per process, under a lock, because the probe opens connections and waits.
4959
- Notification payloads never contain actor arguments or results.

‎docs/operations.md‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,27 @@ to `idle_polling_interval`, which defaults to one second. Actor workers clamp
245245
the ceiling to `lease_renewal_interval` while they may hold cached activations.
246246
Set the fast and idle values equal for a fixed cadence.
247247

248-
The default wake-up interrupts waits only in the current Ruby process. When a
249-
live process record shows that the database is shared across processes and no
250-
adapter is configured, the runtime logs
251-
`solid_objects.polling_only_cross_process_wake_up` once. Configure
252-
`WakeUpAdapters::Postgresql` or `WakeUpAdapters::Redis` when separate processes
253-
need prompt delivery. Without one, newly committed work can wait up to the
254-
current idle polling interval.
248+
Solid Objects selects a wake-up adapter on first use. `wake_up_adapter` defaults
249+
to `:automatic`, which prefers `SOLID_OBJECTS_REDIS_URL`, then PostgreSQL
250+
notifications, then polling. `SolidObjects.wake_up.capability` and the
251+
`wake_up` doctor check report what was installed, whether it crosses processes,
252+
its measured floor, and why.
253+
254+
The in-process signal interrupts waits only in the current Ruby process. When a
255+
live process record shows that the database is shared across processes and the
256+
installed adapter does not cross them, the runtime logs
257+
`solid_objects.polling_only_cross_process_wake_up` once. Newly committed work
258+
can then wait up to the current idle polling interval.
259+
260+
On PostgreSQL, selection proves the path first: it listens on a probe channel,
261+
notifies it from a second connection, and waits for the notification. A probe
262+
that does not arrive logs `solid_objects.wake_up.pooled_session` once and falls
263+
back to polling, because `LISTEN` does not survive a transaction pooler such as
264+
PgBouncer. A requested adapter that the environment cannot provide, such as
265+
`:postgresql` on MySQL or `:redis` without `SOLID_OBJECTS_REDIS_URL`, logs
266+
`solid_objects.wake_up.unavailable` once and polls rather than claim a
267+
cross-process wake-up that cannot happen. Only an unknown name is refused, and
268+
`configure` refuses it at boot.
255269

256270
The warning excludes process rows with the current hostname and PID. It can
257271
therefore appear during a rolling deployment or restart overlap when an older

‎docs/realtime.md‎

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -140,46 +140,52 @@ explicitly serve the module. Turbo's normal morph rules still apply; use
140140

141141
## Cross-process wake-up
142142

143-
Runtime roles poll for work and are woken early by an in-process signal. That
144-
signal cannot cross process boundaries, so a commit in a Puma process does not
145-
wake a broadcast executor in a worker process, and delivery waits out
143+
Runtime roles poll for work and are woken early by a signal. An in-process
144+
signal cannot cross process boundaries, so a commit in a Puma process would not
145+
wake a broadcast executor in a worker process, and delivery would wait out
146146
`polling_interval`, 100 ms by default.
147147

148-
On PostgreSQL, install the notification adapter to remove that delay:
148+
Solid Objects selects the adapter for you. `wake_up_adapter` defaults to
149+
`:automatic`, which prefers a configured Redis URL, then PostgreSQL
150+
notifications, then polling:
149151

150152
```ruby
151153
# config/initializers/solid_objects.rb
152-
configuration.wake_up_adapter = SolidObjects::WakeUpAdapters.for
154+
configuration.wake_up_adapter = :automatic # the default
155+
configuration.wake_up_adapter = :in_process # opt out
156+
configuration.wake_up_adapter = :postgresql # force one
157+
configuration.wake_up_adapter = MyAdapter.new # your own
153158
```
154159

155-
`WakeUpAdapters.for` returns notifications on PostgreSQL and the in-process
156-
default on SQLite and MySQL, so the same line is safe across adapters. Name
157-
`SolidObjects::WakeUpAdapters::Postgresql.new` directly to require it.
158-
159-
MySQL has no notification primitive. MySQL applications either keep polling and
160-
tune `polling_interval`, or configure the Redis adapter:
161-
162-
```ruby
163-
configuration.wake_up_adapter = SolidObjects::WakeUpAdapters::Redis.new(
164-
url: ENV["REDIS_URL"]
165-
)
166-
```
167-
168-
Measured latency for a cross-process wake-up drops from 103.8 ms to 5.7 ms at
169-
p50. The `redis` gem is not a dependency of this gem, so applications add it
170-
themselves. One background subscription per process fans out to every waiting
171-
role in memory, rather than one connection per thread, and `WakeUpAdapters.for`
172-
does not select it: Redis is infrastructure this gem otherwise does not require,
173-
so choosing it is explicit.
160+
`SolidObjects.wake_up.capability` reports what was installed, whether it crosses
161+
processes, its measured floor, and why. `bin/rails solid_objects:doctor` reports
162+
the same record.
174163

164+
On PostgreSQL, selection proves the path before it chooses it. It listens on a
165+
probe channel, sends one `NOTIFY` from a second connection, and waits for it to
166+
arrive, because `LISTEN` does not survive a transaction pooler such as
167+
PgBouncer. A probe that does not deliver falls back to polling and warns once.
175168
Measured latency for a cross-process wake-up drops from 103.7 ms to 2.9 ms at
176169
p50. The adapter keeps `polling_interval` as the upper bound: a missed or failed
177170
notification costs latency, never correctness, and signalling never raises into
178171
the caller that committed. `LISTEN` needs its own connection, so the adapter
179172
opens one outside the pool and releases it on `stop`.
180173

181-
Applications on SQLite or MySQL, or that do not configure the adapter, keep the
182-
existing polling behaviour.
174+
MySQL has no notification primitive, so MySQL applications either keep polling
175+
and tune `polling_interval`, or set `SOLID_OBJECTS_REDIS_URL`, which selects
176+
Redis on any database:
177+
178+
```bash
179+
SOLID_OBJECTS_REDIS_URL=redis://localhost:6379/0
180+
```
181+
182+
Measured latency for a cross-process wake-up drops from 103.8 ms to 5.7 ms at
183+
p50. The `redis` gem is not a dependency of this gem, so applications add it
184+
themselves, and selection polls and says so when the gem is missing. One
185+
background subscription per process fans out to every waiting role in memory,
186+
rather than one connection per thread. Name
187+
`SolidObjects::WakeUpAdapters::Redis.new(url:)` directly for a URL that does not
188+
come from the environment.
183189

184190
## Batched component refreshes
185191

0 commit comments

Comments
 (0)