canvas-lms/lib/canvas/dynamic_settings.rb

145 lines
4.7 KiB
Ruby

#
# Copyright (C) 2015 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
require 'imperium'
module Canvas
class DynamicSettings
class Error < StandardError; end
class ConsulError < Error; end
CONSUL_READ_OPTIONS = %i{recurse stale}.freeze
KV_NAMESPACE = "config/canvas".freeze
class << self
attr_accessor :config, :cache, :environment, :fallback_data
def config=(conf_hash)
@config = conf_hash
if conf_hash.present?
Imperium.configure do |config|
config.ssl = conf_hash.fetch('ssl', true)
config.host = conf_hash.fetch('host')
config.port = conf_hash.fetch('port')
config.token = conf_hash.fetch('acl_token', nil)
config.connect_timeout = conf_hash['connect_timeout'] if conf_hash['connect_timeout']
config.send_timeout = conf_hash['send_timeout'] if conf_hash['send_timeout']
config.receive_timeout = conf_hash['receive_timeout'] if conf_hash['receive_timeout']
end
@environment = conf_hash['environment']
init_values(conf_hash.fetch("init_values", {}))
init_values(conf_hash.fetch("init_values_without_env", {}), use_env: false)
end
end
def find(key, use_env: true)
if config.nil?
return fallback_data.fetch(key) if fallback_data.present?
raise(ConsulError, "Unable to contact consul without config")
else
store_get(key, use_env: use_env)
end
end
# settings found this way with nil expiry will be cached in the process
# the first time they're asked for, and then can only be cleared with a SIGHUP
# or restart of the process. Make sure that's the behavior you want before
# you use this method, or specify a timeout
def from_cache(key, expires_in: nil, use_env: true)
reset_cache! if cache.nil?
cached_value = get_from_cache(key, expires_in)
return cached_value if cached_value.present?
# cache miss or timeout
value = self.find(key, use_env: use_env)
set_in_cache(key, value)
value
end
def kv_client
Imperium::KV.default_client
end
def reset_cache!(hard: false)
@cache = {}
@strategic_reserve = {} if hard
end
private
def get_from_cache(key, timeout)
return nil unless cache.key?(key)
cache_entry = cache[key]
return cache_entry[:value] if timeout.nil?
threshold = (Time.zone.now - timeout).to_i
return cache_entry[:value] if cache_entry[:timestamp] > threshold
end
def set_in_cache(key, value)
cache[key] = {value: value, timestamp: Time.zone.now.to_i}
end
def init_values(hash, use_env: true)
hash.each do |parent_key, settings|
settings.each do |child_key, value|
store_put("#{parent_key}/#{child_key}", value, use_env: use_env)
end
end
rescue Imperium::TimeoutError
return false
end
def store_get(key, use_env: true)
# store all values that we get here to
# kind-of recover in case of big failure
@strategic_reserve ||= {}
parent_key = add_prefix_to(key, use_env)
consul_response = kv_client.get(parent_key, *CONSUL_READ_OPTIONS)
consul_value = consul_response.values
@strategic_reserve[key] = consul_value
consul_value
rescue Imperium::TimeoutError => exception
if @strategic_reserve.key?(key)
# we have an old value for this key, log the error but recover
Canvas::Errors.capture_exception(:consul, exception)
return @strategic_reserve[key]
else
# didn't have an old value cached, raise the error
raise
end
end
def store_put(key, value, use_env: true)
full_key = add_prefix_to(key, use_env)
kv_client.put(full_key, value)
end
def add_prefix_to(key, use_env)
if use_env && environment
"#{KV_NAMESPACE}/#{environment}/#{key}"
else
"#{KV_NAMESPACE}/#{key}"
end
end
end
end
end