solid_objects 0.2.0 → 0.3.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +223 -15
  4. data/benchmark/adoption_latency.rb +5 -0
  5. data/benchmark/support.rb +35 -0
  6. data/docs/architecture.md +62 -15
  7. data/docs/authorization.md +98 -0
  8. data/docs/benchmarks.md +75 -1
  9. data/docs/correctness.md +19 -1
  10. data/docs/database-schema.md +5 -0
  11. data/docs/development.md +45 -4
  12. data/docs/fit.md +98 -0
  13. data/docs/migrating-existing-state.md +140 -0
  14. data/docs/operations.md +94 -4
  15. data/docs/roadmap.md +12 -3
  16. data/docs/security.md +28 -3
  17. data/docs/state-migrations.md +6 -0
  18. data/lib/generators/solid_objects/templates/solid_objects.rb +45 -0
  19. data/lib/solid_objects/activation.rb +33 -4
  20. data/lib/solid_objects/actor.rb +47 -6
  21. data/lib/solid_objects/actor_definition.rb +2 -0
  22. data/lib/solid_objects/actor_snapshot.rb +10 -4
  23. data/lib/solid_objects/application_write_guard.rb +24 -0
  24. data/lib/solid_objects/caller_process.rb +28 -0
  25. data/lib/solid_objects/cli.rb +44 -5
  26. data/lib/solid_objects/client.rb +98 -5
  27. data/lib/solid_objects/commit_action_registry.rb +42 -0
  28. data/lib/solid_objects/configuration.rb +27 -1
  29. data/lib/solid_objects/database_adapter.rb +28 -1
  30. data/lib/solid_objects/database_adapters/mysql.rb +46 -0
  31. data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
  32. data/lib/solid_objects/database_adapters/sqlite.rb +28 -0
  33. data/lib/solid_objects/doctor.rb +311 -0
  34. data/lib/solid_objects/errors.rb +144 -0
  35. data/lib/solid_objects/executor.rb +64 -4
  36. data/lib/solid_objects/instance_pruner.rb +97 -0
  37. data/lib/solid_objects/message_pruner.rb +97 -0
  38. data/lib/solid_objects/message_reference.rb +9 -0
  39. data/lib/solid_objects/process_pruner.rb +49 -0
  40. data/lib/solid_objects/reference.rb +5 -0
  41. data/lib/solid_objects/state_snapshot.rb +41 -0
  42. data/lib/solid_objects/sync_deadline.rb +57 -0
  43. data/lib/solid_objects/sync_diagnostics.rb +133 -0
  44. data/lib/solid_objects/synchronous_invocation.rb +26 -7
  45. data/lib/solid_objects/test_helper.rb +78 -0
  46. data/lib/solid_objects/version.rb +1 -1
  47. data/lib/solid_objects/worker.rb +1 -1
  48. data/lib/solid_objects.rb +34 -0
  49. data/lib/tasks/solid_objects_tasks.rake +10 -0
  50. data/sig/generated/lib/solid_objects/activation.rbs +3 -0
  51. data/sig/generated/lib/solid_objects/actor.rbs +26 -0
  52. data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
  53. data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
  54. data/sig/generated/lib/solid_objects/cli.rbs +11 -2
  55. data/sig/generated/lib/solid_objects/client.rbs +15 -0
  56. data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
  57. data/sig/generated/lib/solid_objects/configuration.rbs +27 -7
  58. data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
  59. data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
  60. data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
  61. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +8 -0
  62. data/sig/generated/lib/solid_objects/doctor.rbs +111 -0
  63. data/sig/generated/lib/solid_objects/errors.rbs +118 -0
  64. data/sig/generated/lib/solid_objects/executor.rbs +12 -0
  65. data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
  66. data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
  67. data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
  68. data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
  69. data/sig/generated/lib/solid_objects/reference.rbs +3 -0
  70. data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
  71. data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
  72. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
  73. data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
  74. data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
  75. data/sig/generated/lib/solid_objects.rbs +12 -0
  76. metadata +26 -1
