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
@@ -4,7 +4,10 @@ module SolidObjects
4
4
  class Supervisor
5
5
  # @rbs @components: Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
6
6
  # @rbs @threads: Array[Thread]
7
+ # @rbs @monitor: Thread?
7
8
  # @rbs @started: bool
9
+ # @rbs @cleaned_up_at: Float
10
+ # @rbs @lifecycle: Thread::Mutex
8
11
 
9
12
  # @rbs (?worker_count: Integer, ?effect_worker_count: Integer, ?broadcast_worker_count: Integer, ?reminder_scheduler_count: Integer) -> void
10
13
  def initialize(
@@ -20,7 +23,10 @@ module SolidObjects
20
23
  reminder_scheduler_count:
21
24
  )
22
25
  @threads = []
26
+ @monitor = nil
23
27
  @started = false
28
+ @cleaned_up_at = nil
29
+ @lifecycle = Thread::Mutex.new
24
30
  end
25
31
 
26
32
  # @rbs () -> void
@@ -36,7 +42,8 @@ module SolidObjects
36
42
  return if @started
37
43
 
38
44
  @started = true
39
- @threads = components.map { |component| Thread.new { component.run } }
45
+ @threads = components.map { |component| supervise(component) }
46
+ @monitor = Thread.new { monitor_loop }
40
47
  SolidObjects.instrument(:"supervisor.started", component_count: components.length)
41
48
  end
42
49
 
@@ -44,17 +51,129 @@ module SolidObjects
44
51
  def stop
45
52
  return unless @started
46
53
 
47
- components.each(&:request_shutdown)
48
- join_until_timeout
49
- components.reject(&:stopped?).each(&:stop)
50
- @started = false
51
- SolidObjects.instrument(:"supervisor.stopped", component_count: components.length)
54
+ begin
55
+ # Flipping the flag under the same lock replacement takes means a
56
+ # replacement either completes before shutdown reads the component
57
+ # list, or never starts.
58
+ @lifecycle.synchronize { @started = false }
59
+ stop_monitor
60
+ components.each(&:request_shutdown)
61
+ join_until_timeout
62
+ components.reject(&:stopped?).each(&:stop)
63
+ ensure
64
+ # Connections held outside the pool must be released even when a
65
+ # component fails to stop, or they accumulate across restarts.
66
+ release_wake_up
67
+ @monitor = nil
68
+ SolidObjects.instrument(:"supervisor.stopped", component_count: components.length)
69
+ end
52
70
  end
53
71
 
54
72
  private
55
73
 
56
74
  attr_reader :components, :threads
57
75
 
76
+ # A role that raises leaves its thread dead. Without replacement the
77
+ # process keeps running while quietly doing less work, so the supervisor
78
+ # watches its threads and restarts any that stopped before shutdown.
79
+ # A failing pass must not stop supervision, and must not retry without
80
+ # pacing either: a persistently failing database would otherwise spin.
81
+ # @rbs () -> void
82
+ def monitor_loop
83
+ while @started
84
+ begin
85
+ replace_dead_roles
86
+ cleanup_dead_processes
87
+ rescue => error
88
+ SolidObjects.instrument(
89
+ :"supervisor.monitor_failed",
90
+ error_class: error.class.name,
91
+ error_message: error.message
92
+ )
93
+ end
94
+ sleep SolidObjects.configuration.supervisor_monitor_interval
95
+ end
96
+ end
97
+
98
+ # A role that raises runs its own shutdown cleanup on the way out, so a
99
+ # crashed component reports itself stopped exactly like one that was asked
100
+ # to stop. While the supervisor is still running, a dead thread can only
101
+ # mean a crash, so replacement keys on the supervisor rather than on the
102
+ # component. The crashed instance has already released its process record,
103
+ # so a fresh one takes its place.
104
+ # @rbs () -> void
105
+ def replace_dead_roles
106
+ components.each_with_index do |component, index|
107
+ thread = threads[index]
108
+ next if thread&.alive?
109
+
110
+ replaced = @lifecycle.synchronize do
111
+ next false unless @started
112
+
113
+ replacement = component.class.new
114
+ components[index] = replacement
115
+ threads[index] = supervise(replacement)
116
+ replacement
117
+ end
118
+ break unless replaced
119
+
120
+ SolidObjects.instrument(
121
+ :"supervisor.role_replaced",
122
+ role: replaced.class.name,
123
+ error_class: thread_error(thread)
124
+ )
125
+ end
126
+ end
127
+
128
+ # @rbs (Thread?) -> String?
129
+ def thread_error(thread)
130
+ thread&.join
131
+ nil
132
+ rescue => error
133
+ error.class.name
134
+ end
135
+
136
+ # @rbs () -> void
137
+ def cleanup_dead_processes
138
+ interval = SolidObjects.configuration.dead_process_cleanup_interval
139
+ return unless interval.positive?
140
+ return if @cleaned_up_at && monotonic_now - @cleaned_up_at < interval
141
+
142
+ @cleaned_up_at = monotonic_now
143
+ ProcessRegistry.cleanup_dead
144
+ end
145
+
146
+ # @rbs (untyped) -> Thread
147
+ def supervise(component)
148
+ Thread.new { component.run }
149
+ end
150
+
151
+ # The monitor only performs maintenance, so shutdown must never return while
152
+ # it is still alive: a pass blocked on the database would otherwise outlive
153
+ # the supervisor that owns it.
154
+ # @rbs () -> void
155
+ def stop_monitor
156
+ monitor = @monitor
157
+ @monitor = nil
158
+ return unless monitor
159
+
160
+ monitor.join(SolidObjects.configuration.shutdown_timeout)
161
+ monitor.kill if monitor.alive?
162
+ monitor.join(SolidObjects.configuration.supervisor_monitor_interval)
163
+ end
164
+
165
+ # A wake-up adapter may hold connections outside the pool, which would
166
+ # otherwise accumulate across restarts in one process.
167
+ # @rbs () -> void
168
+ def release_wake_up
169
+ wake_up = SolidObjects.wake_up
170
+ return unless wake_up.respond_to?(:stop)
171
+
172
+ wake_up.stop
173
+ rescue
174
+ nil
175
+ end
176
+
58
177
  # @rbs (worker_count: Integer, effect_worker_count: Integer, broadcast_worker_count: Integer, reminder_scheduler_count: Integer) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
