solid_objects 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/docs/benchmarks.md +15 -0
- data/docs/local-testing.md +91 -0
- data/docs/realtime.md +43 -0
- data/docs/roadmap.md +35 -26
- data/lib/generators/solid_objects/templates/solid_objects.rb +8 -0
- data/lib/solid_objects/configuration.rb +10 -0
- data/lib/solid_objects/database_adapter.rb +37 -0
- data/lib/solid_objects/database_adapters/mysql.rb +28 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +15 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +11 -4
- data/lib/solid_objects/doctor.rb +16 -0
- data/lib/solid_objects/supervisor.rb +125 -6
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/wake_up_adapters/postgresql.rb +136 -0
- data/lib/solid_objects/wake_up_adapters/redis.rb +183 -0
- data/lib/solid_objects/wake_up_adapters.rb +23 -0
- data/lib/solid_objects.rb +3 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
- data/sig/generated/lib/solid_objects/database_adapter.rbs +17 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +11 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +5 -0
- data/sig/generated/lib/solid_objects/doctor.rbs +3 -0
- data/sig/generated/lib/solid_objects/supervisor.rbs +45 -2
- data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +75 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +96 -0
- data/sig/generated/lib/solid_objects/wake_up_adapters.rbs +17 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7fee6db10af36996dbe9c8484fab405b53e2463a3fd7d2f7414310fedab68d5
|
|
4
|
+
data.tar.gz: 8b71bb8fbf0dc0359ce8a00097428dbbcd189e4d2b4071ba0339b9c3ec1cd933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62f2ce7ecfa4899f82af3682c5a8fb97d7005e6ba9152a8a63433b2ec345d2edc3d795c29a7f6de24d25f275afd4857986ed34302359d0f3aab018f936656acd
|
|
7
|
+
data.tar.gz: 2114e1d8b7757bf598562fba6e1b8ecafeea852d6925a5161f74b1dcab6dab9b5750ca71327b0cfa23b927869cbeee348f1bd6d03cbfc5d964d2bfd53a625435
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-08-10
|
|
4
|
+
|
|
5
|
+
- Add a browser test suite running the refresh modules against real Chromium and
|
|
6
|
+
a real Turbo build, covering `component_refresh.js`, which previously had no
|
|
7
|
+
tests at all. Every batching defect that reached production passed the jsdom
|
|
8
|
+
suite, because jsdom cannot model Turbo applying a morph, task boundaries
|
|
9
|
+
between socket deliveries, or abort semantics.
|
|
10
|
+
- Verify the database server. Each adapter reports its version against the
|
|
11
|
+
oldest one Solid Objects is exercised against, PostgreSQL 13, MySQL 8.0, and
|
|
12
|
+
SQLite 3.35, and MySQL additionally confirms that Solid Objects tables use
|
|
13
|
+
InnoDB, since a non-transactional engine would silently break fenced commits.
|
|
14
|
+
The doctor reports this as `database_server` and warns rather than failing:
|
|
15
|
+
refusing to run on an untested server would be a worse failure than running
|
|
16
|
+
on one.
|
|
17
|
+
- Add `SolidObjects::WakeUpAdapters::Redis`, an optional cross-process wake-up
|
|
18
|
+
using Redis publish and subscribe. This is the option for MySQL, which has no
|
|
19
|
+
notification primitive. Measured cross-process wake-up latency drops from
|
|
20
|
+
103.8 ms to 5.7 ms at p50. One background subscription per process fans out to
|
|
21
|
+
every waiting role in memory. The `redis` gem is not a dependency of this gem,
|
|
22
|
+
and `WakeUpAdapters.for` does not select it, so adopting Redis stays explicit.
|
|
23
|
+
|
|
24
|
+
## 0.8.0 - 2026-08-10
|
|
25
|
+
|
|
26
|
+
- Replace a supervised role whose thread died. A role that raised left its
|
|
27
|
+
thread dead while the process kept running and quietly did less work; the
|
|
28
|
+
supervisor now restarts it until shutdown is requested. Prune dead process
|
|
29
|
+
records on an interval as part of the same monitor. Both intervals are
|
|
30
|
+
configurable through `supervisor_monitor_interval` and
|
|
31
|
+
`dead_process_cleanup_interval`.
|
|
32
|
+
- Run compatibility CI across the span the gemspec advertises: Ruby 3.3 and 3.4
|
|
33
|
+
against Rails 8.0 and 8.1. The suite previously ran on one combination, so
|
|
34
|
+
`>= 8.0` was a claim rather than a tested guarantee. Set `RAILS_VERSION` to
|
|
35
|
+
pin a Rails line locally.
|
|
36
|
+
- Stop a synchronous lock retry from asking for a negative wait when its
|
|
37
|
+
deadline expires between the check and the wait, which raised
|
|
38
|
+
`ArgumentError: time interval must not be negative` instead of the timeout
|
|
39
|
+
the caller expected. Found by the new compatibility matrix.
|
|
40
|
+
- Add `SolidObjects::WakeUpAdapters::Postgresql`, an optional cross-process
|
|
41
|
+
wake-up using PostgreSQL notifications. In-process signalling cannot reach a
|
|
42
|
+
worker process, so reactive delivery waited out `polling_interval`. With the
|
|
43
|
+
adapter configured, measured cross-process wake-up latency drops from 103.7 ms
|
|
44
|
+
to 2.9 ms at p50. The polling interval remains the upper bound, and neither
|
|
45
|
+
signalling nor waiting raises into its caller. `WakeUpAdapters.for` selects
|
|
46
|
+
notifications on PostgreSQL and the in-process default elsewhere; it is not
|
|
47
|
+
the default, because the adapter opens a connection per waiting thread
|
|
48
|
+
outside the pool and `LISTEN` does not survive a transaction-pooling proxy.
|
|
49
|
+
|
|
3
50
|
## 0.7.3 - 2026-08-09
|
|
4
51
|
|
|
5
52
|
- Coordinate batched component refreshes by revision as well as scope and batch
|
data/docs/benchmarks.md
CHANGED
|
@@ -76,6 +76,21 @@ Cable delivery, or browser rendering, which dominate wall-clock time in a real
|
|
|
76
76
|
deployment and make the request-count difference matter more than it appears
|
|
77
77
|
here. End-to-end latency against a deployed application has not been measured.
|
|
78
78
|
|
|
79
|
+
## Cross-process wake-up
|
|
80
|
+
|
|
81
|
+
Measured 2026-08-09 against PostgreSQL 17, 30 samples, with `polling_interval`
|
|
82
|
+
at its 100 ms default and a signal sent 2 ms after the waiter began.
|
|
83
|
+
|
|
84
|
+
| Wake-up strategy | p50 | p95 |
|
|
85
|
+
| --- | ---: | ---: |
|
|
86
|
+
| In-process `WakeUp` | 103.7 ms | 105.1 ms |
|
|
87
|
+
| `WakeUpAdapters::Postgresql` | 2.9 ms | 5.1 ms |
|
|
88
|
+
|
|
89
|
+
The in-process wake-up cannot reach another process, so a worker waits out the
|
|
90
|
+
full polling interval no matter how quickly the web process committed. The
|
|
91
|
+
notification adapter removes that floor rather than shrinking it, and the
|
|
92
|
+
polling interval remains the upper bound if a notification is missed.
|
|
93
|
+
|
|
79
94
|
## Durable row growth
|
|
80
95
|
|
|
81
96
|
The storage cost is deterministic even when latency is not:
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Local testing
|
|
2
|
+
|
|
3
|
+
The default suite runs against SQLite and needs nothing extra:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bundle exec rake
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Everything below is optional. Each adapter and each optional service skips its
|
|
10
|
+
tests when the service is absent, so a missing container degrades coverage
|
|
11
|
+
rather than breaking the run. That is convenient, and it is also a trap: a
|
|
12
|
+
skipped test looks identical to a passing one in the summary line. Check the
|
|
13
|
+
skip count when a change touches an adapter.
|
|
14
|
+
|
|
15
|
+
## PostgreSQL
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
brew services start postgresql@17
|
|
19
|
+
createuser -h 127.0.0.1 -s solid_objects
|
|
20
|
+
psql -h 127.0.0.1 -d postgres -c "ALTER USER solid_objects WITH PASSWORD 'solid_objects';"
|
|
21
|
+
createdb -h 127.0.0.1 -O solid_objects solid_objects_test
|
|
22
|
+
|
|
23
|
+
SOLID_OBJECTS_DATABASE_URL=postgresql://solid_objects:solid_objects@127.0.0.1:5432/solid_objects_test \
|
|
24
|
+
bundle exec rake test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Running this locally is worth the setup: it is what caught the PostgreSQL
|
|
28
|
+
version comparison reading a packed integer, where `170010` compared greater
|
|
29
|
+
than any minimum and made the check useless on the adapter it mattered most for.
|
|
30
|
+
|
|
31
|
+
## MySQL and Redis in Docker
|
|
32
|
+
|
|
33
|
+
These use non-default ports so they cannot collide with a MySQL or Redis that
|
|
34
|
+
another project is already running:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
docker run -d --name so-mysql -p 3307:3306 \
|
|
38
|
+
-e MYSQL_ROOT_PASSWORD=solid_objects \
|
|
39
|
+
-e MYSQL_DATABASE=solid_objects_test \
|
|
40
|
+
-e MYSQL_USER=solid_objects \
|
|
41
|
+
-e MYSQL_PASSWORD=solid_objects \
|
|
42
|
+
mysql:8
|
|
43
|
+
|
|
44
|
+
docker run -d --name so-redis -p 6380:6379 redis:7-alpine
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
SOLID_OBJECTS_DATABASE_URL=mysql2://solid_objects:solid_objects@127.0.0.1:3307/solid_objects_test \
|
|
49
|
+
bundle exec rake test
|
|
50
|
+
|
|
51
|
+
SOLID_OBJECTS_REDIS_URL=redis://127.0.0.1:6380/15 \
|
|
52
|
+
bundle exec rake test TEST=test/integration/redis_wake_up_test.rb
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Stop them with `docker rm -f so-mysql so-redis`.
|
|
56
|
+
|
|
57
|
+
## Recreating a database between runs
|
|
58
|
+
|
|
59
|
+
The test helper migrates unconditionally, so a second run against a database
|
|
60
|
+
that already has the tables fails with a duplicate-table error rather than a
|
|
61
|
+
test failure. Recreate first:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dropdb -h 127.0.0.1 solid_objects_test && createdb -h 127.0.0.1 -O solid_objects solid_objects_test
|
|
65
|
+
|
|
66
|
+
docker exec so-mysql mysql -u root -psolid_objects \
|
|
67
|
+
-e "DROP DATABASE IF EXISTS solid_objects_test; CREATE DATABASE solid_objects_test;
|
|
68
|
+
GRANT ALL ON solid_objects_test.* TO 'solid_objects'@'%';"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Rails and Ruby span
|
|
72
|
+
|
|
73
|
+
`RAILS_VERSION` pins the Rails line the gemspec advertises:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
RAILS_VERSION=8.0 bundle install
|
|
77
|
+
RAILS_VERSION=8.0 bundle exec rake test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Browser modules
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install
|
|
84
|
+
npm test # jsdom, fast
|
|
85
|
+
npx playwright install chromium
|
|
86
|
+
npm run test:browser # real Chromium and a real Turbo build
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The jsdom suite covers logic; the browser suite covers integration with Turbo.
|
|
90
|
+
Both matter: every batching defect that reached production passed the jsdom
|
|
91
|
+
suite alone.
|
data/docs/realtime.md
CHANGED
|
@@ -110,6 +110,49 @@ applications discover the namespaced engine asset. Applications created with
|
|
|
110
110
|
explicitly serve the module. Turbo's normal morph rules still apply; use
|
|
111
111
|
`data-turbo-permanent` for elements that must never be changed.
|
|
112
112
|
|
|
113
|
+
## Cross-process wake-up
|
|
114
|
+
|
|
115
|
+
Runtime roles poll for work and are woken early by an in-process signal. That
|
|
116
|
+
signal cannot cross process boundaries, so a commit in a Puma process does not
|
|
117
|
+
wake a broadcast executor in a worker process, and delivery waits out
|
|
118
|
+
`polling_interval`, 100 ms by default.
|
|
119
|
+
|
|
120
|
+
On PostgreSQL, install the notification adapter to remove that delay:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# config/initializers/solid_objects.rb
|
|
124
|
+
configuration.wake_up_adapter = SolidObjects::WakeUpAdapters.for
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`WakeUpAdapters.for` returns notifications on PostgreSQL and the in-process
|
|
128
|
+
default on SQLite and MySQL, so the same line is safe across adapters. Name
|
|
129
|
+
`SolidObjects::WakeUpAdapters::Postgresql.new` directly to require it.
|
|
130
|
+
|
|
131
|
+
MySQL has no notification primitive. MySQL applications either keep polling and
|
|
132
|
+
tune `polling_interval`, or configure the Redis adapter:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
configuration.wake_up_adapter = SolidObjects::WakeUpAdapters::Redis.new(
|
|
136
|
+
url: ENV["REDIS_URL"]
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Measured latency for a cross-process wake-up drops from 103.8 ms to 5.7 ms at
|
|
141
|
+
p50. The `redis` gem is not a dependency of this gem, so applications add it
|
|
142
|
+
themselves. One background subscription per process fans out to every waiting
|
|
143
|
+
role in memory, rather than one connection per thread, and `WakeUpAdapters.for`
|
|
144
|
+
does not select it: Redis is infrastructure this gem otherwise does not require,
|
|
145
|
+
so choosing it is explicit.
|
|
146
|
+
|
|
147
|
+
Measured latency for a cross-process wake-up drops from 103.7 ms to 2.9 ms at
|
|
148
|
+
p50. The adapter keeps `polling_interval` as the upper bound: a missed or failed
|
|
149
|
+
notification costs latency, never correctness, and signalling never raises into
|
|
150
|
+
the caller that committed. `LISTEN` needs its own connection, so the adapter
|
|
151
|
+
opens one outside the pool and releases it on `stop`.
|
|
152
|
+
|
|
153
|
+
Applications on SQLite or MySQL, or that do not configure the adapter, keep the
|
|
154
|
+
existing polling behaviour.
|
|
155
|
+
|
|
113
156
|
## Batched component refreshes
|
|
114
157
|
|
|
115
158
|
A component refresh costs one browser request. When one actor mutation changes
|
data/docs/roadmap.md
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
- Reconciliation read APIs
|
|
25
25
|
- Installation doctor, authorization reference, fit guide, and legacy-state
|
|
26
26
|
migration cookbook
|
|
27
|
+
- Database server verification: each adapter reports its version against a
|
|
28
|
+
tested minimum, MySQL confirms Solid Objects tables use InnoDB, and the
|
|
29
|
+
doctor warns rather than refusing to run on an untested server
|
|
27
30
|
- Handler Active Record write isolation, same-database commit actions, ambient
|
|
28
31
|
transaction rejection, adapter lock/query deadlines, bounded SQLite lock
|
|
29
32
|
retries outside those deadlines, structured sync timeout diagnostics, and
|
|
@@ -31,23 +34,37 @@
|
|
|
31
34
|
- Bounded message/process pruning, actor-type opt-in instance expiration,
|
|
32
35
|
graceful caller shutdown, committed state snapshots, and an opt-in Minitest
|
|
33
36
|
helper
|
|
37
|
+
- Supervisor role replacement: a role whose thread dies is restarted until
|
|
38
|
+
shutdown is requested, and dead process records are pruned on an interval
|
|
34
39
|
- SQLite, PostgreSQL, and MySQL integration suites
|
|
40
|
+
- Opt-in cross-process wake-up on PostgreSQL through `WakeUpAdapters.for`, with
|
|
41
|
+
a listening connection per waiting thread and release on supervisor shutdown
|
|
42
|
+
- Opt-in cross-process wake-up on Redis, the option for MySQL applications,
|
|
43
|
+
measured at 103.8 ms to 5.7 ms at p50; the `redis` gem stays outside this
|
|
44
|
+
gem's dependencies
|
|
35
45
|
- Inline RBS generation/validation, Steep, Standard Ruby, Solid Queue's exact
|
|
36
46
|
RuboCop policy, and a warning-free Brakeman scan
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
- Compatibility CI across the supported span: Ruby 3.3 and 3.4 against Rails 8.0
|
|
48
|
+
and 8.1, pinned through `RAILS_VERSION` so the advertised range is verified
|
|
49
|
+
rather than assumed
|
|
50
|
+
- A JavaScript suite covering every browser module, run in CI with Node's test
|
|
51
|
+
runner and jsdom, plus a browser suite running the same modules against real
|
|
52
|
+
Chromium and a real Turbo build, with every GitHub Actions reference pinned to
|
|
53
|
+
a commit SHA
|
|
40
54
|
|
|
41
55
|
## Partially implemented
|
|
42
56
|
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
- Wake-up strategy: in-process signaling, durable polling, injection, and an
|
|
58
|
+
opt-in PostgreSQL notification adapter are implemented; a Redis adapter is
|
|
59
|
+
not. In-process signaling cannot cross process boundaries, so without the
|
|
60
|
+
adapter a commit in a web process does not wake a broadcast executor in a
|
|
61
|
+
worker process and that delivery waits up to `polling_interval`, 100 ms by
|
|
62
|
+
default. `WakeUpAdapters.for` removes that delay on PostgreSQL, measured at
|
|
63
|
+
103.7 ms to 2.9 ms at p50. It is opt-in rather than automatic: it opens a
|
|
64
|
+
connection per waiting thread outside the pool, and `LISTEN` does not survive
|
|
65
|
+
a transaction-pooling proxy such as PgBouncer. MySQL has no notification
|
|
66
|
+
primitive, so MySQL applications keep polling unless they configure the Redis
|
|
67
|
+
adapter.
|
|
51
68
|
- Realtime: scalar and dependency-driven keyed ERB component replacement or
|
|
52
69
|
morphing, personalized refresh authorization, revision fencing, coalescing,
|
|
53
70
|
reconnect convergence, batched refreshes, and personalized state payloads are
|
|
@@ -59,27 +76,19 @@
|
|
|
59
76
|
distributed per-actor rate limits and global admission control do not.
|
|
60
77
|
- Administration: actor and dead-letter views plus policy hooks exist; richer
|
|
61
78
|
filtering, audit records, and bulk-safe tools do not.
|
|
62
|
-
- Browser module coverage: the state payload and batched refresh modules have
|
|
63
|
-
JavaScript tests; `component_refresh.js`, which drives individual morph
|
|
64
|
-
refreshes, does not.
|
|
65
79
|
- Outboxes use portable status rows with polling indexes; future versions may
|
|
66
80
|
introduce narrow ready/claimed membership tables for very large outboxes.
|
|
67
81
|
|
|
68
82
|
## Next milestones
|
|
69
83
|
|
|
70
|
-
1. Add
|
|
71
|
-
2. Add
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
3. Add result lookup by request ID and broader deadlock retry classification.
|
|
75
|
-
4. Add scheduled retention and stale-process maintenance.
|
|
76
|
-
5. Add database/server-version checks and MySQL InnoDB verification at boot.
|
|
77
|
-
6. Add Turbo append intents and expand reconnect coverage in a full browser.
|
|
78
|
-
7. Add distributed rate limits, global admission hooks, and cache-capacity
|
|
84
|
+
1. Add result lookup by request ID and broader deadlock retry classification.
|
|
85
|
+
2. Add scheduled retention and stale-process maintenance.
|
|
86
|
+
3. Add Turbo append intents and expand reconnect coverage in a full browser.
|
|
87
|
+
4. Add distributed rate limits, global admission hooks, and cache-capacity
|
|
79
88
|
eviction.
|
|
80
|
-
|
|
81
|
-
Ruby
|
|
82
|
-
|
|
89
|
+
5. Expand security scanning. Compatibility CI across supported Rails and Ruby
|
|
90
|
+
versions is implemented; Ruby 4.0 is not yet in the matrix.
|
|
91
|
+
6. Benchmark all workloads under documented hardware/database settings and
|
|
83
92
|
publish adapter-specific adoption measurements. Throughput, synchronous
|
|
84
93
|
latency, query counts, and the three reactive delivery paths are measured on
|
|
85
94
|
SQLite; adapter-specific and end-to-end browser measurements are not.
|
|
@@ -58,4 +58,12 @@ SolidObjects.configure do |configuration|
|
|
|
58
58
|
# configuration.authorize_administration = lambda do |authorization_context:, **|
|
|
59
59
|
# authorization_context.is_a?(Hash) && authorization_context[:source] == "cli"
|
|
60
60
|
# end
|
|
61
|
+
|
|
62
|
+
# Runtime roles poll for work and are woken early by an in-process signal,
|
|
63
|
+
# which cannot reach another process. On PostgreSQL, notifications remove that
|
|
64
|
+
# delay. This opens a connection per waiting thread outside the pool and does
|
|
65
|
+
# not work through a transaction-pooling proxy such as PgBouncer, so it is
|
|
66
|
+
# opt-in:
|
|
67
|
+
#
|
|
68
|
+
# configuration.wake_up_adapter = SolidObjects::WakeUpAdapters.for
|
|
61
69
|
end
|
|
@@ -21,6 +21,8 @@ module SolidObjects
|
|
|
21
21
|
# @rbs @process_heartbeat_interval: Float
|
|
22
22
|
# @rbs @process_alive_threshold: Float
|
|
23
23
|
# @rbs @shutdown_timeout: Float
|
|
24
|
+
# @rbs @supervisor_monitor_interval: Float
|
|
25
|
+
# @rbs @dead_process_cleanup_interval: Float
|
|
24
26
|
# @rbs @message_retention: Numeric
|
|
25
27
|
# @rbs @message_retention_by_actor_type: Hash[String, Numeric]
|
|
26
28
|
# @rbs @instance_retention_by_actor_type: Hash[String, Numeric]
|
|
@@ -62,6 +64,8 @@ module SolidObjects
|
|
|
62
64
|
:process_heartbeat_interval,
|
|
63
65
|
:process_alive_threshold,
|
|
64
66
|
:shutdown_timeout,
|
|
67
|
+
:supervisor_monitor_interval,
|
|
68
|
+
:dead_process_cleanup_interval,
|
|
65
69
|
:message_retention,
|
|
66
70
|
:message_retention_by_actor_type,
|
|
67
71
|
:instance_retention_by_actor_type,
|
|
@@ -102,6 +106,8 @@ module SolidObjects
|
|
|
102
106
|
@max_attempts = 5
|
|
103
107
|
@retry_delay = ->(attempt) { [ 2**(attempt - 1), 60 ].min.to_f }
|
|
104
108
|
@lock_retry_attempts = 10
|
|
109
|
+
@supervisor_monitor_interval = 1.0
|
|
110
|
+
@dead_process_cleanup_interval = 60.0
|
|
105
111
|
@process_heartbeat_interval = 15.0
|
|
106
112
|
@process_alive_threshold = 60.0
|
|
107
113
|
@shutdown_timeout = 15.0
|
|
@@ -138,6 +144,10 @@ module SolidObjects
|
|
|
138
144
|
raise ArgumentError, "table_name_prefix must contain lowercase letters, digits, and underscores"
|
|
139
145
|
end
|
|
140
146
|
|
|
147
|
+
unless supervisor_monitor_interval.positive?
|
|
148
|
+
raise ArgumentError, "supervisor_monitor_interval must be positive"
|
|
149
|
+
end
|
|
150
|
+
|
|
141
151
|
unless lease_duration > lease_renewal_interval
|
|
142
152
|
raise ArgumentError, "lease_duration must be greater than lease_renewal_interval"
|
|
143
153
|
end
|
|
@@ -32,6 +32,43 @@ module SolidObjects
|
|
|
32
32
|
@fixed_connection = connection_pool ? nil : connection
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# The oldest server the adapter has been exercised against. Reported rather
|
|
36
|
+
# than enforced: refusing to boot on an untested server would be a worse
|
|
37
|
+
# failure than running on one.
|
|
38
|
+
# @rbs () -> Gem::Version?
|
|
39
|
+
def minimum_server_version
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @rbs () -> Gem::Version
|
|
44
|
+
def server_version
|
|
45
|
+
with_connection do |connection|
|
|
46
|
+
Gem::Version.new(connection.database_version.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# One observed version decides both the status and the message. Reading it
|
|
51
|
+
# again could let a transient failure replace an already determined result.
|
|
52
|
+
# @rbs (?Gem::Version?) -> Array[String]
|
|
53
|
+
def unsupported_server_reasons(observed = nil)
|
|
54
|
+
observed ||= server_version
|
|
55
|
+
reasons = []
|
|
56
|
+
minimum = minimum_server_version
|
|
57
|
+
if minimum && observed < minimum
|
|
58
|
+
reasons << "#{self.class.name.demodulize} #{observed} is older than " \
|
|
59
|
+
"Solid Objects requires, which is #{minimum}"
|
|
60
|
+
end
|
|
61
|
+
reasons.concat(additional_server_reasons)
|
|
62
|
+
reasons
|
|
63
|
+
rescue => error
|
|
64
|
+
[ "the database server could not be verified: #{error.class}: #{error.message}" ]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs () -> Array[String]
|
|
68
|
+
def additional_server_reasons
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
35
72
|
# @rbs () -> bool
|
|
36
73
|
def supports_skip_locked?
|
|
37
74
|
false
|
|
@@ -13,6 +13,34 @@ module SolidObjects
|
|
|
13
13
|
"FOR UPDATE SKIP LOCKED"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# A non-transactional engine would silently break fenced commits, so the
|
|
17
|
+
# storage engine is verified rather than assumed.
|
|
18
|
+
# @rbs () -> Array[String]
|
|
19
|
+
def additional_server_reasons
|
|
20
|
+
tables = non_innodb_tables
|
|
21
|
+
return [] if tables.empty?
|
|
22
|
+
|
|
23
|
+
[ "these Solid Objects tables do not use InnoDB, so their commits are " \
|
|
24
|
+
"not transactional: #{tables.join(", ")}" ]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs () -> Array[String]
|
|
28
|
+
def non_innodb_tables
|
|
29
|
+
names = SolidObjects::Doctor::EXPECTED_COLUMNS.keys.map { |name| SolidObjects.table_name(name) }
|
|
30
|
+
with_connection do |connection|
|
|
31
|
+
connection.select_rows(<<~SQL.squish).filter_map { |table, engine| table if engine != "InnoDB" }
|
|
32
|
+
SELECT table_name, engine FROM information_schema.tables
|
|
33
|
+
WHERE table_schema = DATABASE()
|
|
34
|
+
AND table_name IN (#{names.map { |name| connection.quote(name) }.join(", ")})
|
|
35
|
+
SQL
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @rbs () -> Gem::Version?
|
|
40
|
+
def minimum_server_version
|
|
41
|
+
Gem::Version.new("8.0")
|
|
42
|
+
end
|
|
43
|
+
|
|
16
44
|
# @rbs () -> String
|
|
17
45
|
def current_time_expression
|
|
18
46
|
"CURRENT_TIMESTAMP(6)"
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class Postgresql < DatabaseAdapter
|
|
6
|
+
# @rbs () -> Gem::Version?
|
|
7
|
+
def minimum_server_version
|
|
8
|
+
Gem::Version.new("13")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# PostgreSQL reports a packed integer, 170010 for 17.10, so comparing it
|
|
12
|
+
# directly would make every server look newer than any minimum.
|
|
13
|
+
# @rbs () -> Gem::Version
|
|
14
|
+
def server_version
|
|
15
|
+
packed = with_connection { |connection| connection.database_version.to_i }
|
|
16
|
+
return super unless packed.positive?
|
|
17
|
+
|
|
18
|
+
Gem::Version.new("#{packed / 10_000}.#{packed % 10_000}")
|
|
19
|
+
end
|
|
20
|
+
|
|
6
21
|
# @rbs () -> bool
|
|
7
22
|
def supports_skip_locked?
|
|
8
23
|
true
|
|
@@ -8,6 +8,11 @@ module SolidObjects
|
|
|
8
8
|
LOCK_RETRY_MUTEX = Thread::Mutex.new
|
|
9
9
|
LOCK_RETRY_CONDITION = Thread::ConditionVariable.new
|
|
10
10
|
|
|
11
|
+
# @rbs () -> Gem::Version?
|
|
12
|
+
def minimum_server_version
|
|
13
|
+
Gem::Version.new("3.35")
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
# @rbs () -> String
|
|
12
17
|
def current_time_expression
|
|
13
18
|
"STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
|
|
@@ -160,13 +165,15 @@ module SolidObjects
|
|
|
160
165
|
end
|
|
161
166
|
end
|
|
162
167
|
|
|
168
|
+
# The deadline can expire between the check above and this wait, which
|
|
169
|
+
# would otherwise ask for a negative interval.
|
|
163
170
|
# @rbs () -> void
|
|
164
171
|
def wait_before_retry
|
|
172
|
+
interval = [ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
|
|
173
|
+
return unless interval.positive?
|
|
174
|
+
|
|
165
175
|
LOCK_RETRY_MUTEX.synchronize do
|
|
166
|
-
LOCK_RETRY_CONDITION.wait(
|
|
167
|
-
LOCK_RETRY_MUTEX,
|
|
168
|
-
[ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
|
|
169
|
-
)
|
|
176
|
+
LOCK_RETRY_CONDITION.wait(LOCK_RETRY_MUTEX, interval)
|
|
170
177
|
end
|
|
171
178
|
end
|
|
172
179
|
end
|
data/lib/solid_objects/doctor.rb
CHANGED
|
@@ -111,6 +111,7 @@ module SolidObjects
|
|
|
111
111
|
configuration_check,
|
|
112
112
|
schema_check,
|
|
113
113
|
check_authorization,
|
|
114
|
+
check_database_server,
|
|
114
115
|
schema_check.failed? ? skipped_runtime : check_runtime,
|
|
115
116
|
ready_for_round_trip?(configuration_check, schema_check) ?
|
|
116
117
|
check_sync_round_trip :
|
|
@@ -191,6 +192,21 @@ module SolidObjects
|
|
|
191
192
|
pass(:authorization, "#{allowed.length} of 5 policies allowed a neutral context")
|
|
192
193
|
end
|
|
193
194
|
|
|
195
|
+
# @rbs () -> Check
|
|
196
|
+
def check_database_server
|
|
197
|
+
adapter = SolidObjects.database_adapter
|
|
198
|
+
observed = adapter.server_version
|
|
199
|
+
reasons = adapter.unsupported_server_reasons(observed)
|
|
200
|
+
return warn_check(:database_server, reasons.join("; ")) unless reasons.empty?
|
|
201
|
+
|
|
202
|
+
pass(
|
|
203
|
+
:database_server,
|
|
204
|
+
"#{adapter.class.name.demodulize} #{observed} meets the tested minimum"
|
|
205
|
+
)
|
|
206
|
+
rescue => error
|
|
207
|
+
warn_check(:database_server, "#{error.class}: #{error.message}")
|
|
208
|
+
end
|
|
209
|
+
|
|
194
210
|
# @rbs () -> Check
|
|
195
211
|
def check_runtime
|
|
196
212
|
cutoff = SolidObjects.database_adapter.database_now -
|