223 lines
7.9 KiB
Ruby
223 lines
7.9 KiB
Ruby
require_dependency 'canvas/draft_state_validations'
|
|
|
|
module Canvas
|
|
# defines the behavior when a protected attribute is assigned to in mass
|
|
# assignment. The default, and Rails' normal behavior, is to just :log. Set
|
|
# this to :raise to raise an exception.
|
|
mattr_accessor :protected_attribute_error
|
|
|
|
def self.active_record_foreign_key_check(name, type, options)
|
|
if name.to_s =~ /_id\z/ && type.to_s == 'integer' && options[:limit].to_i < 8
|
|
raise ArgumentError, <<-EOS
|
|
All foreign keys need to be at least 8-byte integers. #{name}
|
|
looks like a foreign key, please add this option: `:limit => 8`
|
|
EOS
|
|
end
|
|
end
|
|
|
|
def self.redis
|
|
raise "Redis is not enabled for this install" unless Canvas.redis_enabled?
|
|
@redis ||= begin
|
|
settings = ConfigFile.load('redis')
|
|
Canvas::RedisConfig.from_settings(settings).redis
|
|
end
|
|
end
|
|
|
|
# Builds a redis object using a config hash in the format used by a couple
|
|
# different config/*.yml files, like redis.yml, cache_store.yml and
|
|
# delayed_jobs.yml
|
|
def self.redis_from_config(redis_settings)
|
|
RedisConfig.from_settings(redis_settings).redis
|
|
end
|
|
|
|
def self.redis_enabled?
|
|
@redis_enabled ||= ConfigFile.load('redis').present?
|
|
end
|
|
|
|
def self.reconnect_redis
|
|
@redis = nil
|
|
if Rails.cache && Rails.cache.respond_to?(:reconnect)
|
|
Canvas::Redis.handle_redis_failure(nil, "none") do
|
|
Rails.cache.reconnect
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.cache_stores
|
|
unless @cache_stores
|
|
# this method is called really early in the bootup process, and
|
|
# autoloading might not be available yet, so we need to manually require
|
|
# Config
|
|
require_dependency 'lib/config_file'
|
|
@cache_stores = {}
|
|
configs = ConfigFile.load('cache_store', nil) || {}
|
|
|
|
# sanity check the file
|
|
unless configs.is_a?(Hash)
|
|
raise <<-EOS
|
|
Invalid config/cache_store.yml: Root is not a hash. See comments in
|
|
config/cache_store.yml.example
|
|
EOS
|
|
end
|
|
|
|
non_hashes = configs.keys.select { |k| !configs[k].is_a?(Hash) }
|
|
non_hashes.reject! { |k| configs[k].is_a?(String) && configs[configs[k]].is_a?(Hash) }
|
|
unless non_hashes.empty?
|
|
raise <<-EOS
|
|
Invalid config/cache_store.yml: Some keys are not hashes:
|
|
#{non_hashes.join(', ')}. See comments in
|
|
config/cache_store.yml.example
|
|
EOS
|
|
end
|
|
|
|
configs.each do |env, config|
|
|
if config.is_a?(String)
|
|
# switchman will treat strings as a link to another database server
|
|
@cache_stores[env] = config
|
|
next
|
|
end
|
|
config = {'cache_store' => 'mem_cache_store'}.merge(config)
|
|
case config.delete('cache_store')
|
|
when 'mem_cache_store'
|
|
config['namespace'] ||= config['key']
|
|
servers = config['servers'] || (ConfigFile.load('memcache', env))
|
|
if servers
|
|
@cache_stores[env] = :mem_cache_store, servers, config
|
|
end
|
|
when 'redis_store'
|
|
Bundler.require 'redis'
|
|
require_dependency 'canvas/redis'
|
|
Canvas::Redis.patch
|
|
# if cache and redis data are configured identically, we want to share connections
|
|
if config == {} && env == Rails.env && Canvas.redis_enabled?
|
|
# A bit of gymnastics to wrap an existing Redis::Store into an ActiveSupport::Cache::RedisStore
|
|
store = ActiveSupport::Cache::RedisStore.new([])
|
|
store.instance_variable_set(:@data, Canvas.redis.__getobj__)
|
|
@cache_stores[env] = store
|
|
else
|
|
# merge in redis.yml, but give precedence to cache_store.yml
|
|
#
|
|
# the only options currently supported in redis-cache are the list of
|
|
# servers, not key prefix or database names.
|
|
config = (ConfigFile.load('redis', env) || {}).merge(config)
|
|
config_options = config.symbolize_keys.except(:key, :servers, :database)
|
|
servers = config['servers']
|
|
if servers
|
|
servers = config['servers'].map { |s| Canvas::RedisConfig.url_to_redis_options(s).merge(config_options) }
|
|
@cache_stores[env] = :redis_store, servers
|
|
end
|
|
end
|
|
when 'memory_store'
|
|
@cache_stores[env] = :memory_store
|
|
when 'nil_store'
|
|
@cache_stores[env] = :null_store
|
|
end
|
|
end
|
|
@cache_stores[Rails.env] ||= :null_store
|
|
end
|
|
@cache_stores
|
|
end
|
|
|
|
# `sample` reports KB, not B
|
|
if File.directory?("/proc")
|
|
# linux w/ proc fs
|
|
LINUX_PAGE_SIZE = (size = `getconf PAGESIZE`.to_i; size > 0 ? size : 4096)
|
|
def self.sample_memory
|
|
s = File.read("/proc/#{Process.pid}/statm").to_i rescue 0
|
|
s * LINUX_PAGE_SIZE / 1024
|
|
end
|
|
else
|
|
# generic unix solution
|
|
def self.sample_memory
|
|
if Rails.env.test?
|
|
0
|
|
else
|
|
# hmm this is actually resident set size, doesn't include swapped-to-disk
|
|
# memory.
|
|
`ps -o rss= -p #{Process.pid}`.to_i
|
|
end
|
|
end
|
|
end
|
|
|
|
# can be called by plugins to allow reloading of that plugin in dev mode
|
|
# pass in the path to the plugin directory
|
|
# e.g., in the vendor/plugins/<plugin_name>/init.rb or
|
|
# gems/plugins/<plugin_name>/lib/<plugin_name>/engine.rb:
|
|
# Canvas.reloadable_plugin(File.dirname(__FILE__))
|
|
def self.reloadable_plugin(dirname)
|
|
return unless Rails.env.development?
|
|
base_path = File.expand_path(dirname)
|
|
base_path.gsub(%r{/lib/[^/]*$}, '')
|
|
ActiveSupport::Dependencies.autoload_once_paths.reject! { |p|
|
|
p[0, base_path.length] == base_path
|
|
}
|
|
end
|
|
|
|
def self.revision
|
|
return @revision if defined?(@revision)
|
|
@revision = if File.file?(Rails.root+"VERSION")
|
|
File.readlines(Rails.root+"VERSION").first.try(:strip)
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def self.timeout_protection_error_ttl(service_name)
|
|
(Setting.get("service_#{service_name}_error_ttl", nil) || Setting.get("service_generic_error_ttl", 1.minute.to_s)).to_i
|
|
end
|
|
|
|
# protection against calling external services that could timeout or misbehave.
|
|
# we keep track of timeouts in redis, and if a given service times out more
|
|
# than X times before the redis key expires in Y seconds (reset on each
|
|
# failure), we stop even trying to contact the service until the Y seconds
|
|
# passes.
|
|
#
|
|
# if redis isn't enabled, we'll still apply the timeout, but we won't track failures.
|
|
#
|
|
# all the configurable params have service-specific Settings with fallback to
|
|
# generic Settings.
|
|
def self.timeout_protection(service_name, options={}, &block)
|
|
timeout = (Setting.get("service_#{service_name}_timeout", nil) || options[:fallback_timeout_length] || Setting.get("service_generic_timeout", 15.seconds.to_s)).to_f
|
|
|
|
if Canvas.redis_enabled?
|
|
redis_key = "service:timeouts:#{service_name}"
|
|
cutoff = (Setting.get("service_#{service_name}_cutoff", nil) || Setting.get("service_generic_cutoff", 3.to_s)).to_i
|
|
error_ttl = timeout_protection_error_ttl(service_name)
|
|
short_circuit_timeout(Canvas.redis, redis_key, timeout, cutoff, error_ttl, &block)
|
|
else
|
|
Timeout.timeout(timeout, &block)
|
|
end
|
|
rescue TimeoutCutoff => e
|
|
Rails.logger.error("Skipping service call due to error count: #{service_name} #{e.error_count}")
|
|
raise if options[:raise_on_timeout]
|
|
return nil
|
|
rescue Timeout::Error => e
|
|
Canvas::Errors.capture_exception(:service_timeout, e)
|
|
raise if options[:raise_on_timeout]
|
|
return nil
|
|
end
|
|
|
|
def self.short_circuit_timeout(redis, redis_key, timeout, cutoff, error_ttl, &block)
|
|
error_count = redis.get(redis_key)
|
|
if error_count.to_i >= cutoff
|
|
raise TimeoutCutoff.new(error_count)
|
|
end
|
|
|
|
begin
|
|
Timeout.timeout(timeout, &block)
|
|
rescue Timeout::Error => e
|
|
redis.incrby(redis_key, 1)
|
|
redis.expire(redis_key, error_ttl)
|
|
raise
|
|
end
|
|
end
|
|
|
|
class TimeoutCutoff < Timeout::Error
|
|
attr_accessor :error_count
|
|
|
|
def initialize(error_count)
|
|
@error_count = error_count
|
|
end
|
|
end
|
|
end
|