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,16 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module Calculations
4
- CALL_SUPER = Object.new.freeze
5
- private_constant :CALL_SUPER
6
6
 
7
7
  def pluck(*column_names)
8
- return super(*column_names[1..-1]) if column_names.first.equal?(CALL_SUPER)
9
8
  target_shard = Shard.current(klass.shard_category)
10
9
  shard_count = 0
11
10
  result = self.activate do |relation, shard|
12
11
  shard_count += 1
13
- results = relation.pluck(CALL_SUPER, *column_names)
12
+ results = relation.call_super(:pluck, Calculations, *column_names)
14
13
  if column_names.length > 1
15
14
  column_names.each_with_index do |column_name, idx|
16
15
  if klass.sharded_column?(column_name)
@@ -30,13 +29,12 @@ module Switchman
30
29
  result
31
30
  end
32
31
 
33
- def execute_simple_calculation(operation, column_name, distinct, super_method: false)
34
- return super(operation, column_name, distinct) if super_method
32
+ def execute_simple_calculation(operation, column_name, distinct)
35
33
  operation = operation.to_s.downcase
36
34
  if operation == "average"
37
35
  result = calculate_simple_average(column_name, distinct)
38
36
  else
39
- result = self.activate{ |relation| relation.send(:execute_simple_calculation, operation, column_name, distinct, super_method: true) }
37
+ result = self.activate{ |relation| relation.call_super(:execute_simple_calculation, Calculations, operation, column_name, distinct) }
40
38
  if result.is_a?(Array)
41
39
  case operation
42
40
  when "count", "sum"
@@ -53,7 +51,7 @@ module Switchman
53
51
 
54
52
  def calculate_simple_average(column_name, distinct)
55
53
  # See activerecord#execute_simple_calculation
56
- relation = reorder(nil)
54
+ relation = except(:order)
57
55
  column = aggregate_column(column_name)
58
56
  relation.select_values = [operation_over_aggregate_column(column, "average", distinct).as("average"),
59
57
  operation_over_aggregate_column(column, "count", distinct).as("count")]
@@ -61,12 +59,12 @@ module Switchman
61
59
  initial_results = relation.activate{ |rel| klass.connection.select_all(rel) }
62
60
  if initial_results.is_a?(Array)
63
61
  initial_results.each do |r|
64
- r["average"] = type_cast_calculated_value(r["average"], column_or_type_for(column_name), "average")
65
- r["count"] = type_cast_calculated_value(r["count"], column_or_type_for(column_name), "count")
62
+ r["average"] = type_cast_calculated_value(r["average"], type_for(column_name), "average")
63
+ r["count"] = type_cast_calculated_value(r["count"], type_for(column_name), "count")
66
64
  end
67
65
  result = initial_results.map{|r| r["average"] * r["count"]}.sum / initial_results.map{|r| r["count"]}.sum
68
66
  else
69
- result = type_cast_calculated_value(initial_results.first["average"], column_or_type_for(column_name), "average")
67
+ result = type_cast_calculated_value(initial_results.first["average"], type_for(column_name), "average")
70
68
  end
71
69
  result
72
70
  end
@@ -76,7 +74,7 @@ module Switchman
76
74
  opts = grouped_calculation_options(operation.to_s.downcase, column_name, distinct)
77
75
 
78
76
  relation = build_grouped_calculation_relation(opts)
79
- target_shard = Shard.current(:default)
77
+ target_shard = Shard.current(:primary)
80
78
 
81
79
  rows = relation.activate do |rel, shard|
82
80
  calculated_data = klass.connection.select_all(rel)
@@ -89,14 +87,14 @@ module Switchman
89
87
 
90
88
  calculated_data.map do |row|
91
89
  row[opts[:aggregate_alias]] = type_cast_calculated_value(
92
- row[opts[:aggregate_alias]], column_or_type_for(opts[:column_name]), opts[:operation])
90
+ row[opts[:aggregate_alias]], type_for(opts[:column_name]), opts[:operation])
93
91
  row['count'] = row['count'].to_i if opts[:operation] == 'average'
94
92
 
95
- opts[:group_columns].each do |aliaz, column_or_type, column_name|
93
+ opts[:group_columns].each do |aliaz, type, column_name|
96
94
  if opts[:associated] && (aliaz == opts[:group_aliases].first)
97
95
  row[aliaz] = key_records[Shard.relative_id_for(row[aliaz], shard, target_shard)]
