My app is connected to some third-party APIs.
I have several APIconnector module-singletons that are initialized only once at application start (initialized means the client is instanciated once with the credentials retrieved from secrets)
When I reload!
the application in my console, I am losing those services and I have to exit and restart the console from scratch.
Basically all my connectors include a ServiceConnector module like this one
module ServiceConnector
extend ActiveSupport::Concern
included do
@activated = false
@activation_attempt = false
@client = nil
attr_reader :client, :activated
def self.client
@client ||= service_client
end
def self.service_name
name.gsub('Connector', '')
end
def self.activate
@activation_attempt = true
if credentials_present?
@client = service_client
@activated = true
end
end
Here is an example of a service implementation
module My Connector
include ServiceConnector
@app_id = nil
@api_key = nil
def self.set_credentials(id, key)
@app_id = id
@api_key = key
end
def self.credentials_present?
@app_id.present? and @api_key.present?
end
def self.service_client
::SomeAPI::Client.new(
app_id: @app_id,
api_key: @api_key
)
end
end
I use this pattern that lets me reuse those services outside Rails (eg Capistrano, worker without Rails, etc.). In Rails I would load the services that way
# config/initializers/my_service.rb
if my_service_should_be_activated?
my_service.set_credentials(
Rails.application.secrets.my_service_app_id,
Rails.application.secrets.my_service_app_key
)
my_service.activate
end
I guess that executing reload!
seems to clear all my instance variables including @client
, @app_id
, @api_key
.
Is it possible to add code to be executed after a reload!
? In my case I would need to re-run the initializer. Or is there a way to make sure the instance variables of my services are not cleared with a reload! ?
So I have come up with a solution involving two initializers
First, a 000_initializer that will report which secrets were loaded successfully
module SecretChecker
module_function
# Return true if all secrets are present
def secrets?(secret_list, under:)
secret_root = Rails.application.secrets
if under
if under.is_a?(Array)
secret_root = secret_root.public_send(under.shift)&.dig(*under.map(&:to_s))
else
secret_root = secret_root.public_send(under)
end
secret_list.map do |secret|
secret_root&.dig(secret.to_s).present?
end
else
secret_list.map do |secret|
secret_root&.public_send(secret.to_s).present?
end
end.reduce(:&)
end
def check_secrets(theme, secret_list, under: nil)
return if secrets?(secret_list, under: under)
message = "WARNING - Missing secrets for #{theme} - #{yield}"
puts message and Rails.logger.warn(message)
end
end
SecretChecker.check_secrets('Slack', %i[martine], under: [:slack, :webhooks]) do
'Slack Notifications will not work'
end
SecretChecker.check_secrets('MongoDB', %i[user password], under: :mongodb) do
'No Database Connection if auth is activated'
end
Then, a module to reload the services with ActiveSupport::Reloader (an example featuring Slack)
# config/initializers/0_service_activation.rb
module ServiceActivation
def self.with_reload
ActiveSupport::Reloader.to_prepare do
yield
end
end
module Slack
def self.service
::SlackConnector
end
def self.should_be_activated?
Rails.env.production? ||
Rails.env.staging? ||
(Rails.env.development? && ENV['ENABLE_SLACK'] == 'true')
end
def self.activate
slack = service
slack.webhook = Rails.application.secrets.slack&.dig('webhooks', 'my_webhook')
ENV['SLACK_INTERCEPT_CHANNEL'].try do |channel|
slack.intercept_channel = channel if channel.present?
end
slack.activate
slack
end
end
end
[
...,
ServiceActivation::Slack
] .each do |activator|
ServiceActivation.with_reload do
activator.activate if activator.should_be_activated?
activator.service.status_report
end
end
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.