switchman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +30 -0
  3. data/app/models/switchman/shard.rb +502 -0
  4. data/db/migrate/20130328212039_create_switchman_shards.rb +9 -0
  5. data/db/migrate/20130328224244_create_default_shard.rb +9 -0
  6. data/lib/switchman.rb +9 -0
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -0
  8. data/lib/switchman/active_record/association.rb +108 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +104 -0
  10. data/lib/switchman/active_record/base.rb +95 -0
  11. data/lib/switchman/active_record/calculations.rb +63 -0
  12. data/lib/switchman/active_record/connection_handler.rb +147 -0
  13. data/lib/switchman/active_record/connection_pool.rb +117 -0
  14. data/lib/switchman/active_record/finder_methods.rb +25 -0
  15. data/lib/switchman/active_record/log_subscriber.rb +43 -0
  16. data/lib/switchman/active_record/postgresql_adapter.rb +13 -0
  17. data/lib/switchman/active_record/query_cache.rb +12 -0
  18. data/lib/switchman/active_record/query_methods.rb +184 -0
  19. data/lib/switchman/active_record/relation.rb +69 -0
  20. data/lib/switchman/cache_extensions.rb +12 -0
  21. data/lib/switchman/connection_pool_proxy.rb +62 -0
  22. data/lib/switchman/database_server.rb +197 -0
  23. data/lib/switchman/default_shard.rb +28 -0
  24. data/lib/switchman/engine.rb +91 -0
  25. data/lib/switchman/r_spec_helper.rb +124 -0
  26. data/lib/switchman/shackles.rb +34 -0
  27. data/lib/switchman/test_helper.rb +65 -0
  28. data/lib/switchman/version.rb +3 -0
  29. data/spec/dummy/Rakefile +7 -0
  30. data/spec/dummy/app/models/appendage.rb +24 -0
  31. data/spec/dummy/app/models/digit.rb +9 -0
  32. data/spec/dummy/app/models/feature.rb +5 -0
  33. data/spec/dummy/app/models/mirror_user.rb +5 -0
  34. data/spec/dummy/app/models/user.rb +23 -0
  35. data/spec/dummy/config.ru +4 -0
  36. data/spec/dummy/config/application.rb +59 -0
  37. data/spec/dummy/config/boot.rb +10 -0
  38. data/spec/dummy/config/database.yml +17 -0
  39. data/spec/dummy/config/database.yml.example +25 -0
  40. data/spec/dummy/config/environment.rb +5 -0
  41. data/spec/dummy/config/environments/development.rb +37 -0
  42. data/spec/dummy/config/environments/production.rb +67 -0
  43. data/spec/dummy/config/environments/test.rb +37 -0
  44. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  46. data/spec/dummy/config/initializers/session_store.rb +8 -0
  47. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/spec/dummy/config/routes.rb +8 -0
  49. data/spec/dummy/db/migrate/20130403132607_create_users.rb +10 -0
  50. data/spec/dummy/db/migrate/20130411202442_create_appendages.rb +10 -0
  51. data/spec/dummy/db/migrate/20130411202551_create_mirror_users.rb +9 -0
  52. data/spec/dummy/db/migrate/20131022202028_create_digits.rb +10 -0
  53. data/spec/dummy/db/migrate/20131206172923_create_features.rb +12 -0
  54. data/spec/dummy/db/schema.rb +57 -0
  55. data/spec/dummy/log/development.log +504 -0
  56. data/spec/dummy/log/test.log +29907 -0
  57. data/spec/dummy/script/rails +6 -0
  58. data/spec/dummy/tmp/cache/2E2/830/shard%2F2 +0 -0
  59. data/spec/dummy/tmp/cache/2E3/840/shard%2F3 +0 -0
  60. data/spec/dummy/tmp/cache/313/970/shard%2F30 +0 -0
  61. data/spec/dummy/tmp/cache/314/980/shard%2F31 +0 -0
  62. data/spec/dummy/tmp/cache/316/980/shard%2F15 +1 -0
  63. data/spec/dummy/tmp/cache/316/9D0/shard%2F60 +0 -0
  64. data/spec/dummy/tmp/cache/317/990/shard%2F16 +0 -0
  65. data/spec/dummy/tmp/cache/317/9C0/shard%2F43 +1 -0
  66. data/spec/dummy/tmp/cache/317/9E0/shard%2F61 +0 -0
  67. data/spec/dummy/tmp/cache/318/9A0/shard%2F17 +0 -0
  68. data/spec/dummy/tmp/cache/318/9D0/shard%2F44 +0 -0
  69. data/spec/dummy/tmp/cache/318/9F0/shard%2F62 +1 -0
  70. data/spec/dummy/tmp/cache/319/9E0/shard%2F45 +0 -0
  71. data/spec/dummy/tmp/cache/319/9F0/shard%2F54 +1 -0
  72. data/spec/dummy/tmp/cache/319/A10/shard%2F72 +1 -0
  73. data/spec/dummy/tmp/cache/319/A30/shard%2F90 +0 -0
  74. data/spec/dummy/tmp/cache/31B/9E0/shard%2F29 +1 -0
  75. data/spec/dummy/tmp/cache/321/AA0/shard%2F89 +0 -0
  76. data/spec/dummy/tmp/cache/322/AC0/shard%2F99 +1 -0
  77. data/spec/dummy/tmp/cache/344/D70/shard%2F103 +1 -0
  78. data/spec/dummy/tmp/cache/345/D80/shard%2F104 +0 -0
  79. data/spec/dummy/tmp/cache/345/DB0/shard%2F131 +1 -0
  80. data/spec/dummy/tmp/cache/345/DC0/shard%2F140 +0 -0
  81. data/spec/dummy/tmp/cache/346/D90/shard%2F105 +0 -0
  82. data/spec/dummy/tmp/cache/346/DB0/shard%2F123 +0 -0
  83. data/spec/dummy/tmp/cache/346/DD0/shard%2F222 +1 -0
  84. data/spec/dummy/tmp/cache/346/DE0/shard%2F150 +0 -0
  85. data/spec/dummy/tmp/cache/346/DF0/shard%2F240 +1 -0
  86. data/spec/dummy/tmp/cache/347/DA0/shard%2F106 +1 -0
  87. data/spec/dummy/tmp/cache/347/DC0/shard%2F124 +0 -0
  88. data/spec/dummy/tmp/cache/347/DC0/shard%2F205 +1 -0
  89. data/spec/dummy/tmp/cache/347/E10/shard%2F250 +1 -0
  90. data/spec/dummy/tmp/cache/348/DF0/shard%2F143 +1 -0
  91. data/spec/dummy/tmp/cache/348/DF0/shard%2F224 +1 -0
  92. data/spec/dummy/tmp/cache/348/E10/shard%2F161 +1 -0
  93. data/spec/dummy/tmp/cache/349/DD0/shard%2F117 +1 -0
  94. data/spec/dummy/tmp/cache/349/E40/shard%2F180 +1 -0
  95. data/spec/dummy/tmp/cache/34A/DF0/shard%2F127 +1 -0
  96. data/spec/dummy/tmp/cache/34A/DF0/shard%2F208 +1 -0
  97. data/spec/dummy/tmp/cache/34A/E10/shard%2F145 +1 -0
  98. data/spec/dummy/tmp/cache/34A/E60/shard%2F190 +1 -0
  99. data/spec/dummy/tmp/cache/34B/E30/shard%2F155 +1 -0
  100. data/spec/dummy/tmp/cache/34D/E30/shard%2F139 +0 -0
  101. data/spec/dummy/tmp/cache/34E/E50/shard%2F149 +0 -0
  102. data/spec/dummy/tmp/cache/353/EF0/shard%2F199 +1 -0
  103. data/spec/dummy/tmp/cache/3A4/E90/shard%2F10003 +1 -0
  104. data/spec/dummy/tmp/cache/3A5/ED0/shard%2F10031 +1 -0
  105. data/spec/dummy/tmp/cache/3A9/EF0/shard%2F10017 +1 -0
  106. data/spec/lib/active_record/association_spec.rb +305 -0
  107. data/spec/lib/active_record/attribute_methods_spec.rb +108 -0
  108. data/spec/lib/active_record/base_spec.rb +66 -0
  109. data/spec/lib/active_record/calculations_spec.rb +119 -0
  110. data/spec/lib/active_record/connection_handler_spec.rb +45 -0
  111. data/spec/lib/active_record/connection_pool_spec.rb +23 -0
  112. data/spec/lib/active_record/finder_methods_spec.rb +29 -0
  113. data/spec/lib/active_record/query_cache_spec.rb +20 -0
  114. data/spec/lib/active_record/query_methods_spec.rb +130 -0
  115. data/spec/lib/active_record/relation_spec.rb +38 -0
  116. data/spec/lib/cache_extensions_spec.rb +27 -0
  117. data/spec/lib/connection_pool_proxy_spec.rb +13 -0
  118. data/spec/lib/database_server_spec.rb +154 -0
  119. data/spec/lib/shackles_spec.rb +147 -0
  120. data/spec/models/shard_spec.rb +382 -0
  121. data/spec/spec_helper.rb +32 -0
  122. metadata +344 -0
@@ -0,0 +1,9 @@
1
+ class CreateSwitchmanShards < ActiveRecord::Migration
2
+ def change
3
+ create_table :switchman_shards do |t|
4
+ t.string :name
5
+ t.string :database_server_id
6
+ t.boolean :default, :default => false, :null => false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class CreateDefaultShard < ActiveRecord::Migration
2
+ def up
3
+ unless Switchman::Shard.default.is_a?(Switchman::Shard)
4
+ Switchman::Shard.reset_column_information
5
+ Switchman::Shard.create!(:default => true)
6
+ Switchman::Shard.default(true)
7
+ end
8
+ end
9
+ end
data/lib/switchman.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "shackles"
2
+ require "switchman/engine"
3
+
4
+ module Switchman
5
+ def self.config
6
+ # TODO: load from yaml
7
+ {}
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module Switchman
2
+ module ActiveRecord
3
+ module AbstractAdapter
4
+ attr_writer :shard
5
+
6
+ def shard
7
+ @shard || Shard.default
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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