98
96
  elsif column_name && @klass.sharded_column?(column_name)
99
- row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz], column_or_type), shard, target_shard)
97
+ row[aliaz] = Shard.relative_id_for(type_cast_calculated_value(row[aliaz], type), shard, target_shard)
100
98
  end
101
99
  end
102
100
  row
@@ -112,10 +110,6 @@ module Switchman
112
110
  field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
113
111
  end
114
112
 
115
- def column_or_type_for(field)
116
- ::Rails.version < '4.2' ? column_for(field) : type_for(field)
117
- end
118
-
119
113
  def grouped_calculation_options(operation, column_name, distinct)
120
114
  opts = {:operation => operation, :column_name => column_name, :distinct => distinct}
121
115
 
@@ -129,9 +123,10 @@ module Switchman
129
123
  group_fields = group_attrs
130
124
  end
131
125
 
132
- group_aliases = group_fields.map { |field| column_alias_for(field) }
126
+ # to_s is because Rails 5 returns a string but Rails 6 returns a symbol.
127
+ group_aliases = group_fields.map { |field| column_alias_for(field.downcase.to_s).to_s }
133
128
  group_columns = group_aliases.zip(group_fields).map { |aliaz, field|
134
- [aliaz, column_or_type_for(field), column_name_for(field)]
129
+ [aliaz, type_for(field), column_name_for(field)]
135
130
  }
136
131
  opts.merge!(:association => association, :associated => associated,
137
132
  :group_aliases => group_aliases, :group_columns => group_columns,
@@ -167,7 +162,7 @@ module Switchman
167
162
  'count', opts[:distinct]).as('count')
168
163
  end
169
164
 
170
- haves = ::Rails.version >= "5" ? having_clause.send(:predicates) : having_values
165
+ haves = having_clause.send(:predicates)
171
166
  select_values += select_values unless haves.empty?
