switchman 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +10 -2
  3. data/app/models/switchman/shard.rb +234 -270
  4. data/app/models/switchman/unsharded_record.rb +7 -0
  5. data/db/migrate/20130328212039_create_switchman_shards.rb +1 -1
  6. data/db/migrate/20130328224244_create_default_shard.rb +5 -5
  7. data/db/migrate/20161206323434_add_back_default_string_limits_switchman.rb +1 -0
  8. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  9. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +7 -5
  10. data/db/migrate/20190114212900_add_unique_name_indexes.rb +5 -3
  11. data/lib/switchman.rb +3 -3
  12. data/lib/switchman/action_controller/caching.rb +2 -2
  13. data/lib/switchman/active_record/abstract_adapter.rb +1 -0
  14. data/lib/switchman/active_record/association.rb +78 -85
  15. data/lib/switchman/active_record/attribute_methods.rb +58 -52
  16. data/lib/switchman/active_record/base.rb +58 -59
  17. data/lib/switchman/active_record/calculations.rb +73 -66
  18. data/lib/switchman/active_record/connection_pool.rb +14 -41
  19. data/lib/switchman/active_record/database_configurations.rb +34 -0
  20. data/lib/switchman/active_record/database_configurations/database_config.rb +13 -0
  21. data/lib/switchman/active_record/finder_methods.rb +11 -16
  22. data/lib/switchman/active_record/log_subscriber.rb +4 -8
  23. data/lib/switchman/active_record/migration.rb +6 -15
  24. data/lib/switchman/active_record/model_schema.rb +1 -1
  25. data/lib/switchman/active_record/persistence.rb +4 -6
  26. data/lib/switchman/active_record/postgresql_adapter.rb +42 -53
  27. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  28. data/lib/switchman/active_record/query_cache.rb +18 -19
  29. data/lib/switchman/active_record/query_methods.rb +172 -181
  30. data/lib/switchman/active_record/reflection.rb +6 -10
  31. data/lib/switchman/active_record/relation.rb +27 -21
  32. data/lib/switchman/active_record/spawn_methods.rb +27 -29
  33. data/lib/switchman/active_record/statement_cache.rb +18 -35
  34. data/lib/switchman/active_record/tasks/database_tasks.rb +16 -0
  35. data/lib/switchman/active_support/cache.rb +3 -5
  36. data/lib/switchman/arel.rb +13 -8
  37. data/lib/switchman/database_server.rb +121 -142
  38. data/lib/switchman/default_shard.rb +52 -16
  39. data/lib/switchman/engine.rb +61 -57
  40. data/lib/switchman/environment.rb +4 -8
  41. data/lib/switchman/errors.rb +1 -0
  42. data/lib/switchman/guard_rail.rb +6 -19
  43. data/lib/switchman/guard_rail/relation.rb +5 -7
  44. data/lib/switchman/r_spec_helper.rb +29 -37
  45. data/lib/switchman/rails.rb +14 -12
  46. data/lib/switchman/schema_cache.rb +1 -1
  47. data/lib/switchman/sharded_instrumenter.rb +1 -1
  48. data/lib/switchman/standard_error.rb +15 -3
  49. data/lib/switchman/test_helper.rb +6 -10
  50. data/lib/switchman/version.rb +1 -1
  51. data/lib/tasks/switchman.rake +54 -69
  52. metadata +100 -44
  53. data/lib/switchman/active_record/batches.rb +0 -11
  54. data/lib/switchman/active_record/connection_handler.rb +0 -172
  55. data/lib/switchman/active_record/where_clause_factory.rb +0 -36
  56. data/lib/switchman/connection_pool_proxy.rb +0 -169
@@ -6,15 +6,18 @@ module Switchman
6
6
  module ActiveRecord
7
7
  module ConnectionPool
8
8
  def shard
9
- Thread.current[tls_key] || Shard.default
9
+ shard_stack.last || Shard.default
10
10
  end
11
11
 
12
- def shard=(value)
13
- Thread.current[tls_key] = value
12
+ def shard_stack
13
+ unless (shard_stack = Thread.current.thread_variable_get(tls_key))
14
+ shard_stack = Concurrent::Array.new
15
+ Thread.current.thread_variable_set(tls_key, shard_stack)
16
+ end
17
+ shard_stack
14
18
  end