@@ -0,0 +1,42 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ CommitActionContext = Data.define(
5
+ :message_id,
6
+ :request_id,
7
+ :actor_type,
8
+ :actor_id,
9
+ :activation_generation
10
+ )
11
+
12
+ class CommitActionRegistry
13
+ # @rbs @handlers: Hash[String, Proc]
14
+ # @rbs @mutex: Mutex
15
+
16
+ # @rbs () -> void
17
+ def initialize
18
+ @handlers = {}
19
+ @mutex = Mutex.new
20
+ end
21
+
22
+ # @rbs (String | Symbol, Proc) -> Proc
23
+ def register(name, handler)
24
+ action_name = name.to_s
25
+ raise ArgumentError, "commit action name cannot be empty" if action_name.empty?
26
+
27
+ mutex.synchronize { handlers[action_name] = handler }
28
+ handler
29
+ end
30
+
31
+ # @rbs (String | Symbol) -> Proc
32
+ def fetch(name)
33
+ handlers.fetch(name.to_s) do
34
+ raise UnknownCommitAction, "no commit action registered for #{name.inspect}"
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :handlers, :mutex
41
+ end
42
+ end
@@ -20,6 +20,11 @@ module SolidObjects
20
20
  # @rbs @process_heartbeat_interval: Float
21
21
  # @rbs @process_alive_threshold: Float
22
22
  # @rbs @shutdown_timeout: Float
23
+ # @rbs @message_retention: Numeric
24
+ # @rbs @message_retention_by_actor_type: Hash[String, Numeric]
25
+ # @rbs @instance_retention_by_actor_type: Hash[String, Numeric]
26
+ # @rbs @process_retention: Numeric
27
+ # @rbs @prune_batch_size: Integer
23
28
  # @rbs @worker_count: Integer
24
29
  # @rbs @effect_worker_count: Integer
25
30
  # @rbs @broadcast_worker_count: Integer
@@ -53,6 +58,11 @@ module SolidObjects
53
58
  :process_heartbeat_interval,
54
59
  :process_alive_threshold,
55
60
  :shutdown_timeout,
61
+ :message_retention,
62
+ :message_retention_by_actor_type,
63
+ :instance_retention_by_actor_type,
64
+ :process_retention,
65
+ :prune_batch_size,
56
66
  :worker_count,
57
67
  :effect_worker_count,
58
68
  :broadcast_worker_count,
@@ -88,6 +98,11 @@ module SolidObjects
88
98
  @process_heartbeat_interval = 15.0
89
99
  @process_alive_threshold = 60.0
90
100
  @shutdown_timeout = 15.0
101
+ @message_retention = 30.days
102
+ @message_retention_by_actor_type = {}
103
+ @instance_retention_by_actor_type = {}
104
+ @process_retention = 7.days
105
+ @prune_batch_size = 1_000
91
106
  @worker_count = 1
92
107
  @effect_worker_count = 1
93
108
  @broadcast_worker_count = 1
@@ -128,6 +143,14 @@ module SolidObjects
128
143
  positive_values.each do |name, value|
129
144
  raise ArgumentError, "#{name} must be positive" unless value.positive?
130
145
  end
146
+ message_retention_by_actor_type.each do |actor_type, retention|
147
+ raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
148
+ raise ArgumentError, "message retention must be positive" unless retention.positive?
149
+ end
150
+ instance_retention_by_actor_type.each do |actor_type, retention|
151
+ raise ArgumentError, "actor type cannot be empty" if actor_type.to_s.empty?
152
+ raise ArgumentError, "instance retention must be positive" unless retention.positive?
153
+ end
131
154
 
132
155
  self
133
156
  end
@@ -151,7 +174,10 @@ module SolidObjects
151
174
  max_attempts:,
152
175
  process_heartbeat_interval:,
153
176
  process_alive_threshold:,
154
- shutdown_timeout:
177
+ shutdown_timeout:,
178
+ message_retention:,
179
+ process_retention:,
180
+ prune_batch_size:
155
181
  }
156
182
  end
157
183
 
@@ -54,9 +54,22 @@ module SolidObjects
54
54
 
55
55
  # @rbs () { () -> untyped } -> untyped
56
56
  def transaction(&block)
57
+ raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
58
+
57
59
  with_connection do |connection|
