switchman 1.14.10 → 1.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -30,17 +30,14 @@ module Switchman
30
30
  end
31
31
 
32
32
  module CollectionAssociation
33
- method = ::Rails.version < '5.1' ? :get_records : :find_target
34
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
35
- def #{method}
36
- shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
37
- # activate both the owner and the target's shard category, so that Reflection#join_id_for,
38
- # when called for the owner, will be returned relative to shard the query will execute on
39
- Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
40
- super
41
- end
33
+ def find_target
34
+ shards = reflection.options[:multishard] && owner.respond_to?(:associated_shards) ? owner.associated_shards : [shard]
35
+ # activate both the owner and the target's shard category, so that Reflection#join_id_for,
36
+ # when called for the owner, will be returned relative to shard the query will execute on
37
+ Shard.with_each_shard(shards, [klass.shard_category, owner.class.shard_category].uniq) do
38
+ super
42
39
  end
43
- RUBY
40
+ end
44
41
  end
45
42
 
46
43
  module BelongsToAssociation
@@ -24,6 +24,10 @@ module Switchman
24
24
  end
25
25
 
26
26
  def establish_connection(spec)
27
+ # Just skip establishing a sharded connection if sharding isn't loaded; we'll do it again later
28
+ # This only can happen when loading ActiveRecord::Base; after everything is loaded Shard will
29
+ # be defined and this will actually establish a connection
30
+ return unless defined?(Shard)
27
31
  pool = super
28
32
 
29
33
  # this is the first place that the adapter would have been required; but now we
@@ -39,14 +43,12 @@ module Switchman
39
43
  # to sharding will recurse onto itself trying to access column information
40
44
  Shard.default
41
45
 
46
+ config = pool.spec.config
42
47
  # automatically change config to allow for sharing connections with simple config
43
- config = ::Rails.version < '5.1' ? spec.config : pool.spec.config
44
48
  ConnectionHandler.make_sharing_automagic(config)
45
49
  ConnectionHandler.make_sharing_automagic(Shard.default.database_server.config)
46
50
 
47
- if ::Rails.version < '5.1'
48
- ::ActiveRecord::Base.configurations[::Rails.env] = spec.instance_variable_get(:@config).stringify_keys
49
- elsif ::Rails.version < '6.0'
51
+ if ::Rails.version < '6.0'
50
52
  ::ActiveRecord::Base.configurations[::Rails.env] = config.stringify_keys
51
53
  else
52
54
  # Adopted from the deprecated code that currently lives in rails proper
@@ -132,8 +134,7 @@ module Switchman
132
134
  else
133
135
  ancestor_pool.spec
134
136
  end
135
- spec = spec.to_hash if ::Rails.version >= '5.1'
136
- pool = establish_connection spec
137
+ pool = establish_connection(spec.to_hash)
137
138
  pool.instance_variable_set(:@schema_cache, ancestor_pool.schema_cache) if ancestor_pool.schema_cache
138
139
  pool
139
140
  elsif spec_name != "primary"
@@ -18,18 +18,14 @@ module Switchman
18
18
  shard = " [#{shard[:database_server_id]}:#{shard[:id]} #{shard[:env]}]" if shard
19
19
 
20
20
  unless (payload[:binds] || []).empty?
21
- if ::Rails.version < '5.0.3'
22
- binds = " " + payload[:binds].map { |attr| render_bind(attr) }.inspect
23
- else
24
- use_old_format = (::Rails.version < '5.1') ? (::Rails.version < '5.0.7') : (::Rails.version < '5.1.5')
25
- args = use_old_format ?
26
- [payload[:binds], payload[:type_casted_binds]] :
27
- [payload[:type_casted_binds]]
28
- casted_params = type_casted_binds(*args)
29
- binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
30
- render_bind(attr, value)
31
- }.inspect
32
- end
21
+ use_old_format = (::Rails.version < '5.1.5')
22
+ args = use_old_format ?
23
+ [payload[:binds], payload[:type_casted_binds]] :
24
+ [payload[:type_casted_binds]]
25
+ casted_params = type_casted_binds(*args)
26
+ binds = " " + payload[:binds].zip(casted_params).map { |attr, value|
27
+ render_bind(attr, value)
28
+ }.inspect
33
29
  end
34
30
 
35
31
  name = colorize_payload_name(name, payload[:name])
@@ -52,31 +52,12 @@ module Switchman
52
52
  SQL
53
53
  end
54
54
 
55
- if ::Rails.version >= '5.1'
56
- def extract_schema_qualified_name(string)
57
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
58
- if string && !name.schema && use_qualified_names?
59
- name.instance_variable_set(:@schema, shard.name)
60
- end
61
- [name.schema, name.identifier]
62
- end
63
- else
64
- def data_source_exists?(name)
65
- name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(name.to_s)
66
- return false unless name.identifier
67
- if !name.schema && use_qualified_names?
68
- name.instance_variable_set(:@schema, shard.name)
69
- end
70
-
71
- exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
72
- SELECT COUNT(*)
73
- FROM pg_class c
74
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
75
- WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
76
- AND c.relname = '#{name.identifier}'
77
- AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
78
- SQL
55
+ def extract_schema_qualified_name(string)
56
+ name = ::ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name(string.to_s)
57
+ if string && !name.schema && use_qualified_names?
58
+ name.instance_variable_set(:@schema, shard.name)
79
59
  end
