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
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Supervisor
|
|
5
|
-
@
|
|
5
|
+
@lifecycle: Thread::Mutex
|
|
6
6
|
|
|
7
|
-
@
|
|
7
|
+
@cleaned_up_at: Float
|
|
8
8
|
|
|
9
9
|
@started: bool
|
|
10
10
|
|
|
11
|
+
@monitor: Thread?
|
|
12
|
+
|
|
13
|
+
@threads: Array[Thread]
|
|
14
|
+
|
|
15
|
+
@components: Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
|
|
16
|
+
|
|
11
17
|
# @rbs (?worker_count: Integer, ?effect_worker_count: Integer, ?broadcast_worker_count: Integer, ?reminder_scheduler_count: Integer) -> void
|
|
12
18
|
def initialize: (?worker_count: Integer, ?effect_worker_count: Integer, ?broadcast_worker_count: Integer, ?reminder_scheduler_count: Integer) -> void
|
|
13
19
|
|
|
@@ -26,6 +32,43 @@ module SolidObjects
|
|
|
26
32
|
|
|
27
33
|
attr_reader threads: untyped
|
|
28
34
|
|
|
35
|
+
# A role that raises leaves its thread dead. Without replacement the
|
|
36
|
+
# process keeps running while quietly doing less work, so the supervisor
|
|
37
|
+
# watches its threads and restarts any that stopped before shutdown.
|
|
38
|
+
# A failing pass must not stop supervision, and must not retry without
|
|
39
|
+
# pacing either: a persistently failing database would otherwise spin.
|
|
40
|
+
# @rbs () -> void
|
|
41
|
+
def monitor_loop: () -> void
|
|
42
|
+
|
|
43
|
+
# A role that raises runs its own shutdown cleanup on the way out, so a
|
|
44
|
+
# crashed component reports itself stopped exactly like one that was asked
|
|
45
|
+
# to stop. While the supervisor is still running, a dead thread can only
|
|
46
|
+
# mean a crash, so replacement keys on the supervisor rather than on the
|
|
47
|
+
# component. The crashed instance has already released its process record,
|
|
48
|
+
# so a fresh one takes its place.
|
|
49
|
+
# @rbs () -> void
|
|
50
|
+
def replace_dead_roles: () -> void
|
|
51
|
+
|
|
52
|
+
# @rbs (Thread?) -> String?
|
|
53
|
+
def thread_error: (Thread?) -> String?
|
|
54
|
+
|
|
55
|
+
# @rbs () -> void
|
|
56
|
+
def cleanup_dead_processes: () -> void
|
|
57
|
+
|
|
58
|
+
# @rbs (untyped) -> Thread
|
|
59
|
+
def supervise: (untyped) -> Thread
|
|
60
|
+
|
|
61
|
+
# The monitor only performs maintenance, so shutdown must never return while
|
|
62
|
+
# it is still alive: a pass blocked on the database would otherwise outlive
|
|
63
|
+
# the supervisor that owns it.
|
|
64
|
+
# @rbs () -> void
|
|
65
|
+
def stop_monitor: () -> void
|
|
66
|
+
|
|
67
|
+
# A wake-up adapter may hold connections outside the pool, which would
|
|
68
|
+
# otherwise accumulate across restarts in one process.
|
|
69
|
+
# @rbs () -> void
|
|
70
|
+
def release_wake_up: () -> void
|
|
71
|
+
|
|
29
72
|
# @rbs (worker_count: Integer, effect_worker_count: Integer, broadcast_worker_count: Integer, reminder_scheduler_count: Integer) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
|
|
30
73
|
def build_components: (worker_count: Integer, effect_worker_count: Integer, broadcast_worker_count: Integer, reminder_scheduler_count: Integer) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
|
|
31
74
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/wake_up_adapters/postgresql.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
module WakeUpAdapters
|
|
5
|
+
# Wakes runtime roles across processes using PostgreSQL notifications.
|
|
6
|
+
#
|
|
7
|
+
# The in-process wake-up cannot reach another process, so a commit in a web
|
|
8
|
+
# process leaves a worker waiting out its polling interval. This adapter
|
|
9
|
+
# keeps that polling interval as the upper bound and delivers a notification
|
|
10
|
+
# when one is available, so a missed or failed notification costs latency
|
|
11
|
+
# rather than correctness.
|
|
12
|
+
class Postgresql
|
|
13
|
+
CHANNEL: ::String
|
|
14
|
+
|
|
15
|
+
FAILED_WAIT_INTERVAL: ::Float
|
|
16
|
+
|
|
17
|
+
@connections: Array[untyped]
|
|
18
|
+
|
|
19
|
+
@mutex: Thread::Mutex
|
|
20
|
+
|
|
21
|
+
@channel: String
|
|
22
|
+
|
|
23
|
+
attr_reader channel: untyped
|
|
24
|
+
|
|
25
|
+
# @rbs (?channel: String) -> void
|
|
26
|
+
def initialize: (?channel: String) -> void
|
|
27
|
+
|
|
28
|
+
# @rbs () -> bool
|
|
29
|
+
def signal: () -> bool
|
|
30
|
+
|
|
31
|
+
# @rbs (timeout: Numeric) -> bool
|
|
32
|
+
def wait: (timeout: Numeric) -> bool
|
|
33
|
+
|
|
34
|
+
# Starts listening before a caller blocks, so a notification sent between
|
|
35
|
+
# startup and the first wait is not missed.
|
|
36
|
+
# @rbs () -> bool
|
|
37
|
+
def listen: () -> bool
|
|
38
|
+
|
|
39
|
+
# @rbs () -> bool
|
|
40
|
+
def stop: () -> bool
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader mutex: untyped
|
|
45
|
+
|
|
46
|
+
attr_reader connections: untyped
|
|
47
|
+
|
|
48
|
+
# @rbs () -> void
|
|
49
|
+
def notify_channel: () -> void
|
|
50
|
+
|
|
51
|
+
# A listening connection is dedicated and per thread. `LISTEN` is per
|
|
52
|
+
# connection, a blocking wait must not hold a connection the rest of the
|
|
53
|
+
# runtime needs, and one connection cannot serve concurrent waiters: the
|
|
54
|
+
# supervisor shares one adapter across roles, and a notification consumed
|
|
55
|
+
# by one waiter would leave the others asleep until their poll expired.
|
|
56
|
+
# @rbs () -> untyped
|
|
57
|
+
def listening_connection: () -> untyped
|
|
58
|
+
|
|
59
|
+
# @rbs () -> untyped
|
|
60
|
+
def open_listening_connection: () -> untyped
|
|
61
|
+
|
|
62
|
+
# @rbs () -> Symbol
|
|
63
|
+
def thread_key: () -> Symbol
|
|
64
|
+
|
|
65
|
+
# @rbs (untyped) -> void
|
|
66
|
+
def disconnect: (untyped) -> void
|
|
67
|
+
|
|
68
|
+
# @rbs (Numeric) -> void
|
|
69
|
+
def pace_after_failure: (Numeric) -> void
|
|
70
|
+
|
|
71
|
+
# @rbs (Symbol, Exception) -> void
|
|
72
|
+
def instrument_failure: (Symbol, Exception) -> void
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/wake_up_adapters/redis.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
module WakeUpAdapters
|
|
5
|
+
# Wakes runtime roles across processes using Redis publish/subscribe.
|
|
6
|
+
#
|
|
7
|
+
# MySQL has no notification primitive, so this is the cross-process option
|
|
8
|
+
# for applications that cannot use PostgreSQL notifications. It is optional
|
|
9
|
+
# in every sense: the `redis` gem is not a dependency of this gem, and the
|
|
10
|
+
# polling interval remains the upper bound, so a missed or failed
|
|
11
|
+
# notification costs latency rather than correctness.
|
|
12
|
+
class Redis
|
|
13
|
+
CHANNEL: ::String
|
|
14
|
+
|
|
15
|
+
FAILED_WAIT_INTERVAL: ::Float
|
|
16
|
+
|
|
17
|
+
SUBSCRIBE_TIMEOUT: ::Float
|
|
18
|
+
|
|
19
|
+
@signalled: Integer
|
|
20
|
+
|
|
21
|
+
@subscription: untyped
|
|
22
|
+
|
|
23
|
+
@subscriber: Thread?
|
|
24
|
+
|
|
25
|
+
@condition: Thread::ConditionVariable
|
|
26
|
+
|
|
27
|
+
@mutex: Thread::Mutex
|
|
28
|
+
|
|
29
|
+
@client: untyped
|
|
30
|
+
|
|
31
|
+
@url: String?
|
|
32
|
+
|
|
33
|
+
@channel: String
|
|
34
|
+
|
|
35
|
+
attr_reader channel: untyped
|
|
36
|
+
|
|
37
|
+
# @rbs (?channel: String, ?url: String?, ?client: untyped) -> void
|
|
38
|
+
def initialize: (?channel: String, ?url: String?, ?client: untyped) -> void
|
|
39
|
+
|
|
40
|
+
# @rbs () -> bool
|
|
41
|
+
def signal: () -> bool
|
|
42
|
+
|
|
43
|
+
# The counter is snapshotted before subscribing, and re-checked before
|
|
44
|
+
# blocking, so a signal delivered while this caller was still getting
|
|
45
|
+
# ready is observed rather than absorbed into the new baseline.
|
|
46
|
+
# @rbs (timeout: Numeric) -> bool
|
|
47
|
+
def wait: (timeout: Numeric) -> bool
|
|
48
|
+
|
|
49
|
+
# Redis delivers to a subscribed connection only, and a subscribed
|
|
50
|
+
# connection cannot serve other callers, so one background subscription
|
|
51
|
+
# per process fans out to every waiting role in memory. Subscribing
|
|
52
|
+
# eagerly also closes the window where a signal sent during startup would
|
|
53
|
+
# be missed.
|
|
54
|
+
# @rbs () -> bool
|
|
55
|
+
def listen: () -> bool
|
|
56
|
+
|
|
57
|
+
# @rbs () -> bool
|
|
58
|
+
def stop: () -> bool
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
attr_reader mutex: untyped
|
|
63
|
+
|
|
64
|
+
attr_reader condition: untyped
|
|
65
|
+
|
|
66
|
+
attr_reader url: untyped
|
|
67
|
+
|
|
68
|
+
# @rbs (Queue) -> void
|
|
69
|
+
def subscribe_loop: (Queue) -> void
|
|
70
|
+
|
|
71
|
+
# @rbs () -> void
|
|
72
|
+
def broadcast: () -> void
|
|
73
|
+
|
|
74
|
+
# @rbs (Numeric) -> bool
|
|
75
|
+
def paced_failure: (Numeric) -> bool
|
|
76
|
+
|
|
77
|
+
# @rbs () -> untyped
|
|
78
|
+
def publisher: () -> untyped
|
|
79
|
+
|
|
80
|
+
# @rbs () -> untyped
|
|
81
|
+
def build_client: () -> untyped
|
|
82
|
+
|
|
83
|
+
# @rbs () -> void
|
|
84
|
+
def validate_client!: () -> void
|
|
85
|
+
|
|
86
|
+
# @rbs (untyped) -> void
|
|
87
|
+
def disconnect: (untyped) -> void
|
|
88
|
+
|
|
89
|
+
# @rbs (Numeric) -> void
|
|
90
|
+
def pace_after_failure: (Numeric) -> void
|
|
91
|
+
|
|
92
|
+
# @rbs (Symbol, Exception) -> void
|
|
93
|
+
def instrument_failure: (Symbol, Exception) -> void
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/solid_objects/wake_up_adapters.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
module WakeUpAdapters
|
|
5
|
+
# Returns the best wake-up strategy for a connection: cross-process
|
|
6
|
+
# notifications where the database provides them, and the in-process
|
|
7
|
+
# default everywhere else.
|
|
8
|
+
#
|
|
9
|
+
# This is deliberately not the default. A notification adapter opens a
|
|
10
|
+
# connection per waiting thread outside the pool, and `LISTEN` does not
|
|
11
|
+
# survive a transaction-pooling proxy such as PgBouncer, so adopting it is
|
|
12
|
+
# a deployment decision rather than an upgrade side effect.
|
|
13
|
+
#
|
|
14
|
+
# @rbs (?untyped) -> untyped
|
|
15
|
+
def self?.for: (?untyped) -> untyped
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucas Carlson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: actioncable
|
|
@@ -319,6 +319,7 @@ files:
|
|
|
319
319
|
- docs/development.md
|
|
320
320
|
- docs/fit.md
|
|
321
321
|
- docs/implementation-plan.md
|
|
322
|
+
- docs/local-testing.md
|
|
322
323
|
- docs/migrating-existing-state.md
|
|
323
324
|
- docs/operations.md
|
|
324
325
|
- docs/realtime.md
|
|
@@ -403,6 +404,9 @@ files:
|
|
|
403
404
|
- lib/solid_objects/turbo_stream_renderer.rb
|
|
404
405
|
- lib/solid_objects/version.rb
|
|
405
406
|
- lib/solid_objects/wake_up.rb
|
|
407
|
+
- lib/solid_objects/wake_up_adapters.rb
|
|
408
|
+
- lib/solid_objects/wake_up_adapters/postgresql.rb
|
|
409
|
+
- lib/solid_objects/wake_up_adapters/redis.rb
|
|
406
410
|
- lib/solid_objects/worker.rb
|
|
407
411
|
- lib/tasks/solid_objects_tasks.rake
|
|
408
412
|
- sig/generated/controllers/solid_objects/application_controller.rbs
|
|
@@ -474,6 +478,9 @@ files:
|
|
|
474
478
|
- sig/generated/lib/solid_objects/turbo_stream_renderer.rbs
|
|
475
479
|
- sig/generated/lib/solid_objects/version.rbs
|
|
476
480
|
- sig/generated/lib/solid_objects/wake_up.rbs
|
|
481
|
+
- sig/generated/lib/solid_objects/wake_up_adapters.rbs
|
|
482
|
+
- sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs
|
|
483
|
+
- sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs
|
|
477
484
|
- sig/generated/lib/solid_objects/worker.rbs
|
|
478
485
|
- sig/generated/models/solid_objects/broadcast.rbs
|
|
479
486
|
- sig/generated/models/solid_objects/claimed_message.rbs
|