15
19
 
16
20
  def default_schema
17
- raise "Not postgres!" unless self.spec.config[:adapter] == 'postgresql'
18
21
  connection unless @schemas
19
22
  # default shard will not switch databases immediately, so it won't be set yet
20
23
  @schemas ||= connection.current_schemas
@@ -22,39 +25,29 @@ module Switchman
22
25
  end
23
26
 
24
27
  def checkout_new_connection
25
- conn = synchronize do
26
- # ideally I would just keep a thread-local spec that I could modify
27
- # without locking anything, but if spec returns not-the-object passed
28
- # to initialize this pool, things break
29
- spec.config[:shard_name] = self.shard.name
30
-
31
- super
32
- end
33
- conn.shard = self.shard
28
+ conn = super
29
+ conn.shard = shard
34
30
  conn
35
31
  end
36
32
 
37
33
  def connection(switch_shard: true)
38
34
  conn = super()
39
35
  raise NonExistentShardError if shard.new_record?
40
- switch_database(conn) if conn.shard != self.shard && switch_shard
36
+
37
+ switch_database(conn) if conn.shard != shard && switch_shard
41
38
  conn
42
39
  end
43
40
 
44
41
  def release_connection(with_id = Thread.current)
45
42
  super(with_id)
46
43
 
47
- if spec.config[:idle_timeout]
48
- clear_idle_connections!(Time.now - spec.config[:idle_timeout].to_i)
49
- end
44
+ flush
50
45
  end
51
46
 
52
47
  def remove_shard!(shard)
53
48
  synchronize do
54
49
  # 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
50
+ self.shard = Shard.default if self.shard == shard
58
51
  # Update out any connections that may be using this shard
59
52
  @connections.each do |conn|
60
53
  # This will also update the connection's shard to the default shard
@@ -63,29 +56,9 @@ module Switchman
63
56
  end
64
57
  end
65
58
 
66
- def clear_idle_connections!(since_when)
67
- synchronize do
68
- @connections.reject! do |conn|
69
- if conn.last_query_at < since_when && !conn.in_use?
70
- conn.disconnect!
71
- true
72
- else
73
- false
74
- end
75
- end
76
- @available.clear
77
- @connections.each do |conn|
78
- @available.add conn
79
- end
80
- end
81
- end
82
-
83
59
  def switch_database(conn)
84
- if !@schemas && conn.adapter_name == 'PostgreSQL' && !self.shard.database_server.config[:shard_name]
85
- @schemas = conn.current_schemas
86
- end
60
+ @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !shard.database_server.config[:shard_name]
87
61
 
88
- spec.config[:shard_name] = self.shard.name
89
62
  conn.shard = shard
90
63
  end
91
64
 
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ private
7
+
8
+ # key difference: assumes a hybrid two-tier structure; each third tier
9
+ # is implicitly named, and their config is constructing by merging into
10
+ # its parent
11
+ def build_configs(configs)
12
+ return configs.configurations if configs.is_a?(DatabaseConfigurations)
13
+ return configs if configs.is_a?(Array)
14
+
15
+ db_configs = configs.flat_map do |env_name, config|
16
+ roles = config.keys.select { |k| config[k].is_a?(Hash) }
17
+ base_config = config.except(*roles)
18
+
19
+ name = "#{env_name}/primary"
20
+ name = 'primary' if env_name == default_env
21
+ base_db = build_db_config_from_raw_config(env_name, name, base_config)
22
+ [base_db] + roles.map do |role|
23
+ build_db_config_from_raw_config(env_name, "#{env_name}/#{role}",
24
+ base_config.merge(config[role]).merge(replica: true))
25
+ end
26
+ end
27
+
28
+ db_configs << environment_url_config(default_env, 'primary', {}) unless db_configs.find(&:for_current_env?)
29
+
30
+ merge_db_environment_variables(default_env, db_configs.compact)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module DatabaseConfigurations
6
+ module DatabaseConfig
7
+ def for_current_env?
8
+ true
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -7,19 +7,18 @@ module Switchman
7
7
  return super(id) unless klass.integral_id?
8
8
 
9
9
  if shard_source_value != :implicit