60
+ [name.schema, name.identifier]
80
61
  end
81
62
 
82
63
  def view_exists?(name)
@@ -1,122 +1,32 @@
1
1
  module Switchman
2
2
  module ActiveRecord
3
3
  module QueryCache
4
- if ::Rails.version < '5.0.1'
5
- # thread local accessors to replace @query_cache_enabled
6
- def query_cache
7
- thread_cache = Thread.current[:query_cache] ||= {}
8
- thread_cache[self.object_id] ||= Hash.new { |h,sql| h[sql] = {} }
9
- end
10
-
11
- def query_cache_enabled
12
- Thread.current[:query_cache_enabled]
13
- end
14
-
15
- def query_cache_enabled=(value)
16
- Thread.current[:query_cache_enabled] = value
17
- end
18
-
19
- # basically wholesale repeat of the methods from the original (see
20
- # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb),
21
- # but with self.query_cache_enabled and self.query_cache_enabled= instead
22
- # of @query_cache_enabled.
23
-
24
- def enable_query_cache!
25
- self.query_cache_enabled = true
26
- end
27
-
28
- def disable_query_cache!
29
- self.query_cache_enabled = false
30
- end
31
-
32
- def cache
33
- old, self.query_cache_enabled = query_cache_enabled, true
34
- yield
35
- ensure
36
- self.query_cache_enabled = old
37
- clear_query_cache unless self.query_cache_enabled
38
- end
39
-
40
- def uncached
41
- old, self.query_cache_enabled = query_cache_enabled, false
42
- yield
43
- ensure
44
- self.query_cache_enabled = old
45
- end
46
-
47
- def clear_query_cache
48
- Thread.current[:query_cache]&.clear
49
- end
50
-
51
- def select_all(arel, name = nil, binds = [], preparable: nil)
52
- if self.query_cache_enabled && !locked?(arel)
53
- arel, binds = binds_from_relation(arel, binds)
54
- sql = to_sql(arel, binds)
55
- cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
56
- else
57
- super
58
- end
59
- end
60
-
61
- # no reason to define these on the including class directly. the super
62
- # works just as well from a method on the included module
63
- [:insert, :update, :delete].each do |method_name|
64
- class_eval <<-end_code, __FILE__, __LINE__ + 1
65
- def #{method_name}(*args)
66
- clear_query_cache if self.query_cache_enabled
67
- super
68
- end
69
- end_code
70
- end
71
- end
72
4
 
73
5
  private
74
6
 
75
- if ::Rails.version < '5.1'
76
- def cache_sql(sql, binds)
77
- # have to include the shard id in the cache key because of switching dbs on the same connection
78
- sql = "#{self.shard.id}::#{sql}"
7
+ def cache_sql(sql, name, binds)
8
+ # have to include the shard id in the cache key because of switching dbs on the same connection
9
+ sql = "#{self.shard.id}::#{sql}"
10
+ @lock.synchronize do
79
11
  result =
80
12
  if query_cache[sql].key?(binds)
81
- args = {:sql => sql, :binds => binds, :name => "CACHE", :connection_id => object_id}
82
- args[:type_casted_binds] = -> { type_casted_binds(binds) } if ::Rails.version >= '5.0.7'
83
- ::ActiveSupport::Notifications.instrument("sql.active_record", args)
13
+ args = {
14
+ sql: sql,
15
+ binds: binds,
16
+ name: name,
17
+ connection_id: object_id,
18
+ cached: true
19
+ }
20
+ args[:type_casted_binds] = -> { type_casted_binds(binds) } if ::Rails.version >= '5.1.5'
21
+ ::ActiveSupport::Notifications.instrument(
22
+ "sql.active_record",
23
+ args
24
+ )
84
25
  query_cache[sql][binds]
85
26
  else
86
27
  query_cache[sql][binds] = yield
87
28
  end
88
-
89
- if ::ActiveRecord::Result === result
90
- result.dup
91
- else
92
- result.collect { |row| row.dup }
93
- end
94
- end
95
- else
96
- def cache_sql(sql, name, binds)
97
- # have to include the shard id in the cache key because of switching dbs on the same connection
98
- sql = "#{self.shard.id}::#{sql}"
99
- @lock.synchronize do
100
- result =
101
- if query_cache[sql].key?(binds)
102
- args = {
103
- sql: sql,
104
- binds: binds,
105
- name: name,
106
- connection_id: object_id,
107
- cached: true
108
- }
109
- args[:type_casted_binds] = -> { type_casted_binds(binds) } if ::Rails.version >= '5.1.5'
110
- ::ActiveSupport::Notifications.instrument(
111
- "sql.active_record",
112
- args
113
- )
114
- query_cache[sql][binds]
115
- else
116
- query_cache[sql][binds] = yield
117
- end
118
- result.dup
119
- end
29
+ result.dup
120
30
  end
