2011-02-01 09:57:29 +08:00
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
2011-05-03 13:56:39 +08:00
# defines the behavior when nil or empty array arguments passed into dynamic
# finders. The default (:log) logs a warning if the finder is not scoped and
# if *all* arguments are nil/[], e.g.
# Thing.find_by_foo_and_bar(nil, nil) # warning
# Other.find_by_baz([]) # warning
# Another.find_all_by_a_and_b(123, nil) # ok
# ThisThing.some_scope.find_by_whatsit(nil) # ok
# Set this to :raise to raise an exception.
mattr_accessor :dynamic_finder_nil_arguments_error
2014-08-29 02:39:23 +08:00
# defines extensions that could possibly be used, so that specs can move them to the
# correct schemas for sharding
mattr_accessor :possible_postgres_extensions
self . possible_postgres_extensions = [ :pg_collkey , :pg_trgm ]
2011-02-01 09:57:29 +08:00
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 , " All foreign keys need to be 8-byte integers. #{ name } looks like a foreign key to me, please add this option: `:limit => 8` "
end
end
2011-03-25 05:36:07 +08:00
def self . redis
2013-05-09 23:23:26 +08:00
raise " Redis is not enabled for this install " unless Canvas . redis_enabled?
@redis || = begin
2014-05-17 00:49:42 +08:00
settings = ConfigFile . load ( 'redis' )
2013-05-09 23:23:26 +08:00
Canvas :: RedisConfig . from_settings ( settings ) . redis
end
2012-08-16 23:21:18 +08:00
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 )
2013-05-09 23:23:26 +08:00
RedisConfig . from_settings ( redis_settings ) . redis
2011-05-20 12:29:01 +08:00
end
def self . redis_enabled?
2014-05-17 00:49:42 +08:00
@redis_enabled || = ConfigFile . load ( 'redis' ) . present?
2011-03-25 05:36:07 +08:00
end
2011-05-14 03:20:26 +08:00
2012-03-10 00:34:13 +08:00
def self . reconnect_redis
@redis = nil
if Rails . cache && Rails . cache . respond_to? ( :reconnect )
2012-09-11 07:35:05 +08:00
Canvas :: Redis . handle_redis_failure ( nil , " none " ) do
2012-05-05 00:27:17 +08:00
Rails . cache . reconnect
end
2012-03-10 00:34:13 +08:00
end
end
2014-01-14 01:19:23 +08:00
def self . cache_store_config ( rails_env = :current , nil_is_nil = true )
rails_env = Rails . env if rails_env == :current
cache_stores [ rails_env ]
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
2014-05-17 00:49:42 +08:00
# Config
require_dependency 'lib/config_file'
2014-01-14 01:19:23 +08:00
@cache_stores = { }
2014-05-17 00:49:42 +08:00
configs = ConfigFile . load ( 'cache_store' , nil ) || { }
2014-02-06 01:58:12 +08:00
# sanity check the file
unless configs . is_a? ( Hash )
raise " Invalid config/cache_store.yml: Root is not a hash. See comments in config/cache_store.yml.example "
end
2014-08-26 04:55:46 +08:00
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 ) }
raise " Invalid config/cache_store.yml: Some keys are not hashes: #{ non_hashes . join ( ', ' ) } . See comments in config/cache_store.yml.example " unless non_hashes . empty?
2014-02-06 01:58:12 +08:00
2014-02-05 01:58:20 +08:00
configs . each do | env , config |
2014-08-26 04:55:46 +08:00
if config . is_a? ( String )
# switchman will treat strings as a link to another database server
@cache_stores [ env ] = config
next
end
2014-01-14 01:19:23 +08:00
config = { 'cache_store' = > 'mem_cache_store' } . merge ( config )
case config . delete ( 'cache_store' )
when 'mem_cache_store'
config [ 'namespace' ] || = config [ 'key' ]
2014-05-17 00:49:42 +08:00
servers = config [ 'servers' ] || ( ConfigFile . load ( 'memcache' , env ) )
2014-01-14 01:19:23 +08:00
if servers
@cache_stores [ env ] = :mem_cache_store , servers , config
end
when 'redis_store'
Bundler . require 'redis'
require_dependency 'canvas/redis'
Canvas :: Redis . patch
# 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.
2014-05-17 00:49:42 +08:00
config = ( ConfigFile . load ( 'redis' , env ) || { } ) . merge ( config )
2014-08-26 01:45:30 +08:00
config_options = config . symbolize_keys . except ( :key , :servers , :database )
2014-08-26 23:19:09 +08:00
servers = config [ 'servers' ]
if servers
servers = config [ 'servers' ] . map { | s | :: Redis :: Factory . convert_to_redis_client_options ( s ) . merge ( config_options ) }
@cache_stores [ env ] = :redis_store , servers
end
2014-01-14 01:19:23 +08:00
when 'memory_store'
@cache_stores [ env ] = :memory_store
when 'nil_store'
2014-07-24 01:14:22 +08:00
@cache_stores [ env ] = :null_store
2014-01-14 01:19:23 +08:00
end
2013-11-15 05:22:43 +08:00
end
2014-07-24 01:14:22 +08:00
@cache_stores [ Rails . env ] || = :null_store
2011-07-07 05:43:19 +08:00
end
2014-01-14 01:19:23 +08:00
@cache_stores
2011-07-07 05:43:19 +08:00
end
2011-05-14 03:20:26 +08:00
# `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
2013-10-31 04:17:29 +08:00
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
2011-05-14 03:20:26 +08:00
end
end
2012-01-17 05:00:58 +08:00
# 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:
# Canvas.reloadable_plugin(File.dirname(__FILE__))
def self . reloadable_plugin ( dirname )
return unless Rails . env . development?
base_path = File . expand_path ( dirname )
2013-03-21 04:30:20 +08:00
ActiveSupport :: Dependencies . autoload_once_paths . reject! { | p |
2012-01-17 05:00:58 +08:00
p [ 0 , base_path . length ] == base_path
}
end
2012-03-06 07:36:22 +08:00
def self . revision
2014-08-28 12:49:59 +08:00
return @revision if defined? ( @revision )
@revision = if File . file? ( Rails . root + " VERSION " )
File . readlines ( Rails . root + " VERSION " ) . first . try ( :strip )
else
nil
2012-03-06 07:36:22 +08:00
end
end
2012-05-16 05:15:11 +08:00
# 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.
2014-06-05 06:43:34 +08:00
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
2012-05-16 05:15:11 +08:00
if Canvas . redis_enabled?
2014-06-05 06:43:34 +08:00
redis_key = " service:timeouts: #{ service_name } "
2013-10-05 04:02:49 +08:00
cutoff = ( Setting . get ( " service_ #{ service_name } _cutoff " , nil ) || Setting . get ( " service_generic_cutoff " , 3 . to_s ) ) . to_i
2014-06-05 06:43:34 +08:00
error_ttl = ( Setting . get ( " service_ #{ service_name } _error_ttl " , nil ) || Setting . get ( " service_generic_error_ttl " , 1 . minute . to_s ) ) . to_i
short_circuit_timeout ( Canvas . redis , redis_key , timeout , cutoff , error_ttl , & block )
else
Timeout . timeout ( timeout , & block )
2012-05-16 05:15:11 +08:00
end
2014-06-05 06:43:34 +08:00
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
ErrorReport . log_exception ( :service_timeout , e )
raise if options [ :raise_on_timeout ]
return nil
end
2012-05-16 05:15:11 +08:00
2014-06-05 06:43:34 +08:00
def self . short_circuit_timeout ( redis , redis_key , timeout , cutoff , error_ttl )
error_count = redis . get ( redis_key )
if error_count . to_i > = cutoff
raise TimeoutCutoff . new ( error_count )
end
2013-10-06 00:58:25 +08:00
begin
Timeout . timeout ( timeout ) do
yield
end
rescue Timeout :: Error = > e
2014-06-05 06:43:34 +08:00
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
2012-05-16 05:15:11 +08:00
end
end
2011-02-01 09:57:29 +08:00
end