Move MathMan to being configured by Consul
Fixes: CNVS-35833 There is a lot more than just moving to Consul going on here. The whole PrefixProxy business wouldn't be required for this change, but it will be really useful as we move to adding cluster awareness. Test Plan: - Have MathMan running - Update config/consul.yml to enable use_for_svg and use_for_mml under the math-man init values key - Start Canvas - Build an equation with the rich content editor - The equation should be rendered as usual. Change-Id: I650527ebaecb6224c6ee6ba26346d27dee33b9d7 Reviewed-on: https://gerrit.instructure.com/111543 QA-Review: Tucker McKnight <tmcknight@instructure.com> Tested-by: Jenkins Reviewed-by: Brent Burgoyne <bburgoyne@instructure.com> Product-Review: Tyler Pickett <tpickett@instructure.com>
This commit is contained in:
parent
8700cffa99
commit
823cda8924
|
@ -1,38 +0,0 @@
|
|||
<%
|
||||
# Copyright (C) 2016 - 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/>.
|
||||
%>
|
||||
|
||||
<%= fields_for :settings, OpenObject.new(settings) do |f| %>
|
||||
<table style="width: 500px;" class="formtable">
|
||||
<tr>
|
||||
<td><%= f.blabel :base_url, t('Base url for MathMan service') %></td>
|
||||
<td><%= f.text_field :base_url %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<%= f.blabel :use_for_svg, t('Use MathMan to convert LaTeX to SVG?') %>
|
||||
</td>
|
||||
<td><%= f.check_box :use_for_svg %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<%= f.blabel :use_for_mml, t('Use MathMan to convert LaTeX to MathML?') %>
|
||||
</td>
|
||||
<td><%= f.check_box :use_for_mml %></td>
|
||||
</tr>
|
||||
</table>
|
||||
<% end %>
|
|
@ -41,3 +41,8 @@ development:
|
|||
live-events:
|
||||
aws_endpoint: http://localhost:4567 # assumes kinesalite is running locally
|
||||
kinesis_stream_name: live-events
|
||||
init_values:
|
||||
math-man:
|
||||
base_url: 'http://mathman.docker'
|
||||
use_for_svg: 'false'
|
||||
use_for_mml: 'false'
|
||||
|
|
|
@ -16,3 +16,10 @@ development:
|
|||
canvas:
|
||||
encryption-secret: "astringthatisactually32byteslong"
|
||||
signing-secret: "astringthatisactually32byteslong"
|
||||
live-events:
|
||||
aws_endpoint: http://kinesis.canvaslms.docker
|
||||
kinesis_stream_name: live-events
|
||||
math-man:
|
||||
base_url: 'http://mathman.docker'
|
||||
use_for_svg: 'false'
|
||||
use_for_mml: 'false'
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
# host: 10.11.12.13
|
||||
# port: 8500
|
||||
# ssl: true
|
||||
# environment: prod
|
||||
# acl_token: "xxxxxxxx-yyyy-zzzz-1111-222222222222"
|
||||
# test:
|
||||
# host: consul
|
||||
# port: 8500
|
||||
# ssl: false
|
||||
# environment: test
|
||||
# init_values:
|
||||
# canvas:
|
||||
# signing-secret: astringthatisactually32byteslong
|
||||
|
@ -24,7 +26,8 @@
|
|||
# host: consul
|
||||
# port: 8500
|
||||
# ssl: false
|
||||
# init_values:
|
||||
# environment: "dev"
|
||||
# init_values_witnout_env:
|
||||
# canvas:
|
||||
# signing-secret: astringthatisactually32byteslong
|
||||
# encryption-secret: astringthatisactually32byteslong
|
||||
|
@ -38,5 +41,10 @@
|
|||
# app-host: http://les.docker
|
||||
# sad-panda: null
|
||||
# live-events:
|
||||
# aws_endpoint: http://localhost:4567 # assumes kinesalite is running locally
|
||||
# aws_endpoint: http://kinesis.canvaslms.docker
|
||||
# kinesis_stream_name: live-events
|
||||
# init_values:
|
||||
# math-man:
|
||||
# base_url: 'http://mathman.docker'
|
||||
# use_for_svg: 'false'
|
||||
# use_for_mml: 'false'
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
# 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_dependency 'canvas/dynamic_settings/cache'
|
||||
require_dependency 'canvas/dynamic_settings/fallback_proxy'
|
||||
require_dependency 'canvas/dynamic_settings/prefix_proxy'
|
||||
require 'imperium'
|
||||
|
||||
module Canvas
|
||||
|
@ -22,12 +25,14 @@ module Canvas
|
|||
|
||||
class Error < StandardError; end
|
||||
class ConsulError < Error; end
|
||||
class NoFallbackError < Error; end
|
||||
|
||||
CONSUL_READ_OPTIONS = %i{recurse stale}.freeze
|
||||
KV_NAMESPACE = "config/canvas".freeze
|
||||
|
||||
class << self
|
||||
attr_accessor :config, :cache, :environment, :fallback_data
|
||||
attr_accessor :config, :environment
|
||||
attr_reader :base_prefix_proxy, :fallback_data
|
||||
|
||||
def config=(conf_hash)
|
||||
@config = conf_hash
|
||||
|
@ -47,9 +52,26 @@ module Canvas
|
|||
|
||||
init_values(conf_hash.fetch("init_values", {}))
|
||||
init_values(conf_hash.fetch("init_values_without_env", {}), use_env: false)
|
||||
|
||||
@base_prefix_proxy = DynamicSettings::PrefixProxy.new(
|
||||
[KV_NAMESPACE, @environment.presence].compact.join('/'),
|
||||
kv_client: Imperium::KV.default_client
|
||||
)
|
||||
else
|
||||
@environment = @base_prefix_proxy = nil
|
||||
end
|
||||
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&.with_indifferent_access
|
||||
end
|
||||
|
||||
# This is deprecated, use for_prefix to get a client that will fetch your
|
||||
# values for you and squawks like a hash so you don't have to change much.
|
||||
def find(key, use_env: true)
|
||||
if config.nil?
|
||||
return fallback_data.fetch(key) if fallback_data.present?
|
||||
|
@ -59,18 +81,39 @@ module Canvas
|
|||
end
|
||||
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 default_ttl [ActiveSupport::Duration] How long to retain cached
|
||||
# values
|
||||
def for_prefix(prefix, default_ttl: DynamicSettings::PrefixProxy::DEFAULT_TTL)
|
||||
if @base_prefix_proxy
|
||||
@base_prefix_proxy.for_prefix(prefix, default_ttl: default_ttl)
|
||||
elsif @fallback_data.present?
|
||||
DynamicSettings::FallbackProxy.new(@fallback_data[prefix])
|
||||
else
|
||||
raise NoFallbackError, 'DynamicSettings.fallback_data is not set and'\
|
||||
' consul is not configured, unable to supply configuration values.'
|
||||
end
|
||||
end
|
||||
|
||||
# This is deprecated, use for_prefix to get a client that will fetch your
|
||||
# values for you and squawks like a hash so you don't have to change much.
|
||||
#
|
||||
# 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
|
||||
Cache.fetch(key, ttl: expires_in) do
|
||||
self.find(key, use_env: use_env)
|
||||
end
|
||||
end
|
||||
|
||||
def kv_client
|
||||
|
@ -78,24 +121,12 @@ module Canvas
|
|||
end
|
||||
|
||||
def reset_cache!(hard: false)
|
||||
@cache = {}
|
||||
Cache.reset!
|
||||
@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|
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
# Copyright (C) 2017 - 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/>.
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
# A cache for values fetched from consul
|
||||
module Cache
|
||||
Value = Struct.new(:value, :expiration_time) do
|
||||
def expired?
|
||||
return false unless expiration_time
|
||||
Time.zone.now >= expiration_time
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: consider making this an L{R,f}U cache instead of a boundless one
|
||||
@store = {}
|
||||
|
||||
class << self
|
||||
attr_reader :store
|
||||
end
|
||||
|
||||
# Get the cached value, if any, from the store regardless of expiration
|
||||
#
|
||||
# This is really only meant as an emergency fallback in the event that
|
||||
# Consul can't be reached, not for normal operation.
|
||||
def self.fallback_fetch(key)
|
||||
@store[key]&.value
|
||||
end
|
||||
|
||||
# Return the cached value for `key` or execute the supplied block to get it
|
||||
#
|
||||
# @param key [String] The key to cache the result under
|
||||
# @param ttl [ActiveSupport::Duration] The length of time this key should
|
||||
# be cached for, pass nil for no expiration.
|
||||
def self.fetch(key, ttl: nil)
|
||||
stored = @store[key]
|
||||
if stored && !stored.expired?
|
||||
stored.expiration_time = ttl&.from_now if stored.expiration_time.nil?
|
||||
stored.value
|
||||
else
|
||||
yield.tap do |value|
|
||||
insert(key, value, ttl: ttl) unless value.respond_to?(:not_found?) && value.not_found?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Clear the cache store
|
||||
def self.reset!
|
||||
@store = {}
|
||||
end
|
||||
|
||||
# Insert the supplied value into the cache using the supplied key
|
||||
#
|
||||
# @param key [String] The cache key to use
|
||||
# @param value [Object] The value to store
|
||||
# @param ttl [ActiveSupport::Duration] The length of time this key should
|
||||
# be cached for, pass nil for no expiration.
|
||||
def self.insert(key, value, ttl: nil)
|
||||
@store[key] = Value.new(value, ttl&.from_now)
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright (C) 2017 - 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/>.
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
class FallbackProxy
|
||||
attr_reader :data
|
||||
|
||||
def initialize(data = nil)
|
||||
@data = (data || {}).with_indifferent_access
|
||||
end
|
||||
|
||||
def fetch(key, **_)
|
||||
@data[key]
|
||||
end
|
||||
alias [] fetch
|
||||
|
||||
# TODO: Make this return something API compatible with
|
||||
# Imperium::KVGETResponse once we're actually using Consul's metadata
|
||||
def fetch_object(_, **_)
|
||||
raise NotImplementedError, "Fetching full metadata objects from fallback data isn't implemented yet."
|
||||
end
|
||||
|
||||
def for_prefix(prefix_extension, **_)
|
||||
self.class.new(@data[prefix_extension])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright (C) 2017 - 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/>.
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
# A class for reading values from Consul
|
||||
#
|
||||
# @attr prefix [String] The prefix to be prepended to keys for querying.
|
||||
class PrefixProxy
|
||||
CONSUL_READ_OPTIONS = %i{stale}.freeze
|
||||
private_constant :CONSUL_READ_OPTIONS
|
||||
|
||||
DEFAULT_TTL = 5.minutes
|
||||
# The TTL for cached values if none is specified in the constructor
|
||||
|
||||
attr_reader :prefix
|
||||
|
||||
# Build a new prefix proxy
|
||||
#
|
||||
# @param prefix [String] The prefix to be prepended to keys for querying.
|
||||
# @param default_ttl [ActiveSupport::Duration] The TTL to use for cached
|
||||
# values when not specified to the fetch methods.
|
||||
# @param kv_client [Imperium::KV] The client to use for connecting to
|
||||
# Consul, defaults to Imperium::KV.default_client
|
||||
def initialize(prefix, default_ttl: DEFAULT_TTL, kv_client: Imperium::KV.default_client)
|
||||
@prefix = prefix
|
||||
@default_ttl = default_ttl
|
||||
@kv_client = kv_client
|
||||
end
|
||||
|
||||
# Fetch the value at the requested key using the prefix passed to the
|
||||
# initializer.
|
||||
#
|
||||
# This method is intended to retreive a single key from the keyspace and
|
||||
# will not work for getting multiple values in a hash from the store. If
|
||||
# you need to access values nested deeper in the keyspace use #for_prefix
|
||||
# to move deeper in the nesting.
|
||||
#
|
||||
# @param key [String, Symbol] The key to fetch
|
||||
# @param ttl [ActiveSupport::Duration] The TTL for the value in the cache,
|
||||
# defaults to value supplied to the constructor.
|
||||
# @return [String]
|
||||
# @return [nil] When no value was found
|
||||
def fetch(key, ttl: @default_ttl)
|
||||
fetch_object(key, ttl: ttl)&.values
|
||||
end
|
||||
alias [] fetch
|
||||
|
||||
# Fetch the full object at the specified key including all metadata
|
||||
#
|
||||
# This method is intended to retreive a single key from the keyspace and
|
||||
# will not work for getting multiple values in a hash from the store. If
|
||||
# you need to access values nested deeper in the keyspace use #for_prefix
|
||||
# to move deeper in the nesting.
|
||||
#
|
||||
# @param key [String, Symbol] The key to fetch
|
||||
# @param ttl [ActiveSupport::Duration] The TTL for the value in the cache,
|
||||
# defaults to value supplied to the constructor.
|
||||
# @return [Imperium::KVGETResponse]
|
||||
def fetch_object(key, ttl: @default_ttl)
|
||||
full_key = "#{@prefix}/#{key}"
|
||||
Cache.fetch(full_key, ttl: ttl) do
|
||||
@kv_client.get(full_key, *CONSUL_READ_OPTIONS)
|
||||
end
|
||||
rescue Imperium::TimeoutError => exception
|
||||
Cache.fallback_fetch(full_key).tap do |val|
|
||||
if val
|
||||
Canvas::Errors.capture_exception(:consul, exception)
|
||||
val
|
||||
else
|
||||
raise
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Extend the prefix from this instance returning a new one.
|
||||
#
|
||||
# @param prefix_extension [String]
|
||||
# @param default_ttl [ActiveSupport::Duration] The default TTL to use when
|
||||
# fetching keys from the extended keyspace, defaults to the same value as
|
||||
# the receiver
|
||||
# @return [ProxyPrefix]
|
||||
def for_prefix(prefix_extension, default_ttl: @default_ttl)
|
||||
self.class.new(
|
||||
"#{@prefix}/#{prefix_extension}",
|
||||
default_ttl: default_ttl,
|
||||
kv_client: @kv_client
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -140,19 +140,6 @@ Canvas::Plugin.register('kaltura', nil, {
|
|||
:settings_partial => 'plugins/kaltura_settings',
|
||||
:validator => 'KalturaValidator'
|
||||
})
|
||||
Canvas::Plugin.register('mathman', nil, {
|
||||
:name => lambda{ t :name, 'MathMan' },
|
||||
:description => lambda{ t :description, 'A simple microservice that converts LaTeX formulae to MathML and SVG'},
|
||||
:author => 'Instructure',
|
||||
:author_website => 'http://www.instructure.com',
|
||||
:version => '1.0.0',
|
||||
:settings_partial => 'plugins/mathman_settings',
|
||||
:validator => 'MathmanValidator',
|
||||
:settings => {
|
||||
use_for_svg: false,
|
||||
use_for_mml: false
|
||||
}
|
||||
})
|
||||
Canvas::Plugin.register('wimba', :web_conferencing, {
|
||||
:name => lambda{ t :name, "Wimba" },
|
||||
:description => lambda{ t :description, "Wimba web conferencing support" },
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2011 - 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/>.
|
||||
#
|
||||
|
||||
module Canvas::Plugins::Validators::MathmanValidator
|
||||
def self.validate(settings, plugin_setting)
|
||||
base_url = settings[:base_url]
|
||||
if base_url.match(URI.regexp)
|
||||
settings.to_hash.with_indifferent_access
|
||||
else
|
||||
plugin_setting.errors.add(:base, I18n.t('Must provide a valid url.'))
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,9 +15,13 @@
|
|||
# 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 "addressable/uri"
|
||||
|
||||
module MathMan
|
||||
def self.url_for(latex:, target:)
|
||||
"#{base_url}/#{target}?tex=#{latex}"
|
||||
uri = base_url.merge(path: "/#{target}")
|
||||
uri.query = "tex=#{latex}"
|
||||
uri.to_s
|
||||
end
|
||||
|
||||
def self.use_for_mml?
|
||||
|
@ -36,15 +40,24 @@ module MathMan
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
def self.base_url
|
||||
with_plugin_settings do |plugin_settings|
|
||||
plugin_settings[:base_url].sub(/\/$/, '')
|
||||
class << self
|
||||
private
|
||||
|
||||
def base_url
|
||||
with_plugin_settings do |plugin_settings|
|
||||
Addressable::URI.parse(plugin_settings[:base_url])
|
||||
end
|
||||
end
|
||||
|
||||
def with_plugin_settings
|
||||
plugin_settings = Canvas::DynamicSettings.for_prefix('math-man')
|
||||
yield plugin_settings
|
||||
rescue Canvas::DynamicSettings::NoFallbackError
|
||||
if Rails.env.production?
|
||||
raise
|
||||
else
|
||||
yield({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.with_plugin_settings
|
||||
plugin_settings = Canvas::Plugin.find(:mathman).settings
|
||||
yield plugin_settings
|
||||
end
|
||||
end
|
||||
|
|
|
@ -103,13 +103,10 @@ describe ConsulInitializer do
|
|||
describe "just from loading" do
|
||||
it "clears the DynamicSettings cache on reload" do
|
||||
Canvas::DynamicSettings.reset_cache!
|
||||
Canvas::DynamicSettings.cache["key"] = {
|
||||
value: "value",
|
||||
timestamp: Time.zone.now.to_i
|
||||
}
|
||||
Canvas::DynamicSettings::Cache.insert('key', 'value')
|
||||
expect(Canvas::DynamicSettings.from_cache("key")).to eq("value")
|
||||
Canvas::Reloader.reload!
|
||||
expect(Canvas::DynamicSettings.cache).to eq({})
|
||||
expect(Canvas::DynamicSettings::Cache.store).to eq({})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
#
|
||||
# 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 'spec_helper'
|
||||
require_dependency "canvas/dynamic_settings"
|
||||
require 'imperium/testing' # Not loaded by default
|
||||
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
RSpec.describe Cache do
|
||||
before do
|
||||
Cache.reset!
|
||||
end
|
||||
|
||||
describe '.fallback_fetch(key)' do
|
||||
it 'must return the stored value even if expired' do
|
||||
Cache.fetch('foobar', ttl: 2.seconds) do
|
||||
42
|
||||
end
|
||||
Timecop.travel(1.minute.from_now) do
|
||||
expect(Cache.fallback_fetch('foobar')).to eq 42
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fetch(key, ttl: nil)' do
|
||||
it 'must return the value returned by the supplied block' do
|
||||
val = Cache.fetch('foobar') do
|
||||
42
|
||||
end
|
||||
expect(val).to eq 42
|
||||
end
|
||||
|
||||
it 'must capture the return value from the block in the store' do
|
||||
Cache.fetch('foobar') do
|
||||
42
|
||||
end
|
||||
expect(Cache.store).to include 'foobar' => Cache::Value.new(42)
|
||||
end
|
||||
|
||||
it 'must return the stored value rather than calling the block again on future calls' do
|
||||
Cache.fetch('foobar') do
|
||||
42
|
||||
end
|
||||
val = Cache.fetch('foobar') do
|
||||
51
|
||||
end
|
||||
expect(val).to eq 42
|
||||
end
|
||||
|
||||
it 'must call the block again when the ttl has elapsed' do
|
||||
called = false
|
||||
Cache.fetch('foobar', ttl: 5.minutes) do
|
||||
42
|
||||
end
|
||||
|
||||
Timecop.travel(10.minutes.from_now) do
|
||||
Cache.fetch('foobar', ttl: 5.minutes) do
|
||||
called = true
|
||||
end
|
||||
|
||||
expect(called).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
it 'must not cache not found responses' do
|
||||
Cache.fetch('foobar', ttl: 5.minutes) do
|
||||
Imperium::Testing.kv_not_found_response
|
||||
end
|
||||
expect(Cache.store).to be_empty
|
||||
end
|
||||
|
||||
it 'must update the TTL on the cached value if it was previously nil' do
|
||||
Cache.insert('foo', 'bar')
|
||||
Cache.fetch('foo', ttl: 3.minutes)
|
||||
expect(Cache.store['foo'].expiration_time).to_not be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '.reset!' do
|
||||
it 'must clear the stored values' do
|
||||
described_class.store['foo/bar'] = 'value'
|
||||
described_class.reset!
|
||||
expect(described_class.store).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
# Copyright (C) 2017 - 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 'spec_helper'
|
||||
require_dependency "canvas/dynamic_settings"
|
||||
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
RSpec.describe FallbackProxy do
|
||||
let(:fallback_data) do
|
||||
{
|
||||
foo: 'bar',
|
||||
baz: {
|
||||
qux: 42
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:proxy) { FallbackProxy.new(fallback_data) }
|
||||
|
||||
describe '#initalize' do
|
||||
it 'must store an empty hash when initialized with nil' do
|
||||
proxy = FallbackProxy.new(nil)
|
||||
expect(proxy.data).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch(key, ttl: nil)' do
|
||||
it 'must return the value from the data hash' do
|
||||
expect(proxy.fetch('foo')).to eq 'bar'
|
||||
end
|
||||
|
||||
it 'must return nil when the val' do
|
||||
expect(proxy.fetch('nx-key')).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_object(key, ttl: nil)' do
|
||||
it 'must return a thing that squawks like an Imperium::KVGETResponse'
|
||||
end
|
||||
|
||||
describe '#for_prefix(key, default_ttl: nil)' do
|
||||
it 'must return a new instance populated with the sub hash found at the specified key' do
|
||||
new_proxy = proxy.for_prefix('baz')
|
||||
expect(new_proxy.data).to eq({qux: 42}.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,116 @@
|
|||
# Copyright (C) 2017 - 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 'spec_helper'
|
||||
require_dependency "canvas/dynamic_settings"
|
||||
require 'imperium/testing' # Not loaded by default
|
||||
|
||||
|
||||
module Canvas
|
||||
class DynamicSettings
|
||||
RSpec.describe PrefixProxy do
|
||||
let(:client) { instance_double(Imperium::KV) }
|
||||
let(:proxy) { PrefixProxy.new('foo/bar', default_ttl: 3.minutes, kv_client: client) }
|
||||
|
||||
after(:each) do
|
||||
Cache.reset!
|
||||
end
|
||||
|
||||
describe '.fetch(key, ttl: @default_ttl)' do
|
||||
it 'must return nil when no value was found' do
|
||||
allow(client).to receive(:get)
|
||||
.and_return(
|
||||
Imperium::Testing.kv_not_found_response(options: [:stale])
|
||||
)
|
||||
expect(proxy.fetch('baz')).to be_nil
|
||||
end
|
||||
|
||||
it 'must return the value for the specified key' do
|
||||
allow(client).to receive(:get)
|
||||
.and_return(
|
||||
Imperium::Testing.kv_get_response(
|
||||
body: [
|
||||
{ Key: "foo/bar/baz", Value: 'qux'},
|
||||
],
|
||||
options: [:stale],
|
||||
)
|
||||
)
|
||||
expect(proxy.fetch('baz')).to eq 'qux'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'fetch_object(key, ttl: @default_ttl)' do
|
||||
it 'must fetch the value from consul using the prefix and supplied key' do
|
||||
expect(client).to receive(:get).with('foo/bar/baz', an_instance_of(Symbol))
|
||||
proxy.fetch_object('baz')
|
||||
end
|
||||
|
||||
it 'must use the dynamic settings cache for previously fetched values' do
|
||||
expect(Cache).to receive(:fetch).with('foo/bar/baz', ttl: 3.minutes)
|
||||
proxy.fetch_object('baz')
|
||||
end
|
||||
|
||||
it "must fall back to expired cached values when consul can't be contacted" do
|
||||
Cache.store['foo/bar/baz'] = Cache::Value.new('qux', 3.minutes.ago)
|
||||
expect(client).to receive(:get).and_raise(Imperium::TimeoutError)
|
||||
val = proxy.fetch_object('baz')
|
||||
expect(val).to eq 'qux'
|
||||
end
|
||||
|
||||
it "must log the connection failure when consul can't be contacted" do
|
||||
Cache.store['foo/bar/baz'] = Cache::Value.new('qux', 3.minutes.ago)
|
||||
expect(Canvas::Errors).to receive(:capture_exception).
|
||||
with(:consul, an_instance_of(Imperium::TimeoutError))
|
||||
allow(client).to receive(:get).and_raise(Imperium::TimeoutError)
|
||||
proxy.fetch_object('baz')
|
||||
end
|
||||
|
||||
it "must raise an exception when consul can't be reached and no previous value is found" do
|
||||
expect(client).to receive(:get).and_raise(Imperium::TimeoutError)
|
||||
expect { proxy.fetch_object('baz') }.to raise_error(Imperium::TimeoutError)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for_prefix(prefix_extension, default_ttl: @default_ttl)' do
|
||||
it 'must instantiate a new proxy with the supplied prefix extnsion appended to the current prefix' do
|
||||
new_proxy = proxy.for_prefix('baz')
|
||||
expect(new_proxy).to be_a PrefixProxy
|
||||
expect(new_proxy.prefix).to eq 'foo/bar/baz'
|
||||
end
|
||||
|
||||
it "must pass on the current instance's default ttl if not supplied" do
|
||||
proxy
|
||||
expect(PrefixProxy).to receive(:new)
|
||||
.with(an_instance_of(String), a_hash_including(default_ttl: 3.minutes))
|
||||
proxy.for_prefix('baz')
|
||||
end
|
||||
|
||||
it 'must pass on the supplied default ttl' do
|
||||
proxy
|
||||
expect(PrefixProxy).to receive(:new)
|
||||
.with(an_instance_of(String), a_hash_including(default_ttl: 5.minutes))
|
||||
proxy.for_prefix('baz', default_ttl: 5.minutes)
|
||||
end
|
||||
|
||||
it 'must pass on the kv client from the receiving proxy' do
|
||||
proxy
|
||||
expect(PrefixProxy).to receive(:new)
|
||||
.with(an_instance_of(String), a_hash_including(kv_client: client))
|
||||
proxy.for_prefix('baz')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -117,6 +117,27 @@ module Canvas
|
|||
end
|
||||
end
|
||||
|
||||
describe '.fallback_data =' do
|
||||
before(:each) do
|
||||
@original_fallback = DynamicSettings.fallback_data
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
DynamicSettings.fallback_data = @original_fallback
|
||||
end
|
||||
|
||||
it 'must convert the supplied hash to one with indifferent access' do
|
||||
DynamicSettings.fallback_data = {}
|
||||
expect(DynamicSettings.fallback_data).to be_a(ActiveSupport::HashWithIndifferentAccess)
|
||||
end
|
||||
|
||||
it 'must clear the fallback data when passed nil' do
|
||||
DynamicSettings.fallback_data = {}
|
||||
DynamicSettings.fallback_data = nil
|
||||
expect(DynamicSettings.fallback_data).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe ".find" do
|
||||
describe "with consul config" do
|
||||
# we don't need to interact with a real consul for unit tests
|
||||
|
@ -250,6 +271,38 @@ module Canvas
|
|||
end
|
||||
end
|
||||
|
||||
describe '.for_prefix' do
|
||||
before(:each) do
|
||||
@original_fallback = DynamicSettings.fallback_data
|
||||
end
|
||||
|
||||
after(:each) do
|
||||
DynamicSettings.config = nil
|
||||
DynamicSettings.fallback_data = @original_fallback
|
||||
end
|
||||
|
||||
it 'must return a PrefixProxy when consul is configured' do
|
||||
DynamicSettings.config = valid_config
|
||||
proxy = DynamicSettings.for_prefix('foo')
|
||||
expect(proxy).to be_a(DynamicSettings::PrefixProxy)
|
||||
end
|
||||
|
||||
it 'must raise an error when neither consul or fallback data have been configured' do
|
||||
DynamicSettings.config = nil
|
||||
DynamicSettings.fallback_data = nil
|
||||
expect { DynamicSettings.for_prefix('foo') }.to raise_error(
|
||||
DynamicSettings::NoFallbackError,
|
||||
/fallback_data is not set/
|
||||
)
|
||||
end
|
||||
|
||||
it 'must return a FallbackProxy when consul is not configured' do
|
||||
DynamicSettings.fallback_data = {'foo' => {bar: 'baz'}}
|
||||
proxy = DynamicSettings.for_prefix('foo')
|
||||
expect(proxy).to be_a(DynamicSettings::FallbackProxy)
|
||||
end
|
||||
end
|
||||
|
||||
describe ".from_cache" do
|
||||
before(:each){ DynamicSettings.config = valid_config } # just to be not nil
|
||||
after(:each){ DynamicSettings.reset_cache! }
|
||||
|
@ -323,16 +376,6 @@ module Canvas
|
|||
end
|
||||
end
|
||||
|
||||
it "accepts a timeout on a previously inifinity key" do
|
||||
stub_consul_with("rce.insops.net")
|
||||
value = DynamicSettings.from_cache(parent_key)
|
||||
Timecop.travel(Time.zone.now + 11.minutes) do
|
||||
stub_consul_with("CHANGED VALUE")
|
||||
value = DynamicSettings.from_cache(parent_key, expires_in: 10.minutes)
|
||||
expect(value["app-host"]).to eq("CHANGED VALUE")
|
||||
end
|
||||
end
|
||||
|
||||
context "using catastrophic cache fallback" do
|
||||
let!(:now) { Time.zone.now }
|
||||
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
#
|
||||
# Copyright (C) 2016 - 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 'spec_helper'
|
||||
|
||||
describe Canvas::Plugins::Validators::MathmanValidator do
|
||||
describe '.validate' do
|
||||
let(:plugin_setting) do
|
||||
PluginSetting.new(
|
||||
name: 'mathman',
|
||||
settings: PluginSetting.settings_for_plugin('mathman')
|
||||
)
|
||||
end
|
||||
let(:settings) do
|
||||
{
|
||||
base_url: 'http://mathman.docker'
|
||||
}
|
||||
end
|
||||
|
||||
subject(:validator) do
|
||||
Canvas::Plugins::Validators::MathmanValidator.validate(settings, plugin_setting)
|
||||
end
|
||||
|
||||
it 'should return provided settings when base_url is a valid url' do
|
||||
expect(validator).to eq settings.with_indifferent_access
|
||||
end
|
||||
|
||||
context 'when base_url is invalid' do
|
||||
let(:settings) do
|
||||
{
|
||||
base_url: 'wooper'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(validator).to be_falsey
|
||||
end
|
||||
|
||||
it 'adds errors to plugin_setting' do
|
||||
expect(plugin_setting.errors[:base]).to be_empty, 'precondition'
|
||||
validator
|
||||
expect(plugin_setting.errors[:base]).not_to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,14 +26,18 @@ describe MathMan do
|
|||
let(:use_for_svg) { false }
|
||||
|
||||
before do
|
||||
PluginSetting.create(
|
||||
name: 'mathman',
|
||||
settings: {
|
||||
@original_fallback = Canvas::DynamicSettings.fallback_data
|
||||
Canvas::DynamicSettings.fallback_data = {
|
||||
'math-man': {
|
||||
base_url: service_url,
|
||||
use_for_mml: use_for_mml,
|
||||
use_for_svg: use_for_svg
|
||||
}
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
after do
|
||||
Canvas::DynamicSettings.fallback_data = @original_fallback
|
||||
end
|
||||
|
||||
describe '.url_for' do
|
||||
|
|
|
@ -114,15 +114,16 @@ describe "RequestContextGenerator" do
|
|||
before(:each) do
|
||||
Thread.current[:context] = nil
|
||||
Canvas::DynamicSettings.reset_cache!
|
||||
Canvas::DynamicSettings.cache['canvas'] = {
|
||||
timetamp: Time.zone.now.to_i,
|
||||
value: { "signing-secret" => shared_secret }
|
||||
Canvas::DynamicSettings.fallback_data = {
|
||||
canvas: {
|
||||
'signing-secret' => shared_secret
|
||||
}
|
||||
}
|
||||
env['HTTP_X_REQUEST_CONTEXT_ID'] = Canvas::Security.base64_encode(remote_request_context_id)
|
||||
env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'] = Canvas::Security.base64_encode(remote_signature)
|
||||
end
|
||||
|
||||
after(:each){ Canvas::DynamicSettings.reset_cache! }
|
||||
after(:each){ Canvas::DynamicSettings.fallback_data = {} }
|
||||
|
||||
def run_middleware
|
||||
_, headers, _msg = RequestContextGenerator.new(->(_){ [200, {}, []] }).call(env)
|
||||
|
|
Loading…
Reference in New Issue