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.
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