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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/docs/benchmarks.md +15 -0
  4. data/docs/local-testing.md +91 -0
  5. data/docs/realtime.md +43 -0
  6. data/docs/roadmap.md +35 -26
  7. data/lib/generators/solid_objects/templates/solid_objects.rb +8 -0
  8. data/lib/solid_objects/configuration.rb +10 -0
  9. data/lib/solid_objects/database_adapter.rb +37 -0
  10. data/lib/solid_objects/database_adapters/mysql.rb +28 -0
  11. data/lib/solid_objects/database_adapters/postgresql.rb +15 -0
  12. data/lib/solid_objects/database_adapters/sqlite.rb +11 -4
  13. data/lib/solid_objects/doctor.rb +16 -0
  14. data/lib/solid_objects/supervisor.rb +125 -6
  15. data/lib/solid_objects/version.rb +1 -1
  16. data/lib/solid_objects/wake_up_adapters/postgresql.rb +136 -0
  17. data/lib/solid_objects/wake_up_adapters/redis.rb +183 -0
  18. data/lib/solid_objects/wake_up_adapters.rb +23 -0
  19. data/lib/solid_objects.rb +3 -0
  20. data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
  21. data/sig/generated/lib/solid_objects/database_adapter.rbs +17 -0
  22. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +11 -0
  23. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
  24. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +5 -0
  25. data/sig/generated/lib/solid_objects/doctor.rbs +3 -0
  26. data/sig/generated/lib/solid_objects/supervisor.rbs +45 -2
  27. data/sig/generated/lib/solid_objects/wake_up_adapters/postgresql.rbs +75 -0
  28. data/sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs +96 -0
  29. data/sig/generated/lib/solid_objects/wake_up_adapters.rbs +17 -0
  30. metadata +9 -2
@@ -2,12 +2,18 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Supervisor
5
- @components: Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
5
+ @lifecycle: Thread::Mutex
6
6
 
7
- @threads: Array[Thread]
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.7.3
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-09 00:00:00.000000000 Z
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