59
178
  def build_components(
60
179
  worker_count:,
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.7.3"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -0,0 +1,136 @@
1
+ # rbs_inline: enabled
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 = "solid_objects_wake_up"
14
+ FAILED_WAIT_INTERVAL = 0.05
15
+
16
+ # @rbs @channel: String
17
+ # @rbs @mutex: Thread::Mutex
18
+ # @rbs @connections: Array[untyped]
19
+
20
+ attr_reader :channel
21
+
22
+ # @rbs (?channel: String) -> void
23
+ def initialize(channel: CHANNEL)
24
+ @channel = channel
25
+ @mutex = Thread::Mutex.new
26
+ @connections = []
27
+ end
28
+
29
+ # @rbs () -> bool
30
+ def signal
31
+ notify_channel
32
+ true
33
+ rescue => error
34
+ instrument_failure(:signal, error)
35
+ false
36
+ end
37
+
38
+ # @rbs (timeout: Numeric) -> bool
39
+ def wait(timeout:)
40
+ connection = listening_connection
41
+ !connection.raw_connection.wait_for_notify(timeout.to_f).nil?
42
+ rescue => error
43
+ instrument_failure(:wait, error)
44
+ pace_after_failure(timeout)
45
+ false
46
+ end
47
+
48
+ # Starts listening before a caller blocks, so a notification sent between
49
+ # startup and the first wait is not missed.
50
+ # @rbs () -> bool
51
+ def listen
52
+ listening_connection
53
+ true
54
+ rescue => error
55
+ instrument_failure(:listen, error)
56
+ false
57
+ end
58
+
59
+ # @rbs () -> bool
60
+ def stop
61
+ open = mutex.synchronize do
62
+ listening = connections.dup
63
+ connections.clear
64
+ listening
65
+ end
66
+ Thread.current[thread_key] = nil
67
+ open.each { |connection| disconnect(connection) }
68
+ open.any?
69
+ end
70
+
71
+ private
72
+
73
+ attr_reader :mutex, :connections
74
+
75
+ # @rbs () -> void
76
+ def notify_channel
77
+ Record.connection_pool.with_connection do |connection|
78
+ connection.execute("NOTIFY #{connection.quote_table_name(channel)}")
79
+ end
80
+ end
81
+
82
+ # A listening connection is dedicated and per thread. `LISTEN` is per
83
+ # connection, a blocking wait must not hold a connection the rest of the
84
+ # runtime needs, and one connection cannot serve concurrent waiters: the
85
+ # supervisor shares one adapter across roles, and a notification consumed
86
+ # by one waiter would leave the others asleep until their poll expired.
87
+ # @rbs () -> untyped
88
+ def listening_connection
89
+ connection = Thread.current[thread_key]
90
+ return connection if connection&.active?
91
+
92
+ open_listening_connection
93
+ end
94
+
95
+ # @rbs () -> untyped
96
+ def open_listening_connection
97
+ connection = Record.connection_pool.send(:new_connection)
98
+ connection.execute("LISTEN #{connection.quote_table_name(channel)}")
99
+ Thread.current[thread_key] = connection
100
+ mutex.synchronize { connections << connection }
101
+ connection
102
+ end
103
+
104
+ # @rbs () -> Symbol
105
+ def thread_key
106
+ :"solid_objects_wake_up_#{object_id}"
107
+ end
108
+
109
+ # @rbs (untyped) -> void
110
+ def disconnect(connection)
111
+ connection.disconnect!
112
+ rescue
113
+ nil
114
+ end
115
+
116
+ # @rbs (Numeric) -> void
117
+ def pace_after_failure(timeout)
118
+ interval = [ timeout.to_f, FAILED_WAIT_INTERVAL ].min
119
+ return unless interval.positive?
120
+
121
+ sleep interval
122
+ end
123
+
124
+ # @rbs (Symbol, Exception) -> void
125
+ def instrument_failure(operation, error)
126
+ SolidObjects.instrument(
127
+ :"wake_up.failed",
128
+ adapter: "postgresql",
129
+ operation: operation.to_s,
130
+ error_class: error.class.name,
131
+ error_message: error.message
132
+ )
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,183 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "timeout"
4
+
5
+ module SolidObjects
6
+ module WakeUpAdapters
7
+ # Wakes runtime roles across processes using Redis publish/subscribe.
8
+ #
9
+ # MySQL has no notification primitive, so this is the cross-process option
10
+ # for applications that cannot use PostgreSQL notifications. It is optional
11
+ # in every sense: the `redis` gem is not a dependency of this gem, and the
12
+ # polling interval remains the upper bound, so a missed or failed
13
+ # notification costs latency rather than correctness.
14
+ class Redis
15
+ CHANNEL = "solid_objects_wake_up"
16
+ FAILED_WAIT_INTERVAL = 0.05
17
+ SUBSCRIBE_TIMEOUT = 5.0
18
+
19
+ # @rbs @channel: String
20
+ # @rbs @url: String?
21
+ # @rbs @client: untyped
22
+ # @rbs @mutex: Thread::Mutex
23
+ # @rbs @condition: Thread::ConditionVariable
24
+ # @rbs @subscriber: Thread?
25
+ # @rbs @subscription: untyped
26
+ # @rbs @signalled: Integer
27
+
28
+ attr_reader :channel
29
+
30
+ # @rbs (?channel: String, ?url: String?, ?client: untyped) -> void
31
+ def initialize(channel: CHANNEL, url: nil, client: nil)
32
+ @channel = channel
33
+ @url = url
34
+ @client = client
35
+ @mutex = Thread::Mutex.new
36
+ @condition = Thread::ConditionVariable.new
37
+ @subscriber = nil
38
+ @subscription = nil
39
+ @signalled = 0
40
+ validate_client!
41
+ end
42
+
43
+ # @rbs () -> bool
44
+ def signal
45
+ publisher.publish(channel, "1")
46
+ true
47
+ rescue => error
48
+ instrument_failure(:signal, error)
49
+ false
50
+ end
51
+
52
+ # The counter is snapshotted before subscribing, and re-checked before
53
+ # blocking, so a signal delivered while this caller was still getting
54
+ # ready is observed rather than absorbed into the new baseline.
55
+ # @rbs (timeout: Numeric) -> bool
56
+ def wait(timeout:)
57
+ signalled = mutex.synchronize { @signalled }
58
+ return paced_failure(timeout) unless listen
59
+
60
+ mutex.synchronize do
61
+ return true unless @signalled == signalled
62
+
63
+ condition.wait(mutex, timeout.to_f)
64
+ @signalled != signalled
65
+ end
66
+ end
67
+
68
+ # Redis delivers to a subscribed connection only, and a subscribed
69
+ # connection cannot serve other callers, so one background subscription
70
+ # per process fans out to every waiting role in memory. Subscribing
71
+ # eagerly also closes the window where a signal sent during startup would
72
+ # be missed.
73
+ # @rbs () -> bool
74
+ def listen
75
+ mutex.synchronize do
76
+ return true if @subscriber&.alive?
77
+
78
+ ready = Queue.new
79
+ @subscriber = Thread.new { subscribe_loop(ready) }
80
+ Timeout.timeout(SUBSCRIBE_TIMEOUT) { ready.pop } == :subscribed
81
+ end
82
+ rescue => error
83
+ instrument_failure(:listen, error)
84
+ false
85
+ end
86
+
87
+ # @rbs () -> bool
88
+ def stop
89
+ subscriber = mutex.synchronize do
90
+ thread = @subscriber
91
+ @subscriber = nil
92
+ thread
93
+ end
94
+ return false unless subscriber
95
+
96
+ disconnect(@subscription)
97
+ subscriber.join(SUBSCRIBE_TIMEOUT)
98
+ subscriber.kill if subscriber.alive?
99
+ true
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :mutex, :condition, :url
105
+
106
+ # @rbs (Queue) -> void
107
+ def subscribe_loop(ready)
108
+ connection = build_client
109
+ @subscription = connection
110
+ connection.subscribe(channel) do |on|
111
+ on.subscribe { ready << :subscribed }
112
+ on.message { broadcast }
113
+ end
114
+ rescue => error
115
+ instrument_failure(:subscribe, error)
116
+ ready << :failed
117
+ end
118
+
119
+ # @rbs () -> void
120
+ def broadcast
121
+ mutex.synchronize do
122
+ @signalled += 1
123
+ condition.broadcast
124
+ end
125
+ end
126
+
127
+ # @rbs (Numeric) -> bool
128
+ def paced_failure(timeout)
129
+ pace_after_failure(timeout)
130
+ false
131
+ end
132
+
133
+ # @rbs () -> untyped
134
+ def publisher
135
+ @publisher ||= build_client
136
+ end
137
+
138
+ # @rbs () -> untyped
139
+ def build_client
140
+ return @client.call if @client.respond_to?(:call)
141
+
142
+ require "redis"
143
+ url ? ::Redis.new(url:) : ::Redis.new
144
+ rescue LoadError
145
+ raise ArgumentError,
146
+ "the redis gem is required for SolidObjects::WakeUpAdapters::Redis"
147
+ end
148
+
149
+ # @rbs () -> void
150
+ def validate_client!
151
+ return if @client.nil? || @client.respond_to?(:call)
152
+
153
+ raise ArgumentError, "client must respond to call and return a Redis client"
154
+ end
155
+
156
+ # @rbs (untyped) -> void
157
+ def disconnect(connection)
158
+ connection&.close
159
+ rescue
160
+ nil
161
+ end
162
+
163
+ # @rbs (Numeric) -> void
164
+ def pace_after_failure(timeout)
165
+ interval = [ timeout.to_f, FAILED_WAIT_INTERVAL ].min
166
+ return unless interval.positive?
167
+
168
+ sleep interval
169
+ end
170
+
171
+ # @rbs (Symbol, Exception) -> void
172
+ def instrument_failure(operation, error)
173
+ SolidObjects.instrument(
174
+ :"wake_up.failed",
175
+ adapter: "redis",
176
+ operation: operation.to_s,
177
+ error_class: error.class.name,
178
+ error_message: error.message
179
+ )
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,23 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ module WakeUpAdapters
5
+ module_function
6
+
7
+ # Returns the best wake-up strategy for a connection: cross-process
8
+ # notifications where the database provides them, and the in-process
9
+ # default everywhere else.
10
+ #
11
+ # This is deliberately not the default. A notification adapter opens a
12
+ # connection per waiting thread outside the pool, and `LISTEN` does not
13
+ # survive a transaction-pooling proxy such as PgBouncer, so adopting it is
14
+ # a deployment decision rather than an upgrade side effect.
15
+ #
16
+ # @rbs (?untyped) -> untyped
17
+ def for(connection = Record.connection)
18
+ return Postgresql.new if connection.adapter_name.match?(/postgres/i)
19
+
20
+ WakeUp.new
21
+ end
22
+ end
23
+ end
data/lib/solid_objects.rb CHANGED
@@ -46,6 +46,9 @@ require "solid_objects/actor_view"
46
46
  require "solid_objects/actor_channel"
47
47
  require "solid_objects/action_cable_broadcast_adapter"
48
48
  require "solid_objects/wake_up"
49
+ require "solid_objects/wake_up_adapters/postgresql"
50
+ require "solid_objects/wake_up_adapters/redis"
51
+ require "solid_objects/wake_up_adapters"
49
52
  require "solid_objects/effect_registry"
50
53
  require "solid_objects/commit_action_registry"
51
54
  require "solid_objects/lease"
@@ -2,10 +2,12 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Configuration
5
- @process_alive_threshold: Float
6
-
7
5
  @shutdown_timeout: Float
8
6
 
7
+ @supervisor_monitor_interval: Float
8
+
9
+ @dead_process_cleanup_interval: Float
10
+
9
11
  @message_retention: Numeric
10
12
 
11
13
  @message_retention_by_actor_type: Hash[String, Numeric]
@@ -82,6 +84,8 @@ module SolidObjects
82
84
 
83
85
  @process_heartbeat_interval: Float
84
86
 
87
+ @process_alive_threshold: Float
88
+
85
89
  attr_accessor table_name_prefix: untyped
86
90
 
87
91
  attr_accessor polling_interval: untyped
@@ -120,6 +124,10 @@ module SolidObjects
120
124
 
121
125
  attr_accessor shutdown_timeout: untyped
122
126
 
127
+ attr_accessor supervisor_monitor_interval: untyped
128
+
129
+ attr_accessor dead_process_cleanup_interval: untyped
130
+
123
131
  attr_accessor message_retention: untyped
124
132
 
125
133
  attr_accessor message_retention_by_actor_type: untyped
@@ -16,6 +16,23 @@ module SolidObjects
16
16
  # @rbs (untyped) -> void
17
17
  def initialize: (untyped) -> void
18
18
 
19
+ # The oldest server the adapter has been exercised against. Reported rather
20
+ # than enforced: refusing to boot on an untested server would be a worse
21
+ # failure than running on one.
22
+ # @rbs () -> Gem::Version?
23
+ def minimum_server_version: () -> Gem::Version?
24
+
25
+ # @rbs () -> Gem::Version
26
+ def server_version: () -> Gem::Version
27
+
28
+ # One observed version decides both the status and the message. Reading it
29
+ # again could let a transient failure replace an already determined result.
30
+ # @rbs (?Gem::Version?) -> Array[String]
31
+ def unsupported_server_reasons: (?Gem::Version?) -> Array[String]
32
+
33
+ # @rbs () -> Array[String]
34
+ def additional_server_reasons: () -> Array[String]
35
+
19
36
  # @rbs () -> bool
20
37
  def supports_skip_locked?: () -> bool
21
38
 
@@ -9,6 +9,17 @@ module SolidObjects
9
9
  # @rbs () -> String
10
10
  def claim_lock: () -> String
11
11
 
12
+ # A non-transactional engine would silently break fenced commits, so the
13
+ # storage engine is verified rather than assumed.
14
+ # @rbs () -> Array[String]
15
+ def additional_server_reasons: () -> Array[String]
16
+
17
+ # @rbs () -> Array[String]
18
+ def non_innodb_tables: () -> Array[String]
19
+
20
+ # @rbs () -> Gem::Version?
21
+ def minimum_server_version: () -> Gem::Version?
22
+
12
23
  # @rbs () -> String
13
24
  def current_time_expression: () -> String
14
25
 
@@ -3,6 +3,14 @@
3
3
  module SolidObjects
4
4
  module DatabaseAdapters
5
5
  class Postgresql < DatabaseAdapter
6
+ # @rbs () -> Gem::Version?
7
+ def minimum_server_version: () -> Gem::Version?
8
+
9
+ # PostgreSQL reports a packed integer, 170010 for 17.10, so comparing it
10
+ # directly would make every server look newer than any minimum.
11
+ # @rbs () -> Gem::Version
12
+ def server_version: () -> Gem::Version
13
+
6
14
  # @rbs () -> bool
7
15
  def supports_skip_locked?: () -> bool
8
16
 
@@ -11,6 +11,9 @@ module SolidObjects
11
11
 
12
12
  LOCK_RETRY_CONDITION: untyped
13
13
 
14
+ # @rbs () -> Gem::Version?
15
+ def minimum_server_version: () -> Gem::Version?
16
+
14
17
  # @rbs () -> String
15
18
  def current_time_expression: () -> String
16
19
 
@@ -52,6 +55,8 @@ module SolidObjects
52
55
  # @rbs (Integer) -> void
53
56
  def wait_before_busy_retry: (Integer) -> void
54
57
 
58
+ # The deadline can expire between the check above and this wait, which
59
+ # would otherwise ask for a negative interval.
55
60
  # @rbs () -> void
56
61
  def wait_before_retry: () -> void
57
62
  end
@@ -72,6 +72,9 @@ module SolidObjects
72
72
  # @rbs () -> Check
73
73
  def check_authorization: () -> Check
74
74
 
75
+ # @rbs () -> Check
76
+ def check_database_server: () -> Check
77
+
75
78
  # @rbs () -> Check
76
79
  def check_runtime: () -> Check
77
80