canvas-lms/gems/dynamic_settings/lib/dynamic_settings.rb

165 lines
5.7 KiB
Ruby

# frozen_string_literal: true
#
# Copyright (C) 2021 - 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 "logger"
require "active_support"
require "active_support/core_ext"
require "config_file"
require "diplomat"
require "dynamic_settings/circuit_breaker"
require "dynamic_settings/memory_cache"
require "dynamic_settings/null_request_cache"
require "dynamic_settings/fallback_proxy"
require "dynamic_settings/prefix_proxy"
module DynamicSettings
CONSUL_READ_OPTIONS = %i[recurse stale].freeze
KV_NAMESPACE = "config/canvas"
CACHE_KEY_PREFIX = "dynamic_settings/"
class << self
attr_accessor :environment
attr_reader :fallback_data, :use_consul, :config
attr_writer :fallback_recovery_lambda, :retry_lambda, :cache, :request_cache, :logger
def config=(conf_hash)
@config = conf_hash
if conf_hash.present?
Diplomat.configure do |config|
need_ssl = conf_hash.fetch("ssl", true)
config.url = "#{need_ssl ? "https://" : "http://"}#{conf_hash.fetch("host")}:#{conf_hash.fetch("port")}"
config.acl_token = conf_hash.fetch("acl_token", nil)
options = { request: {} }
options[:request][:open_timeout] = conf_hash["connect_timeout"] if conf_hash["connect_timeout"]
options[:request][:timeout] = conf_hash["timeout"] if conf_hash["timeout"]
config.options = options
end
@environment = conf_hash["environment"]
@use_consul = true
@data_center = conf_hash.fetch("global_dc", nil)
@default_service = conf_hash.fetch("service", :canvas)
@cache = conf_hash.fetch("cache", ::DynamicSettings::MemoryCache.new)
@request_cache = conf_hash.fetch("request_cache", ::DynamicSettings::NullRequestCache.new)
@fallback_recovery_lambda = conf_hash.fetch("fallback_recovery_lambda", nil)
@retry_lambda = conf_hash.fetch("retry_lambda", nil)
@logger = conf_hash.fetch("logger", nil)
else
@environment = nil
@use_consul = false
@default_service = :canvas
@cache = nil
@request_cache = nil
end
end
def logger
@logger ||= Rails.logger
end
def cache
@cache ||= ::DynamicSettings::MemoryCache.new
end
def request_cache
@request_cache ||= ::DynamicSettings::NullRequestCache.new
end
def on_fallback_recovery(exception)
@fallback_recovery_lambda&.call(exception)
end
def on_retry(exception)
@retry_lambda&.call(exception)
end
def on_reload!
@root_fallback_proxy = nil
reset_cache!
end
# Set the fallback data to use in leiu of Consul
#
# This isn't really meant for use in production, but as a convenience for
# development where most won't want to run a consul agent/server.
def fallback_data=(value)
@fallback_data = value
@root_fallback_proxy = if @fallback_data
FallbackProxy.new(@fallback_data.with_indifferent_access)
end
end
def root_fallback_proxy
@root_fallback_proxy ||= FallbackProxy.new(ConfigFile.load("dynamic_settings").dup)
end
# Build an object used to interacting with consul for the given
# keyspace prefix.
#
# If using fallback data for values it is queried by the returned object
# instead of a Consul agent/server. The decision between using fallback
# data or consul is driven by whether or not consul is configured.
#
# @param prefix [String] The portion to extend the base prefix with
# (base prefix: 'config/canvas/<environment>')
# @param tree [String] Which tree to use (config, private, store)
# @param service [String] The service name to use (i.e. who owns the configuration). Defaults to canvas
# @param cluster [String] An optional cluster to override region or global settings
# @param default_ttl [ActiveSupport::Duration] How long to retain cached
# values
# @param data_center [String] location of the data_center the proxy is pointing to
def find(prefix = nil,
tree: :config,
service: nil,
cluster: nil,
default_ttl: PrefixProxy::DEFAULT_TTL,
data_center: nil)
service ||= @default_service || :canvas
if use_consul
PrefixProxy.new(
prefix,
tree:,
service:,
environment: @environment,
cluster:,
default_ttl:,
data_center: data_center || @data_center,
query_logging: @config.fetch("query_logging", true),
retry_limit: @config.fetch("retry_limit", 1),
retry_base: @config.fetch("retry_base", 1.4),
circuit_breaker: @config.fetch("circuit_breaker", nil)
)
else
proxy = root_fallback_proxy
proxy = proxy.for_prefix(tree)
proxy = proxy.for_prefix(service)
proxy = proxy.for_prefix(prefix) if prefix
proxy
end
end
alias_method :kv_proxy, :find
def reset_cache!
# Only Redis glob strings are supported! see: https://redis.io/commands/keys/
cache.delete_matched("#{CACHE_KEY_PREFIX}*")
end
end
end