58
- connection.transaction(requires_new: true, &block)
60
+ with_transaction_deadline(connection) do
61
+ connection.transaction(requires_new: true) do
62
+ configure_transaction_deadline(connection)
63
+ block.call
64
+ end
65
+ end
59
66
  end
67
+ rescue => error
68
+ raise unless deadline_error?(error)
69
+
70
+ raise DatabaseDeadlineExceeded,
71
+ "database lock wait exceeded the synchronous invocation deadline",
72
+ cause: error
60
73
  end
61
74
 
62
75
  # @rbs (ActiveRecord::Relation[untyped]) -> ActiveRecord::Relation[untyped]
@@ -68,6 +81,20 @@ module SolidObjects
68
81
 
69
82
  attr_reader :connection_pool, :fixed_connection
70
83
 
84
+ # @rbs (untyped) { () -> untyped } -> untyped
85
+ def with_transaction_deadline(_connection)
86
+ yield
87
+ end
88
+
89
+ # @rbs (untyped) -> void
90
+ def configure_transaction_deadline(_connection)
91
+ end
92
+
93
+ # @rbs (Exception) -> bool
94
+ def deadline_error?(_error)
95
+ false
96
+ end
97
+
71
98
  # @rbs () { (untyped) -> untyped } -> untyped
72
99
  def with_connection
73
100
  return yield fixed_connection unless connection_pool
@@ -17,6 +17,52 @@ module SolidObjects
17
17
  def current_time_expression
18
18
  "CURRENT_TIMESTAMP(6)"
19
19
  end
20
+
21
+ private
22
+
23
+ # @rbs (untyped) { () -> untyped } -> untyped
24
+ def with_transaction_deadline(connection)
25
+ return yield unless SyncDeadline.active?
26
+
27
+ previous_lock_timeout = connection.select_value(
28
+ "SELECT @@SESSION.innodb_lock_wait_timeout"
29
+ ).to_i
30
+ previous_execution_timeout = connection.select_value(
31
+ "SELECT @@SESSION.max_execution_time"
32
+ ).to_i
33
+ connection.execute(
34
+ "SET SESSION innodb_lock_wait_timeout = #{SyncDeadline.remaining_seconds}"
35
+ )
36
+ connection.execute(
37
+ "SET SESSION max_execution_time = #{SyncDeadline.remaining_milliseconds}"
38
+ )
39
+ yield
40
+ ensure
41
+ if previous_lock_timeout
42
+ connection.execute(
43
+ "SET SESSION innodb_lock_wait_timeout = #{previous_lock_timeout}"
44
+ )
45
+ end
46
+ if previous_execution_timeout
47
+ connection.execute(
48
+ "SET SESSION max_execution_time = #{previous_execution_timeout}"
49
+ )
50
+ end
51
+ end
52
+
53
+ # @rbs (Exception) -> bool
54
+ def deadline_error?(error)
55
+ return false unless SyncDeadline.active?
56
+ return true if error.is_a?(ActiveRecord::LockWaitTimeout)
57
+
58
+ cause = error
59
+ while cause
60
+ return true if cause.respond_to?(:error_number) && cause.error_number == 3024
61
+
62
+ cause = cause.cause
63
+ end
64
+ false
65
+ end
20
66
  end
21
67
  end
22
68
  end
@@ -12,6 +12,35 @@ module SolidObjects
12
12
  def claim_lock
13
13
  "FOR UPDATE SKIP LOCKED"
14
14
  end
15
+
16
+ private
17
+
18
+ # @rbs (untyped) -> void
19
+ def configure_transaction_deadline(connection)
20
+ return unless SyncDeadline.active?
21
+
22
+ connection.execute(
23
+ "SET LOCAL lock_timeout = #{SyncDeadline.remaining_milliseconds}"
24
+ )
25
+ connection.execute(
26
+ "SET LOCAL statement_timeout = #{SyncDeadline.remaining_milliseconds}"
27
+ )
28
+ end
29
+
30
+ # @rbs (Exception) -> bool
31
+ def deadline_error?(error)
32
+ return false unless SyncDeadline.active?
33
+ return true if error.is_a?(ActiveRecord::LockWaitTimeout)
34
+
35
+ cause = error
36
+ while cause
37
+ result = cause.respond_to?(:result) ? cause.result : nil
38
+ return true if result&.error_field(PG::Result::PG_DIAG_SQLSTATE) == "57014"
39
+
40
+ cause = cause.cause
41
+ end
42
+ false
43
+ end
15
44
  end
