switchman 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +30 -0
- data/app/models/switchman/shard.rb +502 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +9 -0
- data/db/migrate/20130328224244_create_default_shard.rb +9 -0
- data/lib/switchman.rb +9 -0
- data/lib/switchman/active_record/abstract_adapter.rb +11 -0
- data/lib/switchman/active_record/association.rb +108 -0
- data/lib/switchman/active_record/attribute_methods.rb +104 -0
- data/lib/switchman/active_record/base.rb +95 -0
- data/lib/switchman/active_record/calculations.rb +63 -0
- data/lib/switchman/active_record/connection_handler.rb +147 -0
- data/lib/switchman/active_record/connection_pool.rb +117 -0
- data/lib/switchman/active_record/finder_methods.rb +25 -0
- data/lib/switchman/active_record/log_subscriber.rb +43 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +13 -0
- data/lib/switchman/active_record/query_cache.rb +12 -0
- data/lib/switchman/active_record/query_methods.rb +184 -0
- data/lib/switchman/active_record/relation.rb +69 -0
- data/lib/switchman/cache_extensions.rb +12 -0
- data/lib/switchman/connection_pool_proxy.rb +62 -0
- data/lib/switchman/database_server.rb +197 -0
- data/lib/switchman/default_shard.rb +28 -0
- data/lib/switchman/engine.rb +91 -0
- data/lib/switchman/r_spec_helper.rb +124 -0
- data/lib/switchman/shackles.rb +34 -0
- data/lib/switchman/test_helper.rb +65 -0
- data/lib/switchman/version.rb +3 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/models/appendage.rb +24 -0
- data/spec/dummy/app/models/digit.rb +9 -0
- data/spec/dummy/app/models/feature.rb +5 -0
- data/spec/dummy/app/models/mirror_user.rb +5 -0
- data/spec/dummy/app/models/user.rb +23 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +59 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +17 -0
- data/spec/dummy/config/database.yml.example +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/routes.rb +8 -0
- data/spec/dummy/db/migrate/20130403132607_create_users.rb +10 -0
- data/spec/dummy/db/migrate/20130411202442_create_appendages.rb +10 -0
- data/spec/dummy/db/migrate/20130411202551_create_mirror_users.rb +9 -0
- data/spec/dummy/db/migrate/20131022202028_create_digits.rb +10 -0
- data/spec/dummy/db/migrate/20131206172923_create_features.rb +12 -0
- data/spec/dummy/db/schema.rb +57 -0
- data/spec/dummy/log/development.log +504 -0
- data/spec/dummy/log/test.log +29907 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/tmp/cache/2E2/830/shard%2F2 +0 -0
- data/spec/dummy/tmp/cache/2E3/840/shard%2F3 +0 -0
- data/spec/dummy/tmp/cache/313/970/shard%2F30 +0 -0
- data/spec/dummy/tmp/cache/314/980/shard%2F31 +0 -0
- data/spec/dummy/tmp/cache/316/980/shard%2F15 +1 -0
- data/spec/dummy/tmp/cache/316/9D0/shard%2F60 +0 -0
- data/spec/dummy/tmp/cache/317/990/shard%2F16 +0 -0
- data/spec/dummy/tmp/cache/317/9C0/shard%2F43 +1 -0
- data/spec/dummy/tmp/cache/317/9E0/shard%2F61 +0 -0
- data/spec/dummy/tmp/cache/318/9A0/shard%2F17 +0 -0
- data/spec/dummy/tmp/cache/318/9D0/shard%2F44 +0 -0
- data/spec/dummy/tmp/cache/318/9F0/shard%2F62 +1 -0
- data/spec/dummy/tmp/cache/319/9E0/shard%2F45 +0 -0
- data/spec/dummy/tmp/cache/319/9F0/shard%2F54 +1 -0
- data/spec/dummy/tmp/cache/319/A10/shard%2F72 +1 -0
- data/spec/dummy/tmp/cache/319/A30/shard%2F90 +0 -0
- data/spec/dummy/tmp/cache/31B/9E0/shard%2F29 +1 -0
- data/spec/dummy/tmp/cache/321/AA0/shard%2F89 +0 -0
- data/spec/dummy/tmp/cache/322/AC0/shard%2F99 +1 -0
- data/spec/dummy/tmp/cache/344/D70/shard%2F103 +1 -0
- data/spec/dummy/tmp/cache/345/D80/shard%2F104 +0 -0
- data/spec/dummy/tmp/cache/345/DB0/shard%2F131 +1 -0
- data/spec/dummy/tmp/cache/345/DC0/shard%2F140 +0 -0
- data/spec/dummy/tmp/cache/346/D90/shard%2F105 +0 -0
- data/spec/dummy/tmp/cache/346/DB0/shard%2F123 +0 -0
- data/spec/dummy/tmp/cache/346/DD0/shard%2F222 +1 -0
- data/spec/dummy/tmp/cache/346/DE0/shard%2F150 +0 -0
- data/spec/dummy/tmp/cache/346/DF0/shard%2F240 +1 -0
- data/spec/dummy/tmp/cache/347/DA0/shard%2F106 +1 -0
- data/spec/dummy/tmp/cache/347/DC0/shard%2F124 +0 -0
- data/spec/dummy/tmp/cache/347/DC0/shard%2F205 +1 -0
- data/spec/dummy/tmp/cache/347/E10/shard%2F250 +1 -0
- data/spec/dummy/tmp/cache/348/DF0/shard%2F143 +1 -0
- data/spec/dummy/tmp/cache/348/DF0/shard%2F224 +1 -0
- data/spec/dummy/tmp/cache/348/E10/shard%2F161 +1 -0
- data/spec/dummy/tmp/cache/349/DD0/shard%2F117 +1 -0
- data/spec/dummy/tmp/cache/349/E40/shard%2F180 +1 -0
- data/spec/dummy/tmp/cache/34A/DF0/shard%2F127 +1 -0
- data/spec/dummy/tmp/cache/34A/DF0/shard%2F208 +1 -0
- data/spec/dummy/tmp/cache/34A/E10/shard%2F145 +1 -0
- data/spec/dummy/tmp/cache/34A/E60/shard%2F190 +1 -0
- data/spec/dummy/tmp/cache/34B/E30/shard%2F155 +1 -0
- data/spec/dummy/tmp/cache/34D/E30/shard%2F139 +0 -0
- data/spec/dummy/tmp/cache/34E/E50/shard%2F149 +0 -0
- data/spec/dummy/tmp/cache/353/EF0/shard%2F199 +1 -0
- data/spec/dummy/tmp/cache/3A4/E90/shard%2F10003 +1 -0
- data/spec/dummy/tmp/cache/3A5/ED0/shard%2F10031 +1 -0
- data/spec/dummy/tmp/cache/3A9/EF0/shard%2F10017 +1 -0
- data/spec/lib/active_record/association_spec.rb +305 -0
- data/spec/lib/active_record/attribute_methods_spec.rb +108 -0
- data/spec/lib/active_record/base_spec.rb +66 -0
- data/spec/lib/active_record/calculations_spec.rb +119 -0
- data/spec/lib/active_record/connection_handler_spec.rb +45 -0
- data/spec/lib/active_record/connection_pool_spec.rb +23 -0
- data/spec/lib/active_record/finder_methods_spec.rb +29 -0
- data/spec/lib/active_record/query_cache_spec.rb +20 -0
- data/spec/lib/active_record/query_methods_spec.rb +130 -0
- data/spec/lib/active_record/relation_spec.rb +38 -0
- data/spec/lib/cache_extensions_spec.rb +27 -0
- data/spec/lib/connection_pool_proxy_spec.rb +13 -0
- data/spec/lib/database_server_spec.rb +154 -0
- data/spec/lib/shackles_spec.rb +147 -0
- data/spec/models/shard_spec.rb +382 -0
- data/spec/spec_helper.rb +32 -0
- metadata +344 -0
data/lib/switchman.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
module Switchman
|
2
|
+
module ActiveRecord
|
3
|
+
module Association
|
4
|
+
def self.included(klass)
|
5
|
+
%w{build_record load_target scoped}.each do |method|
|
6
|
+
klass.alias_method_chain(method, :sharding)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def shard
|
11
|
+
# polymorphic associations assume the same shard as the owning item
|
12
|
+
if @reflection.options[:polymorphic] || @reflection.klass.shard_category == @owner.class.shard_category
|
13
|
+
@owner.shard
|
14
|
+
else
|
15
|
+
Shard.default
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_record_with_sharding(*args)
|
20
|
+
self.shard.activate { build_record_without_sharding(*args) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def load_target_with_sharding
|
24
|
+
self.shard.activate { load_target_without_sharding }
|
25
|
+
end
|
26
|
+
|
27
|
+
def scoped_with_sharding
|
28
|
+
shard_value = @reflection.options[:multishard] ? @owner : self.shard
|
29
|
+
self.shard.activate { scoped_without_sharding.shard(shard_value, :association) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Builder
|
34
|
+
module Association
|
35
|
+
def self.included(klass)
|
36
|
+
klass.descendants.each{|d| d.valid_options += [:multishard]}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Preloader
|
42
|
+
module Association
|
43
|
+
def self.included(klass)
|
44
|
+
klass.send(:remove_method, :associated_records_by_owner)
|
45
|
+
klass.send(:remove_method, :owners_by_key)
|
46
|
+
klass.send(:remove_method, :scoped)
|
47
|
+
end
|
48
|
+
|
49
|
+
def associated_records_by_owner
|
50
|
+
owners_map = owners_by_key
|
51
|
+
|
52
|
+
if klass.nil? || owners_map.empty?
|
53
|
+
records = []
|
54
|
+
else
|
55
|
+
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
|
56
|
+
# Make several smaller queries if necessary or make one query if the adapter supports it
|
57
|
+
records = Shard.partition_by_shard(owners) do |partitioned_owners|
|
58
|
+
sliced_owners = partitioned_owners.each_slice(model.connection.in_clause_length || partitioned_owners.size)
|
59
|
+
sliced_owners.map do |slice|
|
60
|
+
relative_owner_keys = slice.map do |owner|
|
61
|
+
key = owner[owner_key_name]
|
62
|
+
if key && owner.class.sharded_column?(owner_key_name)
|
63
|
+
key = Shard.relative_id_for(key, owner.shard, Shard.current(owner.class.shard_category))
|
64
|
+
end
|
65
|
+
key && key.to_s
|
66
|
+
end
|
67
|
+
relative_owner_keys.compact!
|
68
|
+
records_for(relative_owner_keys)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
records.flatten!
|
72
|
+
end
|
73
|
+
|
74
|
+
# Each record may have multiple owners, and vice-versa
|
75
|
+
records_by_owner = Hash[owners.map { |owner| [owner, []] }]
|
76
|
+
records.each do |record|
|
77
|
+
owner_key = record[association_key_name]
|
78
|
+
owner_key = Shard.global_id_for(owner_key, record.shard) if owner_key && record.class.sharded_column?(association_key_name)
|
79
|
+
|
80
|
+
owners_map[owner_key.to_s].each do |owner|
|
81
|
+
records_by_owner[owner] << record
|
82
|
+
end
|
83
|
+
end
|
84
|
+
records_by_owner
|
85
|
+
end
|
86
|
+
|
87
|
+
def owners_by_key
|
88
|
+
@owners_by_key ||= owners.group_by do |owner|
|
89
|
+
key = owner[owner_key_name]
|
90
|
+
key = Shard.global_id_for(key, owner.shard) if key && owner.class.sharded_column?(owner_key_name)
|
91
|
+
key && key.to_s
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def scoped
|
96
|
+
build_scope
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module CollectionProxy
|
102
|
+
def shard(*args)
|
103
|
+
scoped.shard(*args)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Switchman
|
2
|
+
module ActiveRecord
|
3
|
+
module AttributeMethods
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
def sharded_primary_key?
|
7
|
+
self != Shard && shard_category != :unsharded && integral_id?
|
8
|
+
end
|
9
|
+
|
10
|
+
def sharded_foreign_key?(column_name)
|
11
|
+
reflection = reflection_for_integer_attribute(column_name.to_s)
|
12
|
+
return false unless reflection
|
13
|
+
reflection.options[:polymorphic] || reflection.klass.sharded_primary_key?
|
14
|
+
end
|
15
|
+
|
16
|
+
def sharded_column?(column_name)
|
17
|
+
column_name = column_name.to_s
|
18
|
+
@sharded_column_values ||= {}
|
19
|
+
unless @sharded_column_values.has_key?(column_name)
|
20
|
+
@sharded_column_values[column_name] = (column_name == primary_key && sharded_primary_key?) || sharded_foreign_key?(column_name)
|
21
|
+
end
|
22
|
+
@sharded_column_values[column_name]
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def reflection_for_integer_attribute(attr_name)
|
28
|
+
columns_hash[attr_name] && columns_hash[attr_name].type == :integer &&
|
29
|
+
reflections.find { |_, r| r.belongs_to? && r.foreign_key == attr_name }.try(:last)
|
30
|
+
rescue ::ActiveRecord::StatementInvalid
|
31
|
+
# this is for when models are referenced in initializers before migrations have been run
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_method_global_attribute(attr_name)
|
36
|
+
if sharded_column?(attr_name)
|
37
|
+
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
38
|
+
def __temp__
|
39
|
+
Shard.global_id_for(original_#{attr_name}, shard)
|
40
|
+
end
|
41
|
+
alias_method 'global_#{attr_name}', :__temp__
|
42
|
+
undef_method :__temp__
|
43
|
+
RUBY
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def define_method_local_attribute(attr_name)
|
48
|
+
if sharded_column?(attr_name)
|
49
|
+
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
50
|
+
def __temp__
|
51
|
+
Shard.local_id_for(original_#{attr_name}).first
|
52
|
+
end
|
53
|
+
alias_method 'local_#{attr_name}', :__temp__
|
54
|
+
undef_method :__temp__
|
55
|
+
RUBY
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def shard_category_code_for_reflection(reflection)
|
60
|
+
if reflection
|
61
|
+
if reflection.options[:polymorphic]
|
62
|
+
# a polymorphic association has to be discovered at runtime. This code ends up being something like
|
63
|
+
# context_type.try(:constantize).try(:shard_category) || :default
|
64
|
+
"#{reflection.foreign_type}.try(:constantize).try(:shard_category) || :default"
|
65
|
+
else
|
66
|
+
# otherwise we can just return a symbol for the statically known type of the association
|
67
|
+
reflection.klass.shard_category.inspect
|
68
|
+
end
|
69
|
+
else
|
70
|
+
shard_category.inspect
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def define_method_original_attribute(attr_name)
|
75
|
+
if sharded_column?(attr_name)
|
76
|
+
reflection = reflection_for_integer_attribute(attr_name)
|
77
|
+
generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
78
|
+
# rename the original method to original_
|
79
|
+
alias_method 'original_#{attr_name}', '#{attr_name}'
|
80
|
+
# and replace with one that transposes the id
|
81
|
+
def __temp__
|
82
|
+
Shard.relative_id_for(original_#{attr_name}, shard, Shard.current(#{shard_category_code_for_reflection(reflection)}))
|
83
|
+
end
|
84
|
+
alias_method '#{attr_name}', :__temp__
|
85
|
+
undef_method :__temp__
|
86
|
+
|
87
|
+
alias_method 'original_#{attr_name}=', '#{attr_name}='
|
88
|
+
def __temp__(new_value)
|
89
|
+
self.original_#{attr_name} = Shard.relative_id_for(new_value, Shard.current(#{shard_category_code_for_reflection(reflection)}), shard)
|
90
|
+
end
|
91
|
+
alias_method '#{attr_name}=', :__temp__
|
92
|
+
undef_method :__temp__
|
93
|
+
RUBY
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.included(klass)
|
99
|
+
klass.extend(ClassMethods)
|
100
|
+
klass.attribute_method_prefix "global_", "local_", "original_"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Switchman
|
2
|
+
module ActiveRecord
|
3
|
+
module Base
|
4
|
+
module ClassMethods
|
5
|
+
attr_writer :shard_category
|
6
|
+
delegate :shard, :to => :scoped
|
7
|
+
|
8
|
+
def shard_category
|
9
|
+
@shard_category || :default
|
10
|
+
end
|
11
|
+
|
12
|
+
def shard_category=(category)
|
13
|
+
categories = Shard.const_get(:CATEGORIES)
|
14
|
+
if categories[shard_category]
|
15
|
+
categories[shard_category].delete(self)
|
16
|
+
categories.delete(shard_category) if categories[shard_category].empty?
|
17
|
+
end
|
18
|
+
# TODO: de-initialize the proxy
|
19
|
+
categories[category] ||= []
|
20
|
+
categories[category] << self
|
21
|
+
@shard_category = category
|
22
|
+
connection_handler.initialize_categories(superclass)
|
23
|
+
end
|
24
|
+
|
25
|
+
def integral_id?
|
26
|
+
if @integral_id == nil
|
27
|
+
@integral_id = columns_hash[primary_key].type == :integer
|
28
|
+
end
|
29
|
+
@integral_id
|
30
|
+
end
|
31
|
+
|
32
|
+
def transaction(*args, &block)
|
33
|
+
return super if !block || !current_scope
|
34
|
+
super(*args) do
|
35
|
+
current_scope.activate(&block)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.included(klass)
|
41
|
+
klass.extend(ClassMethods)
|
42
|
+
klass.set_callback(:initialize, :before) { @shard = Shard.current(self.class.shard_category) }
|
43
|
+
end
|
44
|
+
|
45
|
+
def shard
|
46
|
+
@shard || Shard.current(self.class.shard_category) || Shard.default
|
47
|
+
end
|
48
|
+
|
49
|
+
def shard=(new_shard)
|
50
|
+
raise ::ActiveRecord::ReadOnlyRecord if !self.new_record? || @shard_set_in_stone
|
51
|
+
if shard != new_shard
|
52
|
+
# TODO: adjust foreign keys
|
53
|
+
@shard = new_shard
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def save(*args)
|
58
|
+
@shard_set_in_stone = true
|
59
|
+
shard.activate(self.class.shard_category) { super }
|
60
|
+
end
|
61
|
+
|
62
|
+
def save!(*args)
|
63
|
+
@shard_set_in_stone = true
|
64
|
+
shard.activate(self.class.shard_category) { super }
|
65
|
+
end
|
66
|
+
|
67
|
+
def destroy
|
68
|
+
shard.activate(self.class.shard_category) { super }
|
69
|
+
end
|
70
|
+
|
71
|
+
def clone
|
72
|
+
result = super
|
73
|
+
# TODO: adjust foreign keys
|
74
|
+
# don't use the setter, cause the foreign keys are already
|
75
|
+
# relative to this shard
|
76
|
+
result.instance_variable_set(:@shard, self.shard)
|
77
|
+
result
|
78
|
+
end
|
79
|
+
|
80
|
+
def transaction(&block)
|
81
|
+
shard.activate do
|
82
|
+
super
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def hash
|
87
|
+
global_id.hash
|
88
|
+
end
|
89
|
+
|
90
|
+
def to_param
|
91
|
+
Shard.short_id_for(self.id) if persisted?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Switchman
|
2
|
+
module ActiveRecord
|
3
|
+
module Calculations
|
4
|
+
def self.included(klass)
|
5
|
+
%w{execute_simple_calculation pluck}.each do |method|
|
6
|
+
klass.alias_method_chain(method, :sharding)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def pluck_with_sharding(column_name)
|
11
|
+
target_shard = Shard.current(klass.shard_category)
|
12
|
+
self.activate do |relation, shard|
|
13
|
+
results = relation.pluck_without_sharding(column_name)
|
14
|
+
if klass.sharded_column?(column_name.to_s)
|
15
|
+
results = results.map{|result| Shard.relative_id_for(result, shard, target_shard)}
|
16
|
+
end
|
17
|
+
results
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: grouped calculations
|
22
|
+
def execute_simple_calculation_with_sharding(operation, column_name, distinct)
|
23
|
+
operation = operation.to_s.downcase
|
24
|
+
if operation == "average"
|
25
|
+
result = calculate_average(column_name, distinct)
|
26
|
+
else
|
27
|
+
result = self.activate{ |relation| relation.send(:execute_simple_calculation_without_sharding, operation, column_name, distinct) }
|
28
|
+
if result.is_a?(Array)
|
29
|
+
case operation
|
30
|
+
when "count", "sum"
|
31
|
+
result = result.sum
|
32
|
+
when "minimum"
|
33
|
+
result = result.min
|
34
|
+
when "maximum"
|
35
|
+
result = result.max
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_average(column_name, distinct)
|
43
|
+
# See activerecord#execute_simple_calculation
|
44
|
+
relation = reorder(nil)
|
45
|
+
column = aggregate_column(column_name)
|
46
|
+
relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
|
47
|
+
operation_over_aggregate_column(column, "count", distinct).as("count")]
|
48
|
+
|
49
|
+
initial_results = relation.activate{ |rel| @klass.connection.select_all(rel) }
|
50
|
+
if initial_results.is_a?(Array)
|
51
|
+
initial_results.each do |r|
|
52
|
+
r["average"] = type_cast_calculated_value(r["average"], nil, "average")
|
53
|
+
r["count"] = type_cast_calculated_value(r["count"], nil, "count")
|
54
|
+
end
|
55
|
+
result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
|
56
|
+
else
|
57
|
+
result = type_cast_calculated_value(initial_results["average"], nil, "average")
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require_dependency 'switchman/connection_pool_proxy'
|
2
|
+
require_dependency 'switchman/shard'
|
3
|
+
|
4
|
+
module Switchman
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionHandler
|
7
|
+
def self.make_sharing_automagic(config, shard)
|
8
|
+
key = config[:adapter] == 'postgresql' ? :schema_search_path : :database
|
9
|
+
|
10
|
+
# we may not be able to connect to this shard yet, cause it might be an empty database server
|
11
|
+
shard_name = shard.name rescue nil
|
12
|
+
return unless shard_name
|
13
|
+
|
14
|
+
config[:shard_name] ||= shard_name
|
15
|
+
if !config[key] || config[key] == shard_name
|
16
|
+
# this may truncate the schema_search_path if it was not specified in database.yml
|
17
|
+
# but that's what our old behavior was anyway, so I guess it's okay
|
18
|
+
config[key] = '%{shard_name}'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(klass)
|
23
|
+
klass.alias_method_chain(:establish_connection, :sharding)
|
24
|
+
klass.alias_method_chain(:remove_connection, :sharding)
|
25
|
+
end
|
26
|
+
|
27
|
+
def establish_connection_with_sharding(name, spec)
|
28
|
+
establish_connection_without_sharding(name, spec)
|
29
|
+
|
30
|
+
# this is the first place that the adapter would have been required; but now we
|
31
|
+
# need this addition ASAP since it will be called when loading the default shard below
|
32
|
+
if defined?(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
|
33
|
+
require "switchman/active_record/postgresql_adapter"
|
34
|
+
::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:include, ActiveRecord::PostgreSQLAdapter)
|
35
|
+
end
|
36
|
+
|
37
|
+
model = name.constantize
|
38
|
+
pool = connection_pools[spec]
|
39
|
+
|
40
|
+
first_time = !Shard.instance_variable_get(:@default)
|
41
|
+
if first_time
|
42
|
+
# Have to cache the default shard before we insert sharding, otherwise the first access
|
43
|
+
# to sharding will recurse onto itself trying to access column information
|
44
|
+
Shard.default
|
45
|
+
|
46
|
+
# automatically change config to allow for sharing connections with simple config
|
47
|
+
ConnectionHandler.make_sharing_automagic(spec.config, Shard.default)
|
48
|
+
ConnectionHandler.make_sharing_automagic(Shard.default.database_server.config, Shard.default)
|
49
|
+
end
|
50
|
+
@shard_connection_pools ||= { Shard.default.database_server.id => pool }
|
51
|
+
|
52
|
+
proxy = ConnectionPoolProxy.new(model.shard_category,
|
53
|
+
pool,
|
54
|
+
@shard_connection_pools)
|
55
|
+
connection_pools[spec] = proxy
|
56
|
+
|
57
|
+
initialize_categories(model)
|
58
|
+
@class_to_pool[name] = proxy
|
59
|
+
|
60
|
+
# reload the default shard if we just got a new connection
|
61
|
+
# to where the Shards table is
|
62
|
+
# DON'T do it if we're not the current connection handler - that means
|
63
|
+
# we're in the middle of switching environments, and we don't want to
|
64
|
+
# establish a connection with incorrect settings
|
65
|
+
if (model == ::ActiveRecord::Base || model == Shard) && self == ::ActiveRecord::Base.connection_handler && !first_time
|
66
|
+
Shard.default(true) unless first_time
|
67
|
+
proxy.disconnect!
|
68
|
+
end
|
69
|
+
|
70
|
+
if first_time
|
71
|
+
# do the change for other database servers, now that we can switch shards
|
72
|
+
if Shard.default.is_a?(Shard)
|
73
|
+
DatabaseServer.all.each do |server|
|
74
|
+
next if server == Shard.default.database_server
|
75
|
+
shard = server.shards.where(:name => nil).first
|
76
|
+
shard ||= Shard.new(:database_server => server)
|
77
|
+
ConnectionHandler.make_sharing_automagic(server.config, shard)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# we may have established some connections above trying to infer the shard's name.
|
81
|
+
# close them, so that someone that doesn't expect them doesn't try to fork
|
82
|
+
# without closing them
|
83
|
+
self.clear_all_connections!
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def remove_connection_with_sharding(model)
|
88
|
+
uninitialize_ar(model) if @class_to_pool[model.name].is_a?(ConnectionPoolProxy)
|
89
|
+
remove_connection_without_sharding(model)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def uninitialize_ar(model = ::ActiveRecord::Base)
|
95
|
+
# take the proxies out
|
96
|
+
@class_to_pool.each_key do |model_name|
|
97
|
+
pool_model = model_name.constantize
|
98
|
+
# only de-proxify models that inherit from what we're uninitializing
|
99
|
+
next unless pool_model == model || pool_model < model
|
100
|
+
proxy = @class_to_pool[model_name]
|
101
|
+
next unless proxy.is_a?(ConnectionPoolProxy)
|
102
|
+
|
103
|
+
# make sure we're switched back to the default shard for the
|
104
|
+
# connection that will remain
|
105
|
+
if proxy.connected?
|
106
|
+
Shard.default.activate { proxy.connection }
|
107
|
+
end
|
108
|
+
connection_pools[proxy.spec] = proxy.default_pool
|
109
|
+
@class_to_pool[model_name] = proxy.default_pool
|
110
|
+
end
|
111
|
+
|
112
|
+
# prune dups that were created for implementing shard categories
|
113
|
+
@class_to_pool.each_key do |model_name|
|
114
|
+
next if model_name == ::ActiveRecord::Base.name
|
115
|
+
pool_model = model_name.constantize
|
116
|
+
@class_to_pool.delete(model_name) if retrieve_connection_pool(pool_model.superclass) == @class_to_pool[model_name]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# semi-private
|
121
|
+
public
|
122
|
+
def initialize_categories(model = ::ActiveRecord::Base)
|
123
|
+
# now set up pools for models that inherit from this model, but with a different
|
124
|
+
# sharding category
|
125
|
+
Shard.const_get(:CATEGORIES).each do |category, models|
|
126
|
+
next if category == :default
|
127
|
+
next if category == model.shard_category
|
128
|
+
|
129
|
+
this_proxy = nil
|
130
|
+
Array(models).each do |category_model|
|
131
|
+
category_model = category_model.constantize if category_model.is_a? String
|
132
|
+
next unless category_model < model
|
133
|
+
|
134
|
+
# don't replace existing connections
|
135
|
+
next if @class_to_pool[category_model.name]
|
136
|
+
|
137
|
+
default_pool = retrieve_connection_pool(model)
|
138
|
+
default_pool = default_pool.default_pool if default_pool.is_a?(ConnectionPoolProxy)
|
139
|
+
# look for an existing compatible proxy for this category
|
140
|
+
this_proxy ||= ConnectionPoolProxy.new(category_model.shard_category, default_pool, @shard_connection_pools)
|
141
|
+
@class_to_pool[category_model.name] = this_proxy
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|