10
- current_shard = Shard.current(klass.shard_category)
11
- result = self.activate do |relation, shard|
10
+ current_shard = Shard.current(klass.connection_classes)
11
+ result = activate do |relation, shard|
12
12
  current_id = Shard.relative_id_for(id, current_shard, shard)
13
13
  # current_id will be nil for non-integral id
14
14
  next unless current_id
15
15
  # skip the shard if the object can't be on it. unless we're only looking at one shard;
16
16
  # we might be expecting a shadow object
17
- next if current_id > Shard::IDS_PER_SHARD && self.all_shards.length > 1
17
+ next if current_id > Shard::IDS_PER_SHARD && all_shards.length > 1
18
+
18
19
  relation.call_super(:find_one, FinderMethods, current_id)
19
20
  end
20
- if result.is_a?(Array)
21
- result = result.first
22
- end
21
+ result = result.first if result.is_a?(Array)
23
22
  # we may have skipped all shards
24
23
  raise_record_not_found_exception!(id, 0, 1) unless result
25
24
  return result
@@ -34,8 +33,8 @@ module Switchman
34
33
  end
35
34
 
36
35
  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)}
36
+ current_shard = Shard.current(klass.connection_classes)
37
+ ids = ids.map { |id| Shard.relative_id_for(id, current_shard, current_shard) }
39
38
  super(ids)
40
39
  end
41
40
 
@@ -45,14 +44,12 @@ module Switchman
45
44
 
46
45
  def exists?(conditions = :none)
47
46
  conditions = conditions.id if ::ActiveRecord::Base === conditions
48
- return false if !conditions
47
+ return false unless conditions
49
48
 
50
- relation = ::Rails.version >= "5.2" ?
51
- apply_join_dependency(eager_loading: false) :
52
- apply_join_dependency(self, construct_join_dependency)
49
+ relation = apply_join_dependency(eager_loading: false)
53
50
  return false if ::ActiveRecord::NullRelation === relation
54
51
 
55
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
52
+ relation = relation.except(:select, :order).select('1 AS one').limit(1)
56
53
 
57
54
  case conditions
58
55
  when Array, Hash
@@ -62,9 +59,7 @@ module Switchman
62
59
  end
63
60
 
64
61
  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)
62
+ return true if connection.select_value(shard_rel.arel, "#{name} Exists")
68
63
  end
69
64
  false
70
65
  end
@@ -14,20 +14,16 @@ module Switchman
14
14
 
15
15
  name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
16
16
  name = "CACHE #{name}" if payload[:cached]
17
- sql = payload[:sql].squeeze(' '.freeze)
17
+ sql = payload[:sql].squeeze(' ')
18
18
  binds = nil
19
19
  shard = payload[:shard]
20
20
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
21
21
 
22
22
  unless (payload[:binds] || []).empty?
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|
23
+ casted_params = type_casted_binds(payload[:type_casted_binds])
24
+ binds = ' ' + payload[:binds].zip(casted_params).map do |attr, value|
29
25
  render_bind(attr, value)
30
- }.inspect
26
+ end.inspect
31
27
  end
32
28
 
33
29
  name = colorize_payload_name(name, payload[:name])
@@ -4,34 +4,24 @@ module Switchman
4
4
  module ActiveRecord
5
5
  module Migration
6
6
  module Compatibility
7
- module V5_0
7
+ module V5_0 # rubocop:disable Naming/ClassAndModuleCamelCase
8
8
  def create_table(*args, **options)
9
- unless options.key?(:id)
10
- options[:id] = :bigserial
11
- end
12
- if block_given?
13
- super do |td|
14
- yield td
15
- end
16
- else
17
- super
18
- end
9
+ options[:id] = :bigserial unless options.key?(:id)
10
+ super
19
11
  end
20
12
  end
21
13
  end
22
14
 
23
15
  def connection
24
16
  conn = super
25
- if conn.shard != ::ActiveRecord::Base.connection_pool.current_pool.shard
26
- ::ActiveRecord::Base.connection_pool.current_pool.switch_database(conn)
27
- end
17
+ ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.connection_pool.shard
28
18
  conn
29
19
  end
30
20
  end
31
21
 
32
22
  module Migrator