16
45
  end
17
46
  end
@@ -7,6 +7,34 @@ module SolidObjects
7
7
  def current_time_expression
8
8
  "STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')"
9
9
  end
10
+
11
+ private
12
+
13
+ # @rbs (untyped) { () -> untyped } -> untyped
14
+ def with_transaction_deadline(connection)
15
+ return yield unless SyncDeadline.active?
16
+
17
+ previous_timeout = connection.select_value("PRAGMA busy_timeout").to_i
18
+ connection.execute(
19
+ "PRAGMA busy_timeout = #{SyncDeadline.remaining_milliseconds}"
20
+ )
21
+ yield
22
+ ensure
23
+ connection.execute("PRAGMA busy_timeout = #{previous_timeout}") if previous_timeout
24
+ end
25
+
26
+ # @rbs (Exception) -> bool
27
+ def deadline_error?(error)
28
+ return false unless SyncDeadline.active?
29
+
30
+ cause = error
31
+ while cause
32
+ return true if cause.class.name.match?(/BusyException|BusyError/)
33
+
34
+ cause = cause.cause
35
+ end
36
+ false
37
+ end
10
38
  end
11
39
  end
12
40
  end
@@ -0,0 +1,311 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "solid_objects/mailbox"
4
+ require "solid_objects/synchronous_invocation"
5
+
6
+ module SolidObjects
7
+ class Doctor
8
+ class Check
9
+ # @rbs @name: Symbol
10
+ # @rbs @status: Symbol
11
+ # @rbs @message: String
12
+
13
+ attr_reader :name, :status, :message
14
+
15
+ # @rbs (name: Symbol, status: Symbol, message: String) -> void
16
+ def initialize(name:, status:, message:)
17
+ @name = name
18
+ @status = status
19
+ @message = message
20
+ freeze
21
+ end
22
+
23
+ # @rbs () -> bool
24
+ def failed?
25
+ status == :fail
26
+ end
27
+ end
28
+
29
+ class Report
30
+ # @rbs @checks: Array[Check]
31
+
32
+ attr_reader :checks
33
+
34
+ # @rbs (checks: Array[Check]) -> void
35
+ def initialize(checks:)
36
+ @checks = checks.freeze
37
+ freeze
38
+ end
39
+
40
+ # @rbs () -> bool
41
+ def healthy?
42
+ checks.none?(&:failed?)
43
+ end
44
+
45
+ # @rbs (Symbol) -> Check
46
+ def check(name)
47
+ checks.find { |candidate| candidate.name == name } ||
48
+ raise(KeyError, "unknown doctor check #{name.inspect}")
49
+ end
50
+
51
+ # @rbs () -> String
52
+ def to_s
53
+ lines = [ "Solid Objects doctor #{SolidObjects::VERSION}" ]
54
+ lines.concat(
55
+ checks.map do |check|
56
+ "#{check.status.to_s.upcase.ljust(4)} #{check.name}: #{check.message}"
57
+ end
58
+ )
59
+ lines.join("\n")
60
+ end
61
+ end
62
+
63
+ EXPECTED_COLUMNS = {
64
+ processes: %w[id kind hostname pid last_heartbeat_at shutdown_state],
65
+ instances: %w[
66
+ id actor_type actor_id state state_version next_message_sequence
67
+ activation_owner_id activation_token activation_expires_at
68
+ activation_generation
69
+ ],
70
+ messages: %w[
71
+ id instance_id message_kind arguments sequence attempt_count request_id
72
+ result error rejection completed_at rejected_at
73
+ ],
74
+ ready_messages: %w[id message_id instance_id sequence available_at],
75
+ claimed_messages: %w[
76
+ id message_id instance_id process_id activation_token
77
+ activation_generation claimed_at
78
+ ],
79
+ reminders: %w[id instance_id message_name next_run_at status],
80
+ effects: %w[id message_id instance_id effect_id status available_at],
81
+ broadcasts: %w[id message_id instance_id broadcast_id status available_at],
82
+ dead_letters: %w[id message_id instance_id actor_type actor_id attempts]
83
+ }.freeze
84
+
85
+ class ProbeActor < Actor
86
+ actor_type "solid_objects_doctor"
87
+
88
+ # @rbs (value: String) -> String
89
+ def ping(value:)
90
+ value
91
+ end
92
+ end
93
+
94
+ # @rbs @connection: untyped
95
+ # @rbs @configuration: Configuration
96
+
97
+ # @rbs (?connection: untyped, ?configuration: Configuration) -> void
98
+ def initialize(
99
+ connection: SolidObjects::Record.connection,
100
+ configuration: SolidObjects.configuration
101
+ )
102
+ @connection = connection
103
+ @configuration = configuration
104
+ end
105
+
106
+ # @rbs () -> Report
107
+ def call
108
+ configuration_check = check_configuration
109
+ schema_check = check_schema
110
+ checks = [
111
+ configuration_check,
112
+ schema_check,
113
+ check_authorization,
114
+ schema_check.failed? ? skipped_runtime : check_runtime,
115
+ ready_for_round_trip?(configuration_check, schema_check) ?
116
+ check_sync_round_trip :
117
+ skipped_round_trip
118
+ ]
119
+ Report.new(checks:)
120
+ end
121
+
122
+ private
123
+
124
+ attr_reader :connection, :configuration
125
+
126
+ # @rbs () -> Check
127
+ def check_configuration
128
+ configuration.validate!
129
+ pass(:configuration, "configuration is valid")
130
+ rescue => error
131
+ fail_check(:configuration, "#{error.class}: #{error.message}")
132
+ end
133
+
134
+ # @rbs () -> Check
135
+ def check_schema
136
+ missing_tables = expected_table_names - connection.data_sources
137
+ unless missing_tables.empty?
138
+ return fail_check(:schema, "missing tables: #{missing_tables.join(", ")}")
139
+ end
140
+
141
+ missing_columns = EXPECTED_COLUMNS.each_with_object([]) do |(name, expected), missing|
142
+ table_name = SolidObjects.table_name(name)
143
+ actual = connection.columns(table_name).map(&:name)
144
+ (expected - actual).each { |column| missing << "#{table_name}.#{column}" }
145
+ end
146
+ unless missing_columns.empty?
147
+ return fail_check(:schema, "missing columns: #{missing_columns.join(", ")}")
148
+ end
149
+
150
+ pass(:schema, "schema matches the #{SolidObjects::VERSION} runtime")
151
+ rescue => error
152
+ fail_check(:schema, "#{error.class}: #{error.message}")
153
+ end
154
+
155
+ # @rbs () -> Check
156
+ def check_authorization
157
+ outcomes = policy_probes.to_h do |name, arguments|
158
+ outcome = configuration.public_send(name).call(**arguments) ? :allow : :deny
159
+ [ name, outcome ]
160
+ rescue
161
+ [ name, :unknown ]
162
+ end
163
+ allowed = outcomes.select { |_, outcome| outcome == :allow }.keys
164
+ unknown = outcomes.select { |_, outcome| outcome == :unknown }.keys
165
+
166
+ if allowed.empty? && unknown.empty?
167
+ return warn_check(
168
+ :authorization,
169
+ "all five policies denied a neutral context; review the generated initializer before use"
170
+ )
171
+ end
172
+ risky = allowed & %i[
173
+ authorize_destroy
174
+ authorize_subscription
175
+ authorize_administration
176
+ ]
177
+ unless risky.empty?
178
+ return warn_check(
179
+ :authorization,
180
+ "sensitive policies allowed a neutral context: #{risky.join(", ")}"
181
+ )
182
+ end
183
+ unless unknown.empty?
184
+ return warn_check(
185
+ :authorization,
186
+ "#{allowed.length} of 5 policies allowed a neutral context; " \
187
+ "#{unknown.join(", ")} could not evaluate without application context"
188
+ )
189
+ end
190
+
191
+ pass(:authorization, "#{allowed.length} of 5 policies allowed a neutral context")
192
+ end
193
+
194
+ # @rbs () -> Check
195
+ def check_runtime
196
+ cutoff = SolidObjects.database_adapter.database_now -
197
+ configuration.process_alive_threshold
198
+ counts = Process
199
+ .where(shutdown_state: "running", last_heartbeat_at: cutoff..)
200
+ .group(:kind)
201
+ .count
202
+ if counts.empty?
203
+ return info(
204
+ :runtime,
205
+ "no live runtime roles; workerless synchronous calls are available, asynchronous features are not"
206
+ )
207
+ end
208
+
209
+ summary = counts.sort.map { |kind, count| "#{kind}=#{count}" }.join(", ")
210
+ pass(:runtime, "live runtime roles: #{summary}")
211
+ rescue => error
212
+ fail_check(:runtime, "#{error.class}: #{error.message}")
213
+ end
214
+
215
+ # @rbs () -> Check
216
+ def check_sync_round_trip
217
+ actor_id = SecureRandom.uuid
218
+ value = SecureRandom.hex(8)
219
+ process_registry = SolidObjects.caller_process.process_registry
220
+ reference = ProbeActor.ref(actor_id)
221
+ message_reference = Mailbox.new.enqueue(
222
+ reference,
223
+ :ping,
224
+ { value: },
225
+ kind: "sync"
226
+ )
227
+ result = SynchronousInvocation.new.call(message_reference, timeout: 5.seconds)
228
+ raise Error, "unexpected round-trip result" unless result == value
229
+
230
+ pass(:sync_round_trip, "durable synchronous actor call completed without a worker")
231
+ rescue => error
232
+ fail_check(:sync_round_trip, "#{error.class}: #{error.message}")
233
+ ensure
234
+ Instance.where(actor_type: ProbeActor.actor_type, actor_id:).delete_all if actor_id
235
+ process_registry&.stop
236
+ process_registry&.process_record&.delete
237
+ end
238
+
239
+ # @rbs (Check, Check) -> bool
240
+ def ready_for_round_trip?(configuration_check, schema_check)
241
+ !configuration_check.failed? && !schema_check.failed?
242
+ end
243
+
244
+ # @rbs () -> Check
245
+ def skipped_runtime
246
+ skip(:runtime, "schema check failed")
247
+ end
248
+
249
+ # @rbs () -> Check
250
+ def skipped_round_trip
251
+ skip(:sync_round_trip, "configuration or schema check failed")
252
+ end
253
+
254
+ # @rbs () -> Hash[Symbol, Hash[Symbol, untyped]]
255
+ def policy_probes
256
+ actor_arguments = {
257
+ actor_type: ProbeActor.actor_type,
258
+ actor_id: "doctor",
259
+ authorization_context: nil
260
+ }
261
+ {
262
+ authorize_message: actor_arguments.merge(
263
+ message_name: "ping",
264
+ arguments: { "value" => "doctor" }
265
+ ),
266
+ authorize_query: actor_arguments.merge(
267
+ message_name: "value",
268
+ arguments: {}
269
+ ),
270
+ authorize_destroy: actor_arguments,
271
+ authorize_subscription: actor_arguments,
272
+ authorize_administration: {
273
+ action: "doctor",
274
+ resource: "runtime",
275
+ resource_id: nil,
276
+ authorization_context: nil
277
+ }
278
+ }
279
+ end
280
+
281
+ # @rbs () -> Array[String]
282
+ def expected_table_names
283
+ EXPECTED_COLUMNS.keys.map { |name| SolidObjects.table_name(name) }
284
+ end
285
+
286
+ # @rbs (Symbol, String) -> Check
287
+ def pass(name, message)
288
+ Check.new(name:, status: :pass, message:)
289
+ end
290
+
291
+ # @rbs (Symbol, String) -> Check
292
+ def info(name, message)
293
+ Check.new(name:, status: :info, message:)
294
+ end
295
+
296
+ # @rbs (Symbol, String) -> Check
297
+ def warn_check(name, message)
298
+ Check.new(name:, status: :warn, message:)
299
+ end
300
+
301
+ # @rbs (Symbol, String) -> Check
302
+ def fail_check(name, message)
303
+ Check.new(name:, status: :fail, message:)
304
+ end
305
+
306
+ # @rbs (Symbol, String) -> Check
307
+ def skip(name, message)
308
+ Check.new(name:, status: :skip, message:)
309
+ end
310
+ end
311
+ end