switchman 2.0.7 → 3.0.0

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 +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 -89
  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 +5 -14
  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 +90 -48
  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,27 +4,17 @@ 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
@@ -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)