121
31
  end
122
32
  end
@@ -56,15 +56,7 @@ module Switchman
56
56
  end
57
57
  end
58
58
 
59
- if ::Rails.version < '5.1'
60
- def generic_query_builder(connection)
61
- @query_builder ||= connection.cacheable_query(@arel)
62
- end
63
-
64
- def qualified_query_builder(shard, klass)
65
- @qualified_query_builders[shard.id] ||= klass.connection.cacheable_query(@arel)
66
- end
67
- elsif ::Rails.version < '5.2'
59
+ if ::Rails.version < '5.2'
68
60
  def generic_query_builder(connection)
69
61
  @query_builder ||= connection.cacheable_query(self.class, @arel)
70
62
  end
@@ -159,9 +159,7 @@ module Switchman
159
159
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec).tap do |pool|
160
160
  pool.shard = shard
161
161
  pool.set_schema_cache(@schema_cache) if ::Rails.version >= '6'
162
- if ::Rails.version >= '5.0.1'
163
- pool.enable_query_cache! if !@connection_pools.empty? && @connection_pools.first.last.query_cache_enabled
164
- end
162
+ pool.enable_query_cache! if !@connection_pools.empty? && @connection_pools.first.last.query_cache_enabled
165
163
  end
166
164
  end
167
165
  end
@@ -88,7 +88,7 @@ module Switchman
88
88
  require "switchman/call_super"
89
89
  require "switchman/rails"
90
90
  require "switchman/shackles/relation"
91
- require_dependency "switchman/shard_internal"
91
+ require_dependency "switchman/shard"
92
92
  require "switchman/standard_error"
93
93
 
94
94
  ::StandardError.include(StandardError)
@@ -118,11 +118,6 @@ module Switchman
118
118
  ::ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend(ActiveRecord::ConnectionHandler)
119
119
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
120
120
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
121
- # when we call super in Switchman::ActiveRecord::QueryCache#select_all,
122
- # we want it to find the definition from
123
- # ActiveRecord::ConnectionAdapters::DatabaseStatements, not
124
- # ActiveRecord::ConnectionAdapters::QueryCache
125
- ::ActiveRecord::ConnectionAdapters::QueryCache.send(:remove_method, :select_all) if ::Rails.version < '5.0.1'
126
121
 
127
122
  ::ActiveRecord::LogSubscriber.prepend(ActiveRecord::LogSubscriber)
128
123
  ::ActiveRecord::Migration.prepend(ActiveRecord::Migration)
@@ -173,6 +168,12 @@ module Switchman
173
168
  require "switchman/active_record/postgresql_adapter"
174
169
  ::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.prepend(ActiveRecord::PostgreSQLAdapter)
175
170
  end
171
+
172
+ # If Switchman::Shard wasn't loaded as of when ActiveRecord::Base initialized
173
+ # establish a connection here instead
174
+ if !Shard.instance_variable_get(:@default)
175
+ ::ActiveRecord::Base.establish_connection
176
+ end
176
177
  end
177
178
  end
178
179
 
@@ -121,7 +121,7 @@ module Switchman
121
121
  klass.before do
122
122
  raise "Sharding did not set up correctly" if @@sharding_failed
123
123
  Shard.clear_cache
124
- if ::Rails.version >= '5.1' ? use_transactional_tests : (use_transactional_tests || use_transactional_fixtures)
124
+ if use_transactional_tests
125
125
  Shard.default(true)
126
126
  @shard1 = Shard.find(@shard1.id)
127
127
  @shard2 = Shard.find(@shard2.id)
@@ -137,7 +137,7 @@ module Switchman
137
137
 
138
138
  klass.after do
139
139
  next if @@sharding_failed
140
- if ::Rails.version >= '5.1' ? use_transactional_tests : (use_transactional_tests || use_transactional_fixtures)
140
+ if use_transactional_tests
141
141
  shards = [@shard2]
142
142
  shards << @shard1 unless @shard1.database_server == Shard.default.database_server
143
143
  shards.each do |shard|
@@ -1,3 +1,3 @@
1
1
  module Switchman
2
- VERSION = "1.14.10"
2
+ VERSION = "1.15.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.14.10
4
+ version: 1.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-03-31 00:00:00.000000000 Z
13
+ date: 2020-05-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: railties
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: '5.0'
21
+ version: '5.1'
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
24
  version: '6.1'
@@ -28,7 +28,7 @@ dependencies:
28
28
  requirements:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: '5.0'
31
+ version: '5.1'
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
34
  version: '6.1'
