Class: SolidObjects::Doctor

Inherits:
Object
  • Object
show all
Defined in:
lib/solid_objects/doctor.rb,
sig/generated/lib/solid_objects/doctor.rbs

Defined Under Namespace

Classes: Check, ProbeActor, Report

Constant Summary collapse

EXPECTED_COLUMNS =

Returns:

  • (Object)
{
  processes: %w[id kind hostname pid last_heartbeat_at shutdown_state],
  instances: %w[
    id actor_type actor_id state state_version next_message_sequence
    activation_owner_id activation_token activation_expires_at
    activation_generation
  ],
  messages: %w[
    id instance_id message_kind arguments sequence attempt_count request_id
    result error rejection completed_at rejected_at
  ],
  ready_messages: %w[id message_id instance_id sequence available_at],
  claimed_messages: %w[
    id message_id instance_id process_id activation_token
    activation_generation claimed_at
  ],
  reminders: %w[id instance_id message_name next_run_at status],
  effects: %w[id message_id instance_id effect_id status available_at],
  broadcasts: %w[id message_id instance_id broadcast_id status available_at],
  dead_letters: %w[id message_id instance_id actor_type actor_id attempts]
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection: SolidObjects::Record.connection, configuration: SolidObjects.configuration) ⇒ Doctor

Returns a new instance of Doctor.

RBS:

  • (?connection: untyped, ?configuration: Configuration) -> void

Parameters:

  • connection: (Object) (defaults to: SolidObjects::Record.connection)
  • configuration: (Configuration) (defaults to: SolidObjects.configuration)


98
99
100
101
102
103
104
# File 'lib/solid_objects/doctor.rb', line 98

def initialize(
  connection: SolidObjects::Record.connection,
  configuration: SolidObjects.configuration
)
  @connection = connection
  @configuration = configuration
end

Instance Attribute Details

#configurationObject (readonly)

Returns the value of attribute configuration.

Returns:

  • (Object)


124
125
126
# File 'lib/solid_objects/doctor.rb', line 124

def configuration
  @configuration
end

#connectionObject (readonly)

Returns the value of attribute connection.

Returns:

  • (Object)


124
125
126
# File 'lib/solid_objects/doctor.rb', line 124

def connection
  @connection
end

Instance Method Details

#callReport

RBS:

  • () -> Report

Returns:



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/solid_objects/doctor.rb', line 107

def call
  configuration_check = check_configuration
  schema_check = check_schema
  checks = [
    configuration_check,
    schema_check,
    check_authorization,
    schema_check.failed? ? skipped_runtime : check_runtime,
    ready_for_round_trip?(configuration_check, schema_check) ?
      check_sync_round_trip :
      skipped_round_trip
  ]
  Report.new(checks:)
end

#check_authorizationCheck

RBS:

  • () -> Check

Returns:



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/solid_objects/doctor.rb', line 156

def check_authorization
  outcomes = policy_probes.to_h do |name, arguments|
    outcome = configuration.public_send(name).call(**arguments) ? :allow : :deny
    [ name, outcome ]
  rescue
    [ name, :unknown ]
  end
  allowed = outcomes.select { |_, outcome| outcome == :allow }.keys
  unknown = outcomes.select { |_, outcome| outcome == :unknown }.keys

  if allowed.empty? && unknown.empty?
    return warn_check(
      :authorization,
      "all five policies denied a neutral context; review the generated initializer before use"
    )
  end
  risky = allowed & %i[
    authorize_destroy
    authorize_subscription
    authorize_administration
  ]
  unless risky.empty?
    return warn_check(
      :authorization,
      "sensitive policies allowed a neutral context: #{risky.join(", ")}"
    )
  end
  unless unknown.empty?
    return warn_check(
      :authorization,
      "#{allowed.length} of 5 policies allowed a neutral context; " \
        "#{unknown.join(", ")} could not evaluate without application context"
    )
  end

  pass(:authorization, "#{allowed.length} of 5 policies allowed a neutral context")
end

#check_configurationCheck

RBS:

  • () -> Check

Returns:



127
128
129
130
131
132
# File 'lib/solid_objects/doctor.rb', line 127

def check_configuration
  configuration.validate!
  pass(:configuration, "configuration is valid")
rescue => error
  fail_check(:configuration, "#{error.class}: #{error.message}")
end

#check_runtimeCheck

RBS:

  • () -> Check

Returns:



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/solid_objects/doctor.rb', line 195

def check_runtime
  cutoff = SolidObjects.database_adapter.database_now -
    configuration.process_alive_threshold
  counts = Process
    .where(shutdown_state: "running", last_heartbeat_at: cutoff..)
    .group(:kind)
    .count
  if counts.empty?
    return info(
      :runtime,
      "no live runtime roles; workerless synchronous calls are available, asynchronous features are not"
    )
  end

  summary = counts.sort.map { |kind, count| "#{kind}=#{count}" }.join(", ")
  pass(:runtime, "live runtime roles: #{summary}")
rescue => error
  fail_check(:runtime, "#{error.class}: #{error.message}")
end

#check_schemaCheck

RBS:

  • () -> Check

Returns:



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/solid_objects/doctor.rb', line 135