33
23
  def generate_migrator_advisory_lock_id
34
- shard_name_hash = Zlib.crc32(Shard.current.name)
24
+ shard_name_hash = Zlib.crc32("#{Shard.current.id}:#{Shard.current.name}")
35
25
  ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
36
26
  end
37
27
  end
@@ -39,6 +29,7 @@ module Switchman
39
29
  module MigrationContext
40
30
  def migrations
41
31
  return @migrations if instance_variable_defined?(:@migrations)
32
+
42
33
  migrations_cache = Thread.current[:migrations_cache] ||= {}
43
34
  key = Digest::MD5.hexdigest(migration_files.sort.join(','))
44
35
  @migrations = migrations_cache[key] ||= super
@@ -6,7 +6,7 @@ module Switchman
6
6
  module ClassMethods
7
7
  def quoted_table_name
8
8
  @quoted_table_name ||= {}
9
- @quoted_table_name[Shard.current(shard_category).id] ||= connection.quote_table_name(table_name)
9
+ @quoted_table_name[Shard.current(connection_classes).id] ||= connection.quote_table_name(table_name)
10
10
  end
11
11
  end
12
12
  end
@@ -5,14 +5,12 @@ module Switchman
5
5
  module Persistence
6
6
  # touch reads the id attribute directly, so it's not relative to the current shard
7
7
  def touch(*, **)
8
- shard.activate(self.class.shard_category) { super }
8
+ shard.activate(self.class.connection_classes) { super }
9
9
  end
10
10
 
11
- if ::Rails.version >= '5.2'
12
- def update_columns(*)
13
- shard.activate(self.class.shard_category) { super }
14
- end
11
+ def update_columns(*)
12
+ shard.activate(self.class.connection_classes) { super }
15
13
  end
16
14
  end
17
15
  end
18
- end
16
+ end
@@ -9,22 +9,22 @@ module Switchman
9
9
 
10
10
  option_string = options.sum do |key, value|
11
11
  case key
12
- when :owner
13
- " OWNER = \"#{value}\""
14
- when :template
15
- " TEMPLATE = \"#{value}\""
16
- when :encoding
17
- " ENCODING = '#{value}'"
18
- when :collation
19
- " LC_COLLATE = '#{value}'"
20
- when :ctype
21
- " LC_CTYPE = '#{value}'"
22
- when :tablespace
23
- " TABLESPACE = \"#{value}\""
24
- when :connection_limit
25
- " CONNECTION LIMIT = #{value}"
26
- else
27
- ""
12
+ when :owner
13
+ " OWNER = \"#{value}\""
14
+ when :template
15
+ " TEMPLATE = \"#{value}\""
16
+ when :encoding
17
+ " ENCODING = '#{value}'"
18
+ when :collation
19
+ " LC_COLLATE = '#{value}'"
20
+ when :ctype
21
+ " LC_CTYPE = '#{value}'"
22
+ when :tablespace
23
+ " TABLESPACE = \"#{value}\""
24
+ when :connection_limit
25
+ " CONNECTION LIMIT = #{value}"
26
+ else
27
+ ''
28
28
  end
29
29
  end
30
30
 
@@ -37,10 +37,10 @@ module Switchman
37
37
  end
38
38
 
39
39
  def current_schemas
40
- select_values("SELECT * FROM unnest(current_schemas(false))")
40
+ select_values('SELECT * FROM unnest(current_schemas(false))')
41
41
  end
42
42
 
43
- def tables(name = nil)
43
+ def tables(_name = nil)
44
44
  query(<<-SQL, 'SCHEMA').map { |row| row[0] }
45
45
  SELECT tablename
46
46
  FROM pg_tables
@@ -50,18 +50,15 @@ module Switchman
50
50
 
51
51
  def extract_schema_qualified_name(string)
52
52
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
53
- if string && !name.schema
54
- name.instance_variable_set(:@schema, shard.name)
55
- end
53
+ name.instance_variable_set(:@schema, shard.name) if string && !name.schema
56
54
  [name.schema, name.identifier]
57
55
  end
58
56
 
59
57
  def view_exists?(name)
60
58
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
61
59
  return false unless name.identifier
62
- if !name.schema
63
- name.instance_variable_set(:@schema, shard.name)
64
- end
60
+
61
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
65
62
 