@@ -38,7 +38,7 @@ dependencies:
38
38
  requirements:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: '5.0'
41
+ version: '5.1'
42
42
  - - "<"
43
43
  - !ruby/object:Gem::Version
44
44
  version: '6.1'
@@ -48,7 +48,7 @@ dependencies:
48
48
  requirements:
49
49
  - - ">="
50
50
  - !ruby/object:Gem::Version
51
- version: '5.0'
51
+ version: '5.1'
52
52
  - - "<"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '6.1'
@@ -173,7 +173,6 @@ extra_rdoc_files: []
173
173
  files:
174
174
  - Rakefile
175
175
  - app/models/switchman/shard.rb
176
- - app/models/switchman/shard_internal.rb
177
176
  - db/migrate/20130328212039_create_switchman_shards.rb
178
177
  - db/migrate/20130328224244_create_default_shard.rb
179
178
  - db/migrate/20161206323434_add_back_default_string_limits_switchman.rb
@@ -1,718 +0,0 @@
1
- require 'switchman/database_server'
2
- require 'switchman/default_shard'
3
- require 'switchman/environment'
4
- require 'switchman/errors'
5
-
6
- module Switchman
7
- class Shard < ::ActiveRecord::Base
8
- # ten trillion possible ids per shard. yup.
9
- IDS_PER_SHARD = 10_000_000_000_000
10
-
11
- CATEGORIES =
12
- {
13
- # special cased to mean all other models
14
- :primary => nil,
15
- # special cased to not allow activating a shard other than the default
16
- :unsharded => [Shard]
17
- }
18
- private_constant :CATEGORIES
19
- @connection_specification_name = @shard_category = :unsharded
20
-
21
- if defined?(::ProtectedAttributes)
22
- attr_accessible :default, :name, :database_server
23
- end
24
-
25
- # only allow one default
26
- validates_uniqueness_of :default, :if => lambda { |s| s.default? }
27
-
28
- after_save :clear_cache
29
- after_destroy :clear_cache
30
-
31
- after_rollback :on_rollback
32
-
33
- scope :primary, -> { where(name: nil).order(:database_server_id, :id).distinct_on(:database_server_id) }
34
-
35
- class << self
36
- def categories
37
- CATEGORIES.keys
38
- end
39
-
40
- def default(reload_deprecated = false, reload: false, with_fallback: false)
41
- reload = reload_deprecated if reload_deprecated
42
- if !@default || reload
43
- # Have to create a dummy object so that several key methods still work
44
- # (it's easier to do this in one place here, and just assume that sharding
45
- # is up and running everywhere else). This includes for looking up the
46
- # default shard itself. This also needs to be a local so that this method
47
- # can be re-entrant
48
- default = DefaultShard.instance
49
-
50
- # if we already have a default shard in place, and the caller wants
51
- # to use it as a fallback, use that instead of the dummy instance
52
- if with_fallback && @default
53
- default = @default
54
- end
55
-
56
- # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
57
- @default ||= default
58
-
59
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
60
- @default = begin
61
- find_cached("default_shard") { Shard.where(default: true).take } || default
62
- rescue
63
- default
64
- end
65
-
66
- # rebuild current shard activations - it might have "another" default shard serialized there
67
- active_shards.replace(active_shards.dup.map do |category, shard|
68
- shard = Shard.lookup((!shard || shard.default?) ? 'default' : shard.id)
69
- [category, shard]
70
- end.to_h)
71
-
72
- activate!(primary: @default) if active_shards.empty?
73
-
74
- # make sure this is not erroneously cached
75
- if @default.database_server.instance_variable_defined?(:@primary_shard)
76
- @default.database_server.remove_instance_variable(:@primary_shard)
77
- end
78
-
79
- # and finally, check for cached references to the default shard on the existing connection
80
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.shard.default?
81
- ::ActiveRecord::Base.connection.shard = @default
82
- end
83
- end
84
- @default
85
- end
86
-
87
- def current(category = :primary)
88
- active_shards[category] || Shard.default
89
- end
90
-
91
- def activate(shards)
92
- old_shards = activate!(shards)
93
- yield
94
- ensure
95
- active_shards.merge!(old_shards) if old_shards
96
- end
97
-
98
- def activate!(shards)
99
- old_shards = nil
100
- currently_active_shards = active_shards
101
- shards.each do |category, shard|
102
- next if category == :unsharded
103
- unless currently_active_shards[category] == shard
104
- old_shards ||= {}
105
- old_shards[category] = currently_active_shards[category]
106
- currently_active_shards[category] = shard
107
- end
108
- end
109
- old_shards
110
- end
111
-
112
- def lookup(id)
113
- id_i = id.to_i
114
- return current if id_i == current.id || id == 'self'
115
- return default if id_i == default.id || id.nil? || id == 'default'
116
- id = id_i
117
- raise ArgumentError if id == 0
118
-
119
- unless cached_shards.has_key?(id)
120
- cached_shards[id] = Shard.default.activate do
121
- find_cached(['shard', id]) { find_by(id: id) }
122
- end
123
- end
124
- cached_shards[id]
125
- end
126
-
127
- def clear_cache
128
- cached_shards.clear
129
- end
130
-
131
- # ==== Parameters
132
- #
133
- # * +shards+ - an array or relation of Shards to iterate over
134
- # * +categories+ - an array of categories to activate
135
- # * +options+ -
136
- # :parallel - true/false to execute in parallel, or a integer of how many
137
- # sub-processes per database server. Note that parallel
138
- # invocation currently uses forking, so should be used sparingly
139
- # because errors are not raised, and you cannot get results back
140
- # :max_procs - only run this many parallel processes at a time
141
- # :exception - :ignore, :raise, :defer (wait until the end and raise the first
142
- # error), or a proc
143
- def with_each_shard(*args)
144
- raise ArgumentError, "wrong number of arguments (#{args.length} for 0...3)" if args.length > 3
145
-
146
- unless default.is_a?(Shard)
147
- return Array.wrap(yield)
148
- end
149
-
150
- options = args.extract_options!
151
- if args.length == 1
152
- if Array === args.first && args.first.first.is_a?(Symbol)
153
- categories = args.first
154
- else
155
- scope = args.first
156
- end
157
- else
158
- scope, categories = args
159
- end
160
-
161
- parallel = case options[:parallel]
162
- when true
163
- 1
164
- when false, nil
165
- 0
166
- else
167
- options[:parallel]
168
- end
169
- options.delete(:parallel)
170
-
171
- scope ||= Shard.all
172
- if ::ActiveRecord::Relation === scope && scope.order_values.empty?
173
- scope = scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
174
- end
175
-
176
- if parallel > 0
177
- max_procs = determine_max_procs(options.delete(:max_procs), parallel)
178
- if ::ActiveRecord::Relation === scope
179
- # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
180
- database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
181
- map(&:database_server).compact.uniq
182
- # nothing to do
183
- return if database_servers.count == 0
184
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
185
-
186
- scopes = Hash[database_servers.map do |server|
187
- server_scope = server.shards.merge(scope)
188
- if parallel == 1
189
- subscopes = [server_scope]
190
- else
191
- subscopes = []
192
- total = server_scope.count
193
- ranges = []
194
- server_scope.find_ids_in_ranges(:batch_size => (total.to_f / parallel).ceil) do |min, max|
195
- ranges << [min, max]
196
- end
197
- # create a half-open range on the last one
198
- ranges.last[1] = nil
199
- ranges.each do |min, max|
200
- subscope = server_scope.where("id>=?", min)
201
- subscope = subscope.where("id<=?", max) if max
202
- subscopes << subscope
203
- end
204
- end
205
- [server, subscopes]
206
- end]
207
- else
208
- scopes = scope.group_by(&:database_server)
209
- if parallel > 1
210
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
211
- scopes = Hash[scopes.map do |(server, shards)|
212
- [server, shards.in_groups(parallel, false).compact]
213
- end]
214
- else
215
- scopes = Hash[scopes.map { |(server, shards)| [server, [shards]] }]
216
- end
217
- end
218
-
219
- exception_pipes = []
220
- pids = []
221
- out_fds = []
222
- err_fds = []
223
- pid_to_name_map = {}
224
- fd_to_name_map = {}
225
- errors = []
226
-
227
- wait_for_output = lambda do |out_fds, err_fds, fd_to_name_map|
228
- ready, _ = IO.select(out_fds + err_fds)
229
- ready.each do |fd|
230
- if fd.eof?
231
- fd.close
232
- out_fds.delete(fd)
233
- err_fds.delete(fd)
234
- next
235
- end
236
- line = fd.readline
237
- puts "#{fd_to_name_map[fd]}: #{line}"
238
- end
239
- end
240
-
241
- # only one process; don't bother forking
242
- if scopes.length == 1 && parallel == 1
243
- return with_each_shard(scopes.first.last.first, categories, options) { yield }
244
- end
245
-
246
- # clear connections prior to forking (no more queries will be executed in the parent,
247
- # and we want them gone so that we don't accidentally use them post-fork doing something
248
- # silly like dealloc'ing prepared statements)
249
- ::ActiveRecord::Base.clear_all_connections!
250
-
251
- scopes.each do |server, subscopes|
252
- subscopes.each_with_index do |subscope, idx|
253
- if subscopes.length > 1
254
- name = "#{server.id} #{idx + 1}"
255
- else
256
- name = server.id
257
- end
258
-
259
- exception_pipe = IO.pipe
260
- exception_pipes << exception_pipe
261
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
262
- begin
263
- Switchman.config[:on_fork_proc]&.call
264
-
265
- # set a pretty name for the process title, up to 128 characters
266
- # (we don't actually know the limit, depending on how the process
267
- # was started)
268
- # first, simplify the binary name by stripping directories,
269
- # then truncate arguments as necessary
270
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
271
- max_length = 128 - bin.length - name.length - 3
272
- args = ARGV.join(" ")
273
- if max_length >= 0
274
- args = args[0..max_length]
275
- end
276
- new_title = [bin, args, name].join(" ")
277
- Process.setproctitle(new_title)
278
-
279
- with_each_shard(subscope, categories, options) { yield }
280
- exception_pipe.last.close
281
- rescue => e
282
- begin
283
- dumped = Marshal.dump(e)
284
- rescue
285
- # couldn't dump the exception; create a copy with just
286
- # the message and the backtrace
287
- e2 = e.class.new(e.message)
288
- e2.set_backtrace(e.backtrace)
289
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
290
- dumped = Marshal.dump(e2)
291
- end
292
- exception_pipe.last.set_encoding(dumped.encoding)
293
- exception_pipe.last.write(dumped)
294
- exception_pipe.last.flush
295
- exception_pipe.last.close
296
- exit! 1
297
- end
298
- end)
299
- exception_pipe.last.close
300
- pids << pid
301
- io_in.close # don't care about writing to stdin
302
- out_fds << io_out
303
- err_fds << io_err
304
- pid_to_name_map[pid] = name
305
- fd_to_name_map[io_out] = name
306
- fd_to_name_map[io_err] = name
307
-
308
- while max_procs && pids.count >= max_procs
309
- while max_procs && out_fds.count >= max_procs
310
- # wait for output if we've hit the max_procs limit
311
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
312
- end
313
- # we've gotten all the output from one fd so wait for its child process to exit
314
- found_pid, status = Process.wait2
315
- pids.delete(found_pid)
316
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
317
- end
318
- end
319
- end
320
-
321
- while out_fds.any? || err_fds.any?
322
- wait_for_output.call(out_fds, err_fds, fd_to_name_map)
323
- end
324
- pids.each do |pid|
325
- _, status = Process.waitpid2(pid)
326
- errors << pid_to_name_map[pid] if status.exitstatus != 0
327
- end
328
-
329
- # check for an exception; we only re-raise the first one
330
- exception_pipes.each do |exception_pipe|
331
- begin
332
- serialized_exception = exception_pipe.first.read
333
- next if serialized_exception.empty?
334
- exception = Marshal.load(serialized_exception)
335
- raise exception
336
- ensure
337
- exception_pipe.first.close
338
- end
339
- end
340
-
341
- unless errors.empty?
342
- raise ParallelShardExecError.new("The following subprocesses did not exit cleanly: #{errors.sort.join(", ")}")
343
- end
344
- return
345
- end
346
-
347
- categories ||= []
348
-
349
- previous_shard = nil
350
- close_connections_if_needed = lambda do |shard|
351
- # prune the prior connection unless it happened to be the same
352
- if previous_shard && shard != previous_shard && !previous_shard.database_server.shareable?
353
- previous_shard.activate do
354
- ::Shackles.activated_environments.each do |env|
355
- ::Shackles.activate(env) do
356
- if ::ActiveRecord::Base.connected? && ::ActiveRecord::Base.connection.open_transactions == 0
357
- ::ActiveRecord::Base.connection_pool.current_pool.disconnect!
358
- end
359
- end
360
- end
361
- end
362
- end
363
- end
364
-
365
- result = []
366
- exception = nil
367
- scope.each do |shard|
368
- # shard references a database server that isn't configured in this environment
369
- next unless shard.database_server
370
- close_connections_if_needed.call(shard)
371
- shard.activate(*categories) do
372
- begin
373
- result.concat Array.wrap(yield)
374
- rescue
375
- case options[:exception]
376
- when :ignore
377
- when :defer
378
- exception ||= $!
379
- when Proc
380
- options[:exception].call
381
- when :raise
382
- raise
383
- else
384
- raise
385
- end
386
- end
387
- end
388
- previous_shard = shard
389
- end
390
- close_connections_if_needed.call(Shard.current)
391
- raise exception if exception
392
- result
393
- end
394
-
395
- def partition_by_shard(array, partition_proc = nil)
396
- shard_arrays = {}
397
- array.each do |object|
398
- partition_object = partition_proc ? partition_proc.call(object) : object
399
- case partition_object
400
- when Shard
401
- shard = partition_object
402
- when ::ActiveRecord::Base
403
- if partition_object.respond_to?(:associated_shards)
404
- partition_object.associated_shards.each do |a_shard|
405
- shard_arrays[a_shard] ||= []
406
- shard_arrays[a_shard] << object
407
- end
408
- next
409
- else
410
- shard = partition_object.shard
411
- end
412
- when Integer, /^\d+$/, /^(\d+)~(\d+)$/
413
- local_id, shard = Shard.local_id_for(partition_object)
414
- local_id ||= partition_object
415
- object = local_id if !partition_proc
416
- end
417
- shard ||= Shard.current
418
- shard_arrays[shard] ||= []
419
- shard_arrays[shard] << object
420
- end
421
- # TODO: use with_each_shard (or vice versa) to get
422
- # connection management and parallelism benefits
423
- shard_arrays.inject([]) do |results, (shard, objects)|
424
- results.concat shard.activate { Array.wrap(yield objects) }
425
- end
426
- end
427
-
428
- # converts an AR object, integral id, string id, or string short-global-id to a
429
- # integral id. nil if it can't be interpreted
430
- def integral_id_for(any_id)
431
- if any_id.is_a?(::Arel::Nodes::Casted)
432
- any_id = any_id.val
433
- elsif any_id.is_a?(::Arel::Nodes::BindParam) && ::Rails.version >= "5.2"
434
- any_id = any_id.value.value_before_type_cast
435
- end
436
-
437
- case any_id
438
- when ::ActiveRecord::Base
439
- any_id.id
440
- when /^(\d+)~(\d+)$/
441
- local_id = $2.to_i
442
- # doesn't make sense to have a double-global id
443
- return nil if local_id > IDS_PER_SHARD
444
- $1.to_i * IDS_PER_SHARD + local_id
445
- when Integer, /^\d+$/
446
- any_id.to_i
447
- else
448
- nil
449
- end
450
- end
451
-
452
- # takes an id-ish, and returns a local id and the shard it's
453
- # local to. [nil, nil] if it can't be interpreted. [id, nil]
454
- # if it's already a local ID. [nil, nil] if it's a well formed
455
- # id, but the shard it refers to does not exist
456
- NIL_NIL_ID = [nil, nil].freeze
457
- def local_id_for(any_id)
458
- id = integral_id_for(any_id)
459
- return NIL_NIL_ID unless id
460
- if id < IDS_PER_SHARD
461
- [id, nil]
462
- elsif shard = lookup(id / IDS_PER_SHARD)
463
- [id % IDS_PER_SHARD, shard]
464
- else
465
- NIL_NIL_ID
466
- end
467
- end
468
-
469
- # takes an id-ish, and returns an integral id relative to
470
- # target_shard. returns nil if it can't be interpreted,
471
- # or the integral version of the id if it refers to a shard
472
- # that does not exist
473
- def relative_id_for(any_id, source_shard, target_shard)
474
- integral_id = integral_id_for(any_id)
475
- local_id, shard = local_id_for(integral_id)
476
- return integral_id unless local_id
477
- shard ||= source_shard
478
- return local_id if shard == target_shard
479
- shard.global_id_for(local_id)
480
- end
481
-
482
- # takes an id-ish, and returns a shortened global
483
- # string id if global, and itself if local.
484
- # returns any_id itself if it can't be interpreted
485
- def short_id_for(any_id)
486
- local_id, shard = local_id_for(any_id)
487
- return any_id unless local_id
488
- return local_id unless shard
489
- "#{shard.id}~#{local_id}"
490
- end
491
-
492
- # takes an id-ish, and returns an integral global id.
493
- # returns nil if it can't be interpreted
494
- def global_id_for(any_id, source_shard = nil)
495
- id = integral_id_for(any_id)
496
- return any_id unless id
497
- if id >= IDS_PER_SHARD
498
- id
499
- else
500
- source_shard ||= Shard.current
501
- source_shard.global_id_for(id)
502
- end
503
- end
504
-
505
- def shard_for(any_id, source_shard = nil)
506
- return any_id.shard if any_id.is_a?(::ActiveRecord::Base)
507
- _, shard = local_id_for(any_id)
508
- shard || source_shard || Shard.current
509
- end
510
-
511
- # given the provided option, determines whether we need to (and whether
512
- # it's possible) to determine a reasonable default.
513
- def determine_max_procs(max_procs_input, parallel_input=2)
514
- max_procs = nil
515
- if max_procs_input
516
- max_procs = max_procs_input.to_i
517
- max_procs = nil if max_procs == 0
518
- else
519
- return 1 if parallel_input.nil? || parallel_input < 1
520
- cpus = Environment.cpu_count
521
- if cpus && cpus > 0
522
- max_procs = cpus * parallel_input
523
- end
524
- end
525
-
526
- return max_procs
527
- end
528
-
529
- private
530
- # in-process caching
531
- def cached_shards
532
- @cached_shards ||= {}.compare_by_identity
533
- end
534
-
535
- def add_to_cache(shard)
536
- cached_shards[shard.id] = shard
537
- end
538
-
539
- def remove_from_cache(shard)
540
- cached_shards.delete(shard.id)
541
- end
542
-
543
- def find_cached(key)
544
- # can't simply cache the AR object since Shard has a custom serializer
545
- # that calls this method
546
- attributes = Switchman.cache.fetch(key) { yield&.attributes }
547
- return nil unless attributes
548
-
549
- shard = Shard.new
550
- attributes.each do |attr, value|
551
- shard.send(:"#{attr}=", value) if shard.respond_to?(:"#{attr}=")
552
- end
553
- shard.clear_changes_information
554
- shard.instance_variable_set(:@new_record, false)
555
- # connection info doesn't exist in database.yml;
556
- # pretend the shard doesn't exist either
557
- shard = nil unless shard.database_server
558
- shard
559
- end
560
-
561
- def active_shards
562
- Thread.current[:active_shards] ||= {}.compare_by_identity
563
- end
564
- end
565
-
566
- def name
567
- unless instance_variable_defined?(:@name)
568
- # protect against re-entrancy
569
- @name = nil
570
- @name = read_attribute(:name) || default_name
571
- end
572
- @name
573
- end
574
-
575
- def name=(name)
576
- write_attribute(:name, @name = name)
577
- remove_instance_variable(:@name) if name == nil
578
- end
579
-
580
- def database_server
581
- @database_server ||= DatabaseServer.find(self.database_server_id)
582
- end
583
-
584
- def database_server=(database_server)
585
- self.database_server_id = database_server.id
586
- @database_server = database_server
587
- end
588
-
589
- def primary?
590
- self == database_server.primary_shard
591
- end
592
-
593
- def description
594
- [database_server.id, name].compact.join(':')
595
- end
596
-
597
- # Shards are always on the default shard
598
- def shard
599
- Shard.default
600
- end
601
-
602
- def activate(*categories)
603
- shards = hashify_categories(categories)
604
- Shard.activate(shards) do
605
- yield
606
- end
607
- end
608
-
609
- # for use from console ONLY
610
- def activate!(*categories)
611
- shards = hashify_categories(categories)
612
- Shard.activate!(shards)
613
- nil
614
- end
615
-
616
- # custom serialization, since shard is self-referential
617
- def _dump(depth)
618
- self.id.to_s
619
- end
620
-
621
- def self._load(str)
622
- lookup(str.to_i)
623
- end
624
-
625
- def drop_database
626
- raise("Cannot drop the database of the default shard") if self.default?
627
- return unless read_attribute(:name)
628
-
629
- begin
630
- adapter = self.database_server.config[:adapter]
631
- sharding_config = Switchman.config || {}
632
- drop_statement = sharding_config[adapter]&.[](:drop_statement)
633
- drop_statement ||= sharding_config[:drop_statement]
634
- if drop_statement
635
- drop_statement = Array(drop_statement).dup.
636
- map { |statement| statement.gsub('%{name}', self.name) }
637
- end
638
-
639
- case adapter
640
- when 'mysql', 'mysql2'
641
- self.activate do
642
- ::Shackles.activate(:deploy) do
643
- drop_statement ||= "DROP DATABASE #{self.name}"
644
- Array(drop_statement).each do |stmt|
645
- ::ActiveRecord::Base.connection.execute(stmt)
646
- end
647
- end
648
- end
649
- when 'postgresql'
650
- self.activate do
651
- ::Shackles.activate(:deploy) do
652
- # Shut up, Postgres!
653
- conn = ::ActiveRecord::Base.connection
654
- old_proc = conn.raw_connection.set_notice_processor {}
655
- begin
656
- drop_statement ||= "DROP SCHEMA #{self.name} CASCADE"
657
- Array(drop_statement).each do |stmt|
658
- ::ActiveRecord::Base.connection.execute(stmt)
659
- end
660
- ensure
661
- conn.raw_connection.set_notice_processor(&old_proc) if old_proc
662
- end
663
- end
664
- end
665
- end
666
- rescue
667
- logger.info "Drop failed: #{$!}"
668
- end
669
- end
670
-
671
- # takes an id local to this shard, and returns a global id
672
- def global_id_for(local_id)
673
- return nil unless local_id
674
- local_id + self.id * IDS_PER_SHARD
675
- end
676
-
677
- # skip global_id.hash
678
- def hash
679
- id.hash
680
- end
681
-
682
- def destroy
683
- raise("Cannot destroy the default shard") if self.default?
684
- super
685
- end
686
-
687
- private
688
-
689
- def clear_cache
690
- Shard.default.activate do
691
- Switchman.cache.delete(['shard', id].join('/'))
692
- Switchman.cache.delete("default_shard") if default?
693
- end
694
- end
695
-
696
- def default_name
697
- database_server.shard_name(self)
698
- end
699
-
700
- def on_rollback
701
- # make sure all connection pool proxies are referencing valid pools
702
- ::ActiveRecord::Base.connection_handler.connection_pools.each do |pool|
703
- next unless pool.is_a?(ConnectionPoolProxy)
704
-
705
- pool.remove_shard!(self)
706
- end
707
- end
708
-
709
- def hashify_categories(categories)
710
- if categories.empty?
711
- { :primary => self }
712
- else
713
- categories.inject({}) { |h, category| h[category] = self; h }
714
- end
715
- end
716
-
717
- end
718
- end