switchman 1.5.21 → 2.1.5

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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/app/models/switchman/shard.rb +757 -11
  3. data/db/migrate/20130328212039_create_switchman_shards.rb +3 -1
  4. data/db/migrate/20130328224244_create_default_shard.rb +4 -2
  5. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +13 -0
  6. data/db/migrate/20180828183945_add_default_shard_index.rb +15 -0
  7. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +17 -0
  8. data/db/migrate/20190114212900_add_unique_name_indexes.rb +9 -0
  9. data/lib/switchman/action_controller/caching.rb +2 -0
  10. data/lib/switchman/active_record/abstract_adapter.rb +14 -4
  11. data/lib/switchman/active_record/association.rb +64 -37
  12. data/lib/switchman/active_record/attribute_methods.rb +54 -22
  13. data/lib/switchman/active_record/base.rb +76 -31
  14. data/lib/switchman/active_record/batches.rb +3 -1
  15. data/lib/switchman/active_record/calculations.rb +17 -22
  16. data/lib/switchman/active_record/connection_handler.rb +88 -78
  17. data/lib/switchman/active_record/connection_pool.rb +28 -23
  18. data/lib/switchman/active_record/finder_methods.rb +37 -28
  19. data/lib/switchman/active_record/log_subscriber.rb +14 -19
  20. data/lib/switchman/active_record/migration.rb +80 -0
  21. data/lib/switchman/active_record/model_schema.rb +3 -1
  22. data/lib/switchman/active_record/persistence.rb +9 -1
  23. data/lib/switchman/active_record/postgresql_adapter.rb +170 -126
  24. data/lib/switchman/active_record/predicate_builder.rb +3 -1
  25. data/lib/switchman/active_record/query_cache.rb +22 -87
  26. data/lib/switchman/active_record/query_methods.rb +139 -125
  27. data/lib/switchman/active_record/reflection.rb +42 -14
  28. data/lib/switchman/active_record/relation.rb +108 -33
  29. data/lib/switchman/active_record/spawn_methods.rb +2 -0
  30. data/lib/switchman/active_record/statement_cache.rb +44 -52
  31. data/lib/switchman/active_record/table_definition.rb +4 -2
  32. data/lib/switchman/active_record/type_caster.rb +2 -0
  33. data/lib/switchman/active_record/where_clause_factory.rb +5 -2
  34. data/lib/switchman/active_support/cache.rb +18 -0
  35. data/lib/switchman/arel.rb +8 -25
  36. data/lib/switchman/call_super.rb +19 -0
  37. data/lib/switchman/connection_pool_proxy.rb +70 -24
  38. data/lib/switchman/database_server.rb +69 -59
  39. data/lib/switchman/default_shard.rb +3 -0
  40. data/lib/switchman/engine.rb +44 -41
  41. data/lib/switchman/environment.rb +2 -0
  42. data/lib/switchman/errors.rb +2 -0
  43. data/lib/switchman/{shackles → guard_rail}/relation.rb +7 -5
  44. data/lib/switchman/{shackles.rb → guard_rail.rb} +6 -4
  45. data/lib/switchman/open4.rb +2 -0
  46. data/lib/switchman/r_spec_helper.rb +14 -8
  47. data/lib/switchman/rails.rb +2 -0
  48. data/lib/switchman/schema_cache.rb +17 -0
  49. data/lib/switchman/sharded_instrumenter.rb +4 -2
  50. data/lib/switchman/standard_error.rb +4 -2
  51. data/lib/switchman/test_helper.rb +7 -10
  52. data/lib/switchman/version.rb +3 -1
  53. data/lib/switchman.rb +5 -1
  54. data/lib/tasks/switchman.rake +53 -72
  55. metadata +84 -38
  56. data/app/models/switchman/shard_internal.rb +0 -692
@@ -1,19 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module StatementCache
4
6
  module ClassMethods
5
- def create(connection, block = Proc.new)
7
+ def create(connection, &block)
6
8
  relation = block.call ::ActiveRecord::StatementCache::Params.new
7
9
 