172
167
  select_values.concat opts[:group_fields].zip(opts[:group_aliases]).map { |field,aliaz|
173
168
  if field.respond_to?(:as)
@@ -1,29 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/connection_pool_proxy'
2
4
 
3
5
  module Switchman
4
6
  module ActiveRecord
5
7
  module ConnectionHandler
6
- def self.make_sharing_automagic(config)
7
- key = config[:adapter] == 'postgresql' ? :schema_search_path : :database
8
-
8
+ def self.make_sharing_automagic(config, shard = Shard.current)
9
9
  # only load the shard name from the db if we have to
10
- if config[key] || !config[:shard_name]
10
+ if !config[:shard_name]
11
11
  # we may not be able to connect to this shard yet, cause it might be an empty database server
12
- shard_name = Shard.current.name rescue nil
12
+ shard = shard.call if shard.is_a?(Proc)
13
+ shard_name = shard.name rescue nil
13
14
  return unless shard_name
14
15
 
15
16
  config[:shard_name] ||= shard_name
16
17
  end
17
-
18
- if !config[key] || config[key] == shard_name
19
- # this may truncate the schema_search_path if it was not specified in database.yml
20
- # but that's what our old behavior was anyway, so I guess it's okay
21
- config[key] = '%{shard_name}'
22
- end
23
18
  end
24
19
 
25
- def establish_connection(owner, spec)
26
- super
20
+ def establish_connection(spec)
21
+ # Just skip establishing a sharded connection if sharding isn't loaded; we'll do it again later
22
+ # This only can happen when loading ActiveRecord::Base; after everything is loaded Shard will
23
+ # be defined and this will actually establish a connection
24
+ return unless defined?(Shard)
25
+ pool = super
27
26
 
28
27
  # this is the first place that the adapter would have been required; but now we
29
28
  # need this addition ASAP since it will be called when loading the default shard below
@@ -32,45 +31,48 @@ module Switchman
32
31
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
33
32
  end
34
33
 
35
- # AR3 uses the name, AR4 uses the model
36
- model = case owner
37
- when String
38
- owner.constantize
39
- when Class
40
- owner
41
- else
42
- raise "unknown owner #{owner}"
43
- end
44
- pool = owner_to_pool[owner.name]
45
-
46
34
  first_time = !Shard.instance_variable_get(:@default)
47
35
  if first_time
48
36
  # Have to cache the default shard before we insert sharding, otherwise the first access
49
37
  # to sharding will recurse onto itself trying to access column information
50
38
  Shard.default
51
39
 
40
+ config = pool.spec.config
52
41
  # automatically change config to allow for sharing connections with simple config
53
- ConnectionHandler.make_sharing_automagic(spec.config)
42
+ ConnectionHandler.make_sharing_automagic(config)
54
43
  ConnectionHandler.make_sharing_automagic(Shard.default.database_server.config)
55
44
 
56
- ::ActiveRecord::Base.configurations[::Rails.env] = spec.instance_variable_get(:@config).stringify_keys
45
+ if ::Rails.version < '6.0'
46
+ ::ActiveRecord::Base.configurations[::Rails.env] = config.stringify_keys
47
+ else
48
+ # Adopted from the deprecated code that currently lives in rails proper
49
+ remaining_configs = ::ActiveRecord::Base.configurations.configurations.reject { |db_config| db_config.env_name == ::Rails.env }
50
+ new_config = ::ActiveRecord::DatabaseConfigurations.new(::Rails.env => config.stringify_keys).configurations
51
+ new_configs = remaining_configs + new_config
52
+
53
+ ::ActiveRecord::Base.configurations = new_configs
54
+ end
55
+ else
56
+ # this is probably wrong now
57
+ Shard.default.remove_instance_variable(:@name) if Shard.default.instance_variable_defined?(:@name)
57
58
  end
58
- @shard_connection_pools ||= { [:master, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
59
59
 
60
- proxy = ConnectionPoolProxy.new(model.shard_category,
60
+ @shard_connection_pools ||= { [:primary, Shard.default.database_server.shareable? ? ::Rails.env : Shard.default] => pool}
61
+
62
+ category = pool.spec.name.to_sym
63
+ proxy = ConnectionPoolProxy.new(category,
61
64
  pool,
62
65
  @shard_connection_pools)
63
- owner_to_pool[owner.name] = proxy
64
- class_to_pool.clear
66
+ owner_to_pool[pool.spec.name] = proxy
65
67
 
66
68
  if first_time
67
- if Shard.default.database_server.config[:prefer_slave]
68
- Shard.default.database_server.shackle!
69
+ if Shard.default.database_server.config[:prefer_secondary]
70
+ Shard.default.database_server.guard!
69
71
  end
70
72
 
71
- if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:slave]
72
- Shard.default.database_server.shackle!
73
- Shard.default(true)
73
+ if Shard.default.is_a?(DefaultShard) && Shard.default.database_server.config[:secondary]
74
+ Shard.default.database_server.guard!
75
+ Shard.default(reload: true)
74
76
  end
75
77
  end
76
78
 
@@ -79,7 +81,7 @@ module Switchman
79
81
  # DON'T do it if we're not the current connection handler - that means
80
82
  # we're in the middle of switching environments, and we don't want to
81
83
  # establish a connection with incorrect settings
82
- if (model == ::ActiveRecord::Base || model == Shard) && self == ::ActiveRecord::Base.connection_handler && !first_time
84
+ if [:primary, :unsharded].include?(category) && self == ::ActiveRecord::Base.connection_handler && !first_time
83
85
  Shard.default(reload: true, with_fallback: true)
84
86
  proxy.disconnect!
85
87
  end
@@ -89,12 +91,15 @@ module Switchman
89
91
  if Shard.default.is_a?(Shard)
90
92
  DatabaseServer.all.each do |server|
91
93
  next if server == Shard.default.database_server
92
- shard = server.shards.where(:name => nil).first
93
- shard ||= Shard.new(:database_server => server)
94
- shard.activate do
95
- ConnectionHandler.make_sharing_automagic(server.config)
96
- ConnectionHandler.make_sharing_automagic(proxy.current_pool.spec.config)
94
+
95
+ shard = nil
96
+ shard_proc = -> do
97
+ shard ||= server.shards.where(:name => nil).first
98
+ shard ||= Shard.new(:database_server => server)
99
+ shard
97
100
  end
101
+ ConnectionHandler.make_sharing_automagic(server.config, shard_proc)
102
+ ConnectionHandler.make_sharing_automagic(proxy.current_pool.spec.config, shard_proc)
98
103
  end
99
104
  end
100
105
  # we may have established some connections above trying to infer the shard's name.
@@ -106,60 +111,65 @@ module Switchman
106
111
  proxy
107
112
  end
108
113
 
109
- def remove_connection(model)
110
- uninitialize_ar(model) if owner_to_pool[model.name].is_a?(ConnectionPoolProxy)
111
- result = super
112
- initialize_categories
113
- result
114
- end
114
+ def remove_connection(spec_name)
115
+ # also remove pools based on the same spec name that are for shard category purposes
116
+ # can't just use delete_if, because it's a Concurrent::Map, not a Hash
117
+ owner_to_pool.keys.each do |k|
118
+ next if k == spec_name
115
119
 
116
- def pool_for(owner)
117
- # copypasted from AR#ConnectionHandler other than proxy handling
120
+ v = owner_to_pool[k]
121
+ owner_to_pool.delete(k) if v.is_a?(ConnectionPoolProxy) && v.default_pool.spec.name == spec_name
122
+ end
118
123
 
119
- owner_to_pool.fetch(owner.name) {
120
- if ancestor_pool = pool_from_any_process_for(owner)
124
+ # unwrap the pool from inside a ConnectionPoolProxy
125
+ pool = owner_to_pool[spec_name]
126
+ owner_to_pool[spec_name] = pool.default_pool if pool.is_a?(ConnectionPoolProxy)
127
+
128
+ # now let Rails do its thing with the data type it expects
129
+ super
130
+ end
131
+
132
+ def retrieve_connection_pool(spec_name)
133
+ owner_to_pool.fetch(spec_name) do
134
+ if ancestor_pool = pool_from_any_process_for(spec_name)
121
135
  # A connection was established in an ancestor process that must have
122
136
  # subsequently forked. We can't reuse the connection, but we can copy
123
137
  # the specification and establish a new connection with it.
124
- if ancestor_pool.is_a?(ConnectionPoolProxy)
125
- establish_connection owner, ancestor_pool.default_pool.spec
138
+ spec = if ancestor_pool.is_a?(ConnectionPoolProxy)
139
+ ancestor_pool.default_pool.spec
126
140
  else
127
- establish_connection owner, ancestor_pool.spec
141
+ ancestor_pool.spec
142
+ end
143
+ # avoid copying "duplicate" pools that implement shard categories.
144
+ # they'll have a spec.name of primary, but a spec_name of something else, like unsharded
145
+ if spec.name == spec_name
146
+ pool = establish_connection(spec.to_hash)
147
+ pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
148
+ next pool
128
149
  end
129
- else
130
- owner_to_pool[owner.name] = nil
131
- end
132
- }
133
- end
134
-
135
- def retrieve_connection_pool(klass)
136
- class_to_pool[klass.name] ||= begin
137
- original_klass = klass
138
- until pool = pool_for(klass)
139
- klass = klass.superclass
140
- break unless klass <= Base
141
150
  end
142
151
 
143
- if pool.is_a?(ConnectionPoolProxy) && pool.category != original_klass.shard_category
144
- default_pool = pool.default_pool
145
- pool = nil
146
- class_to_pool.each_value { |p| pool = p if p.is_a?(ConnectionPoolProxy) &&
147
- p.category == original_klass.shard_category &&
148
- p.default_pool == default_pool }
149
- pool ||= ConnectionPoolProxy.new(original_klass.shard_category, default_pool, @shard_connection_pools)
152
+ if spec_name != "primary"
153
+ primary_pool = retrieve_connection_pool("primary")
154
+ if primary_pool.is_a?(ConnectionPoolProxy)
155
+ pool = ConnectionPoolProxy.new(spec_name.to_sym, primary_pool.default_pool, @shard_connection_pools)
156
+ pool.schema_cache.copy_references(primary_pool.schema_cache)
157
+ owner_to_pool[spec_name] = pool
158
+ else
159
+ primary_pool
160
+ end
161
+ else
162
+ owner_to_pool[spec_name] = nil
150
163
  end
151
-
152
- class_to_pool[original_klass.name] = pool
153
164
  end
154
165
  end
155
166
 
156
167
  def clear_idle_connections!(since_when)
157
- # TODO in rails 4.2+ s/connection_pools.values/connection_pool_list/
158
- connection_pools.values.each{ |pool| pool.clear_idle_connections!(since_when) }
168
+ connection_pool_list.each{ |pool| pool.clear_idle_connections!(since_when) }
159
169
  end
160
170
 
161
171
  def switchman_connection_pool_proxies
162
- class_to_pool.values.uniq
172
+ owner_to_pool.values.uniq.select{|p| p.is_a?(ConnectionPoolProxy)}
163
173
  end
164
174
 
165
175
  private
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'switchman/errors'
2
4
 
3
5
  module Switchman
4
6
  module ActiveRecord
5
7
  module ConnectionPool
6
8
  def shard
7
- Thread.current["#{object_id}_shard".to_sym] || Shard.default
9
+ Thread.current[tls_key] || Shard.default
8
10
  end
9
11
 
10
12
  def shard=(value)
11
- Thread.current["#{object_id}_shard".to_sym] = value
13
+ Thread.current[tls_key] = value
12
14
  end
13
15
 
14
16
  def default_schema
@@ -32,19 +34,14 @@ module Switchman
32
34
  conn
33
35
  end
34
36
 
35
- def connection
36
- conn = super
37
+ def connection(switch_shard: true)
38
+ conn = super()
37
39
  raise NonExistentShardError if shard.new_record?
38
- switch_database(conn) if conn.shard != self.shard
40
+ switch_database(conn) if conn.shard != self.shard && switch_shard
39
41
  conn
40
42
  end
41
43
 
42
- def release_connection(with_id = nil)
43
- with_id ||= if ::Rails.version >= '5'
44
- Thread.current
45
- else
46
- current_connection_id
47
- end
44
+ def release_connection(with_id = Thread.current)
48
45
  super(with_id)
49
46
 
50
47
  if spec.config[:idle_timeout]
@@ -52,6 +49,20 @@ module Switchman
52
49
  end
53
50
  end
54
51
 
52
+ def remove_shard!(shard)
53
+ synchronize do
54
+ # The shard might be currently active, so we need to update our own shard
55
+ if self.shard == shard
56
+ self.shard = Shard.default
57
+ end
58
+ # Update out any connections that may be using this shard
59
+ @connections.each do |conn|
60
+ # This will also update the connection's shard to the default shard
61
+ switch_database(conn) if conn.shard == shard
62
+ end
63
+ end
64
+ end
65
+
55
66
  def clear_idle_connections!(since_when)
56
67
  synchronize do
57
68
  @connections.reject! do |conn|
@@ -75,20 +86,14 @@ module Switchman
75
86
  end
76
87
 
77
88
  spec.config[:shard_name] = self.shard.name
78
- case conn.adapter_name
79
- when 'MySQL', 'Mysql2'
80
- conn.execute("USE #{spec.config[:database]}")
81
- when 'PostgreSQL'
82
- if conn.schema_search_path != spec.config[:schema_search_path]
83
- conn.schema_search_path = spec.config[:schema_search_path]
84
- end
85
- when 'SQLite'
86
- # This is an artifact of the adapter modifying the path to be an absolute path when it is instantiated; just let it slide
87
- else
88
- raise("Cannot switch databases on same DatabaseServer with adapter type: #{conn.adapter_name}. Limit one Shard per DatabaseServer.")
89
- end
90
89
  conn.shard = shard
91
90
  end
91
+
92
+ private
93
+
94
+ def tls_key
95
+ "#{object_id}_shard".to_sym
96
+ end
92
97
  end
93
98
  end
94
99
  end
@@ -1,31 +1,44 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module FinderMethods
4
6
  def find_one(id)
7
+ return super(id) unless klass.integral_id?
8
+
5
9
  if shard_source_value != :implicit
6
- return self.activate { super(Shard.relative_id_for(id, Shard.current(klass.shard_category), primary_shard)) }
10
+ current_shard = Shard.current(klass.shard_category)
11
+ result = self.activate do |relation, shard|
12
+ current_id = Shard.relative_id_for(id, current_shard, shard)
13
+ # current_id will be nil for non-integral id
14
+ next unless current_id
15
+ # skip the shard if the object can't be on it. unless we're only looking at one shard;
16
+ # we might be expecting a shadow object
17
+ next if current_id > Shard::IDS_PER_SHARD && self.all_shards.length > 1
18
+ relation.call_super(:find_one, FinderMethods, current_id)
19
+ end
20
+ if result.is_a?(Array)
21
+ result = result.first
22
+ end
23
+ # we may have skipped all shards
24
+ raise_record_not_found_exception!(id, 0, 1) unless result
25
+ return result
7
26
  end
8
27
 
9
28
  local_id, shard = Shard.local_id_for(id)
10
29
  if shard
11
- if ::Rails.version < '4.2'
12
- # find_one uses binds, so we can't depend on QueryMethods
13
- # catching it
14
- begin
15
- old_shard_value = shard_value
16
- self.shard_value = shard
17
- super(local_id)
18
- ensure
19
- self.shard_value = old_shard_value
20
- end
21
- else
22
- shard.activate { super(local_id) }
23
- end
30
+ shard.activate { super(local_id) }
24
31
  else
25
- super
32
+ super(id)
26
33
  end
27
34
  end
28
35
 
36
+ def find_some_ordered(ids)
37
+ current_shard = Shard.current(klass.shard_category)
38
+ ids = ids.map{|id| Shard.relative_id_for(id, current_shard, current_shard)}
39
+ super(ids)
40
+ end
41
+
29
42
  def find_or_instantiator_by_attributes(match, attributes, *args)
30
43
  primary_shard.activate { super }
31
44
  end
@@ -34,13 +47,10 @@ module Switchman
34
47
  conditions = conditions.id if ::ActiveRecord::Base === conditions
35
48
  return false if !conditions
36
49
 
37
- if ::Rails.version >= '4.1'
38
- relation = apply_join_dependency(self, construct_join_dependency)
39
- return false if ::ActiveRecord::NullRelation === relation
40
- else
41
- join_dependency = construct_join_dependency_for_association_find
42
- relation = construct_relation_for_association_find(join_dependency)
43
- end
50
+ relation = ::Rails.version >= "5.2" ?
51
+ apply_join_dependency(eager_loading: false) :
52
+ apply_join_dependency(self, construct_join_dependency)
53
+ return false if ::ActiveRecord::NullRelation === relation
44
54
 
45
55
  relation = relation.except(:select, :order).select("1 AS one").limit(1)
46
56
 
@@ -51,12 +61,11 @@ module Switchman
51
61
  relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
52
62
  end
53
63
 
54
- args = [relation, "#{name} Exists"]
55
- args << relation.bind_values if ::Rails.version >= '4.1'
56
- activate { return true if connection.select_value(*args) }
57
- false
58
- rescue
59
- raise if ::Rails.version >= '4.1' || !(::ActiveRecord::ThrowResult === $!)
64
+ relation.activate do |shard_rel|
65
+ return true if ::Rails.version >= "5.2" ?
66
+ connection.select_value(shard_rel.arel, "#{name} Exists") :
67
+ connection.select_value(shard_rel, "#{name} Exists", shard_rel.bound_attributes)
68
+ end
60
69
  false
61
70
  end
62
71
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Switchman
2
4
  module ActiveRecord
3
5
  module LogSubscriber
@@ -8,35 +10,28 @@ module Switchman
8
10
 
9
11
  payload = event.payload
10
12
 
11
- return if 'SCHEMA'.freeze == payload[:name]
13
+ return if ::ActiveRecord::LogSubscriber::IGNORE_PAYLOAD_NAMES.include?(payload[:name])
12
14
 
13
- name = '%s (%.1fms)'.freeze % [payload[:name], event.duration]
15
+ name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
+ name = "CACHE #{name}" if payload[:cached]
14
17
  sql = payload[:sql].squeeze(' '.freeze)
15
18
  binds = nil
16
19
  shard = payload[:shard]
17
20
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
18
21
 
19
22
  unless (payload[:binds] || []).empty?
20
- binds = " " + payload[:binds].map { |col,v|
21
- if col
22
- [col.name, v]
23
- else
24
- [nil, v]
25
- end
23
+ use_old_format = (::Rails.version < '5.1.5')
24
+ args = use_old_format ?
25
+ [payload[:binds], payload[:type_casted_binds]] :
26
+ [payload[:type_casted_binds]]
27
+ casted_params = type_casted_binds(*args)
28
+ binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
29
+ render_bind(attr, value)
26
30
  }.inspect
27
31
  end
28
32
 
29
- if ::Rails.version >= '5'
30
- name = colorize_payload_name(name, payload[:name])
31
- sql = color(sql, sql_color(sql), true)
32
- else
33
- if odd?
34
- name = color(name, self.class::CYAN, true)
35
- sql = color(sql, nil, true)
36
- else
37
- name = color(name, self.class::MAGENTA, true)
38
- end
39
- end
33
+ name = colorize_payload_name(name, payload[:name])
34
+ sql = color(sql, sql_color(sql), true)
40
35
 
41
36
  debug " #{name} #{sql}#{binds}#{shard}"
42
37
  end