66
63
  select_values(<<-SQL, 'SCHEMA').any?
67
64
  SELECT c.relname
@@ -86,41 +83,37 @@ module Switchman
86
83
  ORDER BY i.relname
87
84
  SQL
88
85
 
89
-
90
86
  result.map do |row|
91
87
  index_name = row[0]
92
88
  unique = row[1] == true || row[1] == 't'
93
- indkey = row[2].split(" ")
89
+ indkey = row[2].split
94
90
  inddef = row[3]
95
91
  oid = row[4]
96
92
 
97
- columns = Hash[query(<<-SQL, "SCHEMA")]
93
+ columns = Hash[query(<<-SQL, 'SCHEMA')] # rubocop:disable Style/HashConversion
98
94
  SELECT a.attnum, a.attname
99
95
  FROM pg_attribute a
100
96
  WHERE a.attrelid = #{oid}
101
- AND a.attnum IN (#{indkey.join(",")})
97
+ AND a.attnum IN (#{indkey.join(',')})
102
98
  SQL
103
99
 
104
100
  column_names = columns.stringify_keys.values_at(*indkey).compact
105
101
 
106
- unless column_names.empty?
107
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
108
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
109
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
110
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
111
- using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
112
-
113
- if ::Rails.version >= "5.2"
114
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, orders: orders, where: where, using: using)
115
- else
116
- ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
117
- end
118
- end
102
+ next if column_names.empty?
103
+
104
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
105
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
106
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map { |order_column| [order_column, :desc] }] : {} # rubocop:disable Style/HashConversion
107
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
108
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
109
+
110
+ ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, index_name, unique, column_names,
111
+ orders: orders, where: where, using: using)
119
112
  end.compact
120
113
  end
121
114
 
122
115
  def index_name_exists?(table_name, index_name, _default = nil)
123
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
116
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i.positive?
124
117
  SELECT COUNT(*)
125
118
  FROM pg_class t
126
119
  INNER JOIN pg_index d ON t.oid = d.indrelid
@@ -141,14 +134,13 @@ module Switchman
141
134
 
142
135
  def quote_table_name(name)
143
136
  return quote_local_table_name(name) if @use_local_table_name
137
+
144
138
  name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
145
- if !name.schema
146
- name.instance_variable_set(:@schema, shard.name)
147
- end
139
+ name.instance_variable_set(:@schema, shard.name) unless name.schema
148
140
  name.quoted
149
141
  end
150
142
 
151
- def with_local_table_name(enable = true)
143
+ def with_local_table_name(enable = true) # rubocop:disable Style/OptionalBooleanParameter
152
144
  old_value = @use_local_table_name
153
145
  @use_local_table_name = enable
154
146
  yield
@@ -157,7 +149,6 @@ module Switchman
157
149
  end
158
150
 
159
151
  def foreign_keys(table_name)
160
-
161
152
  # mostly copy-pasted from AR - only change is to the nspname condition for qualified names support
162
153
  fk_info = select_all <<-SQL.strip_heredoc
163
154
  SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
@@ -186,18 +177,16 @@ module Switchman
186
177
  # strip the schema name from to_table if it matches
187
178
  to_table = row['to_table']
188
179
  to_table_qualified_name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(to_table)
189
- if to_table_qualified_name.schema == shard.name
190
- to_table = to_table_qualified_name.identifier
191
- end
180
+ to_table = to_table_qualified_name.identifier if to_table_qualified_name.schema == shard.name
192
181
 
193
182
  ::ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(table_name, to_table, options)
194
183
  end
195
184
  end
196
185
 
197
186
  def add_index_options(_table_name, _column_name, **)
198
- index_name, index_type, index_columns, index_options, algorithm, using = super
199
- algorithm = nil if DatabaseServer.creating_new_shard && algorithm == "CONCURRENTLY"
200
- [index_name, index_type, index_columns, index_options, algorithm, using]
187
+ index, algorithm, if_not_exists = super
188
+ algorithm = nil if DatabaseServer.creating_new_shard && algorithm == 'CONCURRENTLY'
189
+ [index, algorithm, if_not_exists]
201
190
  end
202
191
 
203
192
  def rename_table(table_name, new_name)