8
- binds = ::Rails.version >= '5' ? relation.bound_attributes : relation.bind_values
9
- bind_map = ::ActiveRecord::StatementCache::BindMap.new(binds)
10
- new relation.arel, bind_map
10
+ if ::Rails.version >= "5.2"
11
+ query_builder, binds = connection.cacheable_query(self, relation.arel)
12
+ bind_map = ::ActiveRecord::StatementCache::BindMap.new(binds)
13
+ new(relation.arel, bind_map, relation.klass)
14
+ else
15
+ bind_map = ::ActiveRecord::StatementCache::BindMap.new(relation.bound_attributes)
16
+ new relation.arel, bind_map
17
+ end
11
18
  end
12
19
  end
13
20
 
14
- def initialize(arel, bind_map)
21
+ def initialize(arel, bind_map, klass=nil)
15
22
  @arel = arel
16
23
  @bind_map = bind_map
24
+ @klass = klass
17
25
  @qualified_query_builders = {}
18
26
  end
19
27
 
@@ -22,7 +30,13 @@ module Switchman
22
30
  # we can make some assumptions about the shard source
23
31
  # (e.g. infer from the primary key or use the current shard)
24
32
 
25
- def execute(params, klass, connection)
33
+ def execute(*args)
34
+ if ::Rails.version >= '5.2'
35
+ params, connection = args
36
+ klass = @klass
37
+ else
38
+ params, klass, connection = args
39
+ end
26
40
  target_shard = nil
27
41
  if primary_index = bind_map.primary_value_index
28
42
  primary_value = params[primary_index]
@@ -34,65 +48,43 @@ module Switchman
34
48
  bind_values = bind_map.bind(params, current_shard, target_shard)
35
49
 
36
50
  target_shard.activate(klass.shard_category) do
37
- if connection.use_qualified_names?
38
- sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
39
- klass.find_by_sql(sql, bind_values)
40
- else
41
- sql = generic_query_builder(connection).sql_for(bind_values, connection)
42
- klass.find_by_sql(sql, bind_values)
43
- end
51
+ sql = qualified_query_builder(target_shard, klass).sql_for(bind_values, connection)
52
+ klass.find_by_sql(sql, bind_values)
44
53
  end
45
54
  end
46
55
 
47
- def generic_query_builder(connection)
48
- @query_builder ||= connection.cacheable_query(@arel)
49
- end
50
-
51
- def qualified_query_builder(shard, klass)
52
- @qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(@arel)
56
+ if ::Rails.version < '5.2'
57
+ def qualified_query_builder(shard, klass)
58
+ @qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(self.class, @arel)
59
+ end
60
+ else
61
+ def qualified_query_builder(shard, klass)
62
+ @qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(self.class, @arel).first
63
+ end
53
64
  end
54
65
 
55
66
  module BindMap
56
67
  # performs id transposition here instead of query_methods.rb
57
68
  def bind(values, current_shard, target_shard)
58
- if ::Rails.version >= '5'
59
- bas = @bound_attributes.dup
60
- @indexes.each_with_index do |offset,i|
61
- ba = bas[offset]
62
- if ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.sharded
63
- new_value = Shard.relative_id_for(values[i], current_shard, target_shard || current_shard)
64
- else
65
- new_value = values[i]
66
- end
67
- bas[offset] = ba.with_cast_value(new_value)
68
- end
69
- bas
70
- else
71
- bvs = @bind_values.map { |pair| pair.dup }
72
- @indexes.each_with_index do |offset,i|
73
- bv = bvs[offset]
74
- if bv[1].sharded
75
- bv[1] = Shard.relative_id_for(values[i], current_shard, target_shard || current_shard)
76
- else
77
- bv[1] = values[i]
78
- end
69
+ bas = @bound_attributes.dup
70
+ @indexes.each_with_index do |offset,i|
71
+ ba = bas[offset]
72
+ if ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.sharded
73
+ new_value = Shard.relative_id_for(values[i], current_shard, target_shard || current_shard)
74
+ else
75
+ new_value = values[i]
79
76
  end
80
- bvs
77
+ bas[offset] = ba.with_cast_value(new_value)
81
78
  end
79
+ bas
82
80
  end
83
81
 
84
82
  def primary_value_index
85
- if ::Rails.version >= '5'
86
- primary_ba_index = @bound_attributes.index do |ba|
87
- ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.primary
88
- end
89
- if primary_ba_index
90
- @indexes.index(primary_ba_index)
91
- end
92
- else
93
- if primary_bv_index = @bind_values.index{|col, sub| sub.primary}
94
- @indexes.index(primary_bv_index)
95
- end
83
+ primary_ba_index = @bound_attributes.index do |ba|
84
+ ba.is_a?(::ActiveRecord::Relation::QueryAttribute) && ba.value.primary
85
+ end
86
+ if primary_ba_index
87
+ @indexes.index(primary_ba_index)
96
88
  end
