switchman 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|