def check_schema
  missing_tables = expected_table_names - connection.data_sources
  unless missing_tables.empty?
    return fail_check(:schema, "missing tables: #{missing_tables.join(", ")}")
  end

  missing_columns = EXPECTED_COLUMNS.each_with_object([]) do |(name, expected), missing|
    table_name = SolidObjects.table_name(name)
    actual = connection.columns(table_name).map(&:name)
    (expected - actual).each { |column| missing << "#{table_name}.#{column}" }
  end
  unless missing_columns.empty?
    return fail_check(:schema, "missing columns: #{missing_columns.join(", ")}")
  end

  pass(:schema, "schema matches the #{SolidObjects::VERSION} runtime")
rescue => error
  fail_check(:schema, "#{error.class}: #{error.message}")
end

#check_sync_round_tripCheck

RBS:

  • () -> Check

Returns:



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/solid_objects/doctor.rb', line 216

def check_sync_round_trip
  actor_id = SecureRandom.uuid
  value = SecureRandom.hex(8)
  process_registry = SolidObjects.caller_process.process_registry
  reference = ProbeActor.ref(actor_id)
  message_reference = Mailbox.new.enqueue(
    reference,
    :ping,
    { value: },
    kind: "sync"
  )
  result = SynchronousInvocation.new.call(message_reference, timeout: 5.seconds)
  raise Error, "unexpected round-trip result" unless result == value

  pass(:sync_round_trip, "durable synchronous actor call completed without a worker")
rescue => error
  fail_check(:sync_round_trip, "#{error.class}: #{error.message}")
ensure
  Instance.where(actor_type: ProbeActor.actor_type, actor_id:).delete_all if actor_id
  process_registry&.stop
  process_registry&.process_record&.delete
end

#expected_table_namesArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


282
283
284
# File 'lib/solid_objects/doctor.rb', line 282

def expected_table_names
  EXPECTED_COLUMNS.keys.map { |name| SolidObjects.table_name(name) }
end

#fail_check(name, message) ⇒ Check

RBS:

  • (Symbol, String) -> Check

Parameters:

  • (Symbol)
  • (String)

Returns:



302
303
304
# File 'lib/solid_objects/doctor.rb', line 302

def fail_check(name, message)
  Check.new(name:, status: :fail, message:)
end

#info(name, message) ⇒ Check

RBS:

  • (Symbol, String) -> Check

Parameters:

  • (Symbol)
  • (String)

Returns:



292
293
294
# File 'lib/solid_objects/doctor.rb', line 292

def info(name, message)
  Check.new(name:, status: :info, message:)
end

#pass(name, message) ⇒ Check

RBS:

  • (Symbol, String) -> Check

Parameters:

  • (Symbol)
  • (String)

Returns:



287
288
289
# File 'lib/solid_objects/doctor.rb', line 287

def pass(name, message)
  Check.new(name:, status: :pass, message:)
end

#policy_probesHash[Symbol, Hash[Symbol, untyped]]

RBS:

  • () -> Hash[Symbol, Hash[Symbol, untyped]]

Returns:

  • (Hash[Symbol, Hash[Symbol, untyped]])


255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/solid_objects/doctor.rb', line 255

def policy_probes
  actor_arguments = {
    actor_type: ProbeActor.actor_type,
    actor_id: "doctor",
    authorization_context: nil
  }
  {
    authorize_message: actor_arguments.merge(
      message_name: "ping",
      arguments: { "value" => "doctor" }
    ),
    authorize_query: actor_arguments.merge(
      message_name: "value",
      arguments: {}
    ),
    authorize_destroy: actor_arguments,
    authorize_subscription: actor_arguments,
    authorize_administration: {
      action: "doctor",
      resource: "runtime",
      resource_id: nil,
      authorization_context: nil
    }
  }
end

#ready_for_round_trip?(configuration_check, schema_check) ⇒ Boolean

RBS:

  • (Check, Check) -> bool

Parameters:

Returns:

  • (Boolean)


240
241
242
# File 'lib/solid_objects/doctor.rb', line 240

def ready_for_round_trip?(configuration_check, schema_check)
  !configuration_check.failed? && !schema_check.failed?
end

#skip(name, message) ⇒ Check

RBS:

  • (Symbol, String) -> Check

Parameters:

  • (Symbol)
  • (String)

Returns:



307
308
309
# File 'lib/solid_objects/doctor.rb', line 307

def skip(name, message)
  Check.new(name:, status: :skip, message:)
end

#skipped_round_tripCheck

RBS:

  • () -> Check

Returns:



250
251
252
# File 'lib/solid_objects/doctor.rb', line 250

def skipped_round_trip
  skip(:sync_round_trip, "configuration or schema check failed")
end

#skipped_runtimeCheck

RBS:

  • () -> Check

Returns:



245
246
247
# File 'lib/solid_objects/doctor.rb', line 245

def skipped_runtime
  skip(:runtime, "schema check failed")
end

#warn_check(name, message) ⇒ Check

RBS:

  • (Symbol, String) -> Check

Parameters:

  • (Symbol)
  • (String)

Returns:



297
298
299
# File 'lib/solid_objects/doctor.rb', line 297

def warn_check(name, message)
  Check.new(name:, status: :warn, message:)
end