97
89
  end
98
90
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module TableDefinition
4
- def column(name, type, options = {})
5
- Engine.foreign_key_check(name, type, options)
6
+ def column(name, type, limit: nil, **)
7
+ Engine.foreign_key_check(name, type, limit: limit)
6
8
  super
7
9
  end
8
10
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module TypeCaster
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module WhereClauseFactory
@@ -19,9 +21,10 @@ module Switchman
19
21
  super
20
22
  when Hash, ::Arel::Nodes::Node
21
23
  where_clause = super
24
+ binds = ::Rails.version >= "5.2" ? nil : where_clause.binds
22
25
  predicates = where_clause.send(:predicates)
23
- @scope.send(:infer_shards_from_primary_key, predicates, where_clause.binds) if @scope.shard_source_value == :implicit && @scope.shard_value.is_a?(Shard)
24
- predicates = @scope.transpose_predicates(predicates, nil, @scope.primary_shard, false, where_clause.binds) if @scope.shard_source_value != :explicit
26
+ @scope.send(:infer_shards_from_primary_key, predicates, binds) if @scope.shard_source_value == :implicit && @scope.shard_value.is_a?(Shard)
27
+ predicates, _new_binds = @scope.transpose_predicates(predicates, nil, @scope.primary_shard, false, binds: binds)
25
28
  where_clause.instance_variable_set(:@predicates, predicates)
26
29
  where_clause
27
30
  else
@@ -1,13 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveSupport
3
5
  module Cache
4
6
  module ClassMethods
5
7
  def lookup_store(*store_options)
6
8
  store = super
9
+ # can't use defined?, because it's a _ruby_ autoloaded constant,
10
+ # so just checking that will cause it to get required
11
+ if store.class.name == "ActiveSupport::Cache::RedisCacheStore" && !::ActiveSupport::Cache::RedisCacheStore.ancestors.include?(RedisCacheStore)
12
+ ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
13
+ end
7
14
  store.options[:namespace] ||= lambda { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
8
15
  store
9
16
  end
10
17
  end
18
+
19
+ module RedisCacheStore
20
+ def clear(namespace: nil, **)
21
+ # RedisCacheStore tries to be smart and only clear the cache under your namespace, if you have one set
22
+ # unfortunately, it uses the keys command, which is extraordinarily inefficient in a large redis instance
23
+ # fortunately, we can assume we control the entire instance, because we set up the namespacing, so just
24
+ # always unset it temporarily for clear calls
25
+ namespace = nil
26
+ super
27
+ end
28
+ end
11
29
  end
12
30
  end
13
31
  end
@@ -1,35 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module Arel
3
5
  module Table
4
6
  def model
5
- if ::Rails.version >= '5'
6
- type_caster.model
7
- else
8
- engine
9
- end
7
+ type_caster.model
10
8
  end
11
9
  end
12
10
  module Visitors
13
11
  module ToSql
14
12
  def visit_Arel_Nodes_TableAlias *args
15
- if ::Rails.version < '4.2'
16
- o = args.shift
17
- "#{visit o.relation, *args} #{quote_local_table_name o.name}"
18
- else
19
- o, collector = args
20
- collector = visit o.relation, collector
21
- collector << " "
22
- collector << quote_local_table_name(o.name)
23
- end
13
+ o, collector = args
14
+ collector = visit o.relation, collector
15
+ collector << " "
16
+ collector << quote_local_table_name(o.name)
24
17
  end
25
18
 
26
19
  def visit_Arel_Attributes_Attribute *args
27
20
  o = args.first
28
21
  join_name = o.relation.table_alias || o.relation.name
29
22
  result = "#{quote_local_table_name join_name}.#{quote_column_name o.name}"
30
- unless ::Rails.version < '4.2'.freeze
31
- result = args.last << result
32
- end
23
+ result = args.last << result
33
24
  result
34
25
  end
35
26
 
@@ -38,14 +29,6 @@ module Switchman
38
29
  @connection.quote_local_table_name(name)
39
30
  end
40
31
  end
41
-
42
- module PostgreSQL
43
- # the only difference is to remove caching (which only applies to Arel < 6.0/AR < 4.2)
44
- def quote_table_name name
45
- return name if ::Arel::Nodes::SqlLiteral === name
46
- @connection.quote_table_name(name)
47
- end
48
- end
49
32
  end
50
33
  end
51
34
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module CallSuper
5
+ def super_method_above(method_name, above_module)
6
+ method = method(method_name)
7
+ last_owner = method.owner
8
+ while method.owner != above_module
9
+ method = method.super_method
10
+ raise "Could not find super method ``#{method_name}' for #{self.class}" if method.owner == last_owner
11
+ end
12
+ method.super_method
13
+ end
14
+
15
+ def call_super(method, above_module, *args, &block)
16
+ super_method_above(method, above_module).call(*args, &block)
17
+ end
18
+ end
19
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/schema_cache'
2
4
 
3
5
  module Switchman
@@ -10,7 +12,7 @@ module Switchman
10
12
  end
11
13
 
12
14
  class ConnectionPoolProxy
13
- delegate :spec, :connected?, :default_schema, :with_connection,
15
+ delegate :spec, :connected?, :default_schema, :with_connection, :query_cache_enabled, :active_connection?,
14
16
  :to => :current_pool
15
17
 
16
18
  attr_reader :category, :schema_cache
@@ -23,44 +25,51 @@ module Switchman
23
25
  @category = category
24
26
  @default_pool = default_pool
25
27
  @connection_pools = shard_connection_pools
26
- @schema_cache = SchemaCache.new(self)
28
+ @schema_cache = default_pool.get_schema_cache(nil) if ::Rails.version >= '6'
29
+ @schema_cache = SchemaCache.new(self) unless @schema_cache.is_a?(SchemaCache)
30
+ if ::Rails.version >= '6'
31
+ @default_pool.set_schema_cache(@schema_cache)
32
+ @connection_pools.each_value do |pool|
33
+ pool.set_schema_cache(@schema_cache)
34
+ end
35
+ end
27
36
  end
28
37
 
29
38
  def active_shard
30
39
  Shard.current(@category)
31
40
  end
32
41
 
33
- def active_shackles_environment
34
- ::Rails.env.test? ? :master : active_shard.database_server.shackles_environment
42
+ def active_guard_rail_environment
43
+ ::Rails.env.test? ? :primary : active_shard.database_server.guard_rail_environment
35
44
  end
36
45
 
37
46
  def current_pool
38
47
  current_active_shard = active_shard
39
- pool = self.default_pool if current_active_shard.database_server == Shard.default.database_server && active_shackles_environment == :master && (current_active_shard.default? || current_active_shard.database_server.shareable?)
48
+ pool = self.default_pool if current_active_shard.database_server == Shard.default.database_server && active_guard_rail_environment == :primary && (current_active_shard.default? || current_active_shard.database_server.shareable?)
40
49
  pool = @connection_pools[pool_key] ||= create_pool unless pool
41
50
  pool.shard = current_active_shard
42
51
  pool
43
52
  end
44
53
 
45
54
  def connections
46
- @connection_pools.values.map(&:connections).inject([], &:+)
55
+ connection_pools.map(&:connections).inject([], &:+)
47
56
  end
48
57
 
49
- def connection
58
+ def connection(switch_shard: true)
50
59
  pool = current_pool
51
60
  begin
52
- connection = pool.connection
53
- connection.instance_variable_set(:@schema_cache, @schema_cache)
61
+ connection = pool.connection(switch_shard: switch_shard)
62
+ connection.instance_variable_set(:@schema_cache, @schema_cache) unless ::Rails.version >= '6'
54
63
  connection
55
64
  rescue ConnectionError
56
- raise if active_shard.database_server == Shard.default.database_server && active_shackles_environment == :master
57
- configs = active_shard.database_server.config(active_shackles_environment)
65
+ raise if active_shard.database_server == Shard.default.database_server && active_guard_rail_environment == :primary
66
+ configs = active_shard.database_server.config(active_guard_rail_environment)
58
67
  raise unless configs.is_a?(Array)
59
68
  configs.each_with_index do |config, idx|
60
69
  pool = create_pool(config.dup)
61
70
  begin
62
71
  connection = pool.connection
63
- connection.instance_variable_set(:@schema_cache, @schema_cache)
72
+ connection.instance_variable_set(:@schema_cache, @schema_cache) unless ::Rails.version >= '6'
64
73
  rescue ConnectionError
65
74
  raise if idx == configs.length - 1
66
75
  next
@@ -71,22 +80,53 @@ module Switchman
71
80
  end
72
81
  end
73
82
 
74
- %w{release_connection disconnect! clear_reloadable_connections! verify_active_connections! clear_stale_cached_connections!}.each do |method|
75
- class_eval(<<-EOS)
83
+ def get_schema_cache(_connection)
84
+ @schema_cache
85
+ end
86
+
87
+ def set_schema_cache(cache)
88
+ @schema_cache.copy_values(cache)
89
+ end
90
+
91
+ %w{release_connection
92
+ disconnect!
93
+ flush!
94
+ clear_reloadable_connections!
95
+ verify_active_connections!
96
+ clear_stale_cached_connections!
97
+ enable_query_cache!
98
+ disable_query_cache! }.each do |method|
99
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
76
100
  def #{method}
77
- @connection_pools.values.each(&:#{method})
101
+ connection_pools.each(&:#{method})
78
102
  end
79
- EOS
103
+ RUBY
104
+ end
105
+
106
+ def discard!
107
+ # this breaks everything if i try to pass it onto the pools and i'm not sure why
108
+ end
109
+
110
+ def automatic_reconnect=(value)
111
+ connection_pools.each { |pool| pool.automatic_reconnect = value }
80
112
  end
81
113
 
82
114
  def clear_idle_connections!(since_when)
83
- @connection_pools.values.each { |pool| pool.clear_idle_connections!(since_when) }
115
+ connection_pools.each { |pool| pool.clear_idle_connections!(since_when) }
116
+ end
117
+
118
+ def remove_shard!(shard)
119
+ connection_pools.each { |pool| pool.remove_shard!(shard) }
84
120
  end
85
121
 
86
122
  protected
87
123
 
124
+ def connection_pools
125
+ (@connection_pools.values + [default_pool]).uniq
126
+ end
127
+
88
128
  def pool_key
89
- [active_shackles_environment,
129
+ [active_guard_rail_environment,
90
130
  active_shard.database_server.shareable? ? active_shard.database_server.pool_key : active_shard]
91
131
  end
92
132
 
@@ -94,7 +134,7 @@ module Switchman
94
134
  shard = active_shard
95
135
  unless config
96
136
  if shard != Shard.default
97
- config = shard.database_server.config(active_shackles_environment)
137
+ config = shard.database_server.config(active_guard_rail_environment)
98
138
  config = config.first if config.is_a?(Array)
99
139
  config = config.dup
100
140
  else
@@ -104,22 +144,28 @@ module Switchman
104
144
  # different models could be using different configs on the default
105
145
  # shard, and database server wouldn't know about that
106
146
  config = default_pool.spec.instance_variable_get(:@config)
107
- if config[active_shackles_environment].is_a?(Hash)
108
- config = config.merge(config[active_shackles_environment])
109
- elsif config[active_shackles_environment].is_a?(Array)
110
- config = config.merge(config[active_shackles_environment].first)
147
+ if config[active_guard_rail_environment].is_a?(Hash)
148
+ config = config.merge(config[active_guard_rail_environment])
149
+ elsif config[active_guard_rail_environment].is_a?(Array)
150
+ config = config.merge(config[active_guard_rail_environment].first)
111
151
  else
112
152
  config = config.dup
113
153
  end
114
154
  end
115
155
  end
116
- spec = ::ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(config, "#{config[:adapter]}_connection")
156
+ spec = ::ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(
157
+ default_pool.spec.name,
158
+ config,
159
+ "#{config[:adapter]}_connection"
160
+ )
117
161
  # unfortunately the AR code that does this require logic can't really be
118
162
  # called in isolation
119
163
  require "active_record/connection_adapters/#{config[:adapter]}_adapter"
120
164
 
121
165
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec).tap do |pool|
122
166
  pool.shard = shard
167
+ pool.set_schema_cache(@schema_cache) if ::Rails.version >= '6'
168
+ pool.enable_query_cache! if !@connection_pools.empty? && @connection_pools.first.last.query_cache_enabled
123
169
  end
124
170
  end
125
171
  end