switchman 3.1.0 → 3.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48b27124d5233adf585aed105d0b2522c76249bdd1e13fbcfd7e904241d3a624
4
- data.tar.gz: fea1ccc0ad63f00633fb90bee1d7bd4606f6de0e18564250865c1b1dde61ef8d
3
+ metadata.gz: 39d8ce1bd79211b12278263fda3f0a88adc841051bd1e4f52e8d01ccd371978f
4
+ data.tar.gz: b126fe1b7b700fb424351d901c58b66f9719892deab08a2242de6cf6c5645b77
5
5
  SHA512:
6
- metadata.gz: c05b445111dd0e4b16ecbfaea6ab84557fa2cbadc6102fa0af6fb43cff2f6bbb6606e825f76ed20f6b8a4a2a2966e31babadf66c5eb065a6c4d8a7eea6a8b92a
7
- data.tar.gz: 0af9e15706142f90772d24e2187db93ec83331997bccbb25c1b89cec2dde1bd2414d2774fec3785c7bea1a1b06d3fa5ad49e15b716f0aa35794e2e5734c848a8
6
+ metadata.gz: 266b29135322d76e811feb72414a5c3a1621afe07e65175672092ddefd21296b73fa3fe3ce1b6ce0d34b2e5d1a071b186f0b859b44398ff65e3a19cc3e747830
7
+ data.tar.gz: e26ecce87b654d3dd8ae8caa3111b9cbbc89896ce00cca467ba4bd66527ddf76c2e0db1c4ebff30cd133dbcefc1580f4f6b2dc745ab795813cf85848893badca
@@ -83,16 +83,62 @@ module Switchman
83
83
 
84
84
  module Preloader
85
85
  module Association
86
- module LoaderQuery
87
- def load_records_in_batch(loaders)
88
- # While in theory loading multiple associations that end up being effectively the same would be nice
89
- # it's not very switchman compatible, so just don't bother trying to use that logic
90
- # raw_records = records_for(loaders)
86
+ # significant changes:
87
+ # * associate shards with records
88
+ # * look on all appropriate shards when loading records
89
+ module LoaderRecords
90
+ def populate_keys_to_load_and_already_loaded_records
91
+ @sharded_keys_to_load = {}
91
92
 
92
93
  loaders.each do |loader|
93
- loader.load_records(nil)
94
- loader.run
94
+ multishard = loader.send(:reflection).options[:multishard]
95
+ belongs_to = loader.send(:reflection).macro == :belongs_to
96
+ loader.owners_by_key.each do |key, owners|
97
+ if (loaded_owner = owners.find { |owner| loader.loaded?(owner) })
98
+ already_loaded_records_by_key[key] = loader.target_for(loaded_owner)
99
+ else
100
+ shard_set = @sharded_keys_to_load[key] ||= Set.new
101
+ owner_key_name = loader.send(:owner_key_name)
102
+ owners.each do |owner|
103
+ if multishard && owner.respond_to?(:associated_shards)
104
+ shard_set.merge(owner.associated_shards.map(&:id))
105
+ elsif belongs_to && owner.class.sharded_column?(owner_key_name)
106
+ shard_set.add(Shard.shard_for(owner[owner_key_name], owner.shard).id)
107
+ elsif belongs_to
108
+ shard_set.add(Shard.current.id)
109
+ else
110
+ shard_set.add(owner.shard.id)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ @sharded_keys_to_load.delete_if { |key, _shards| already_loaded_records_by_key.include?(key) }
118
+ end
119
+
120
+ def load_records
121
+ ret = []
122
+
123
+ shards_with_keys = @sharded_keys_to_load.each_with_object({}) do |(key, shards), h|
124
+ shards.each { |shard| (h[shard] ||= []) << key }
125
+ end
126
+
127
+ shards_with_keys.each do |shard, keys|
128
+ Shard.lookup(shard).activate do
129
+ scope_was = loader_query.scope
130
+ begin
131
+ loader_query.instance_variable_set(:@scope, loader_query.scope.shard(Shard.current(loader_query.scope.model.connection_class_for_self)))
132
+ ret += loader_query.load_records_for_keys(keys) do |record|
133
+ loaders.each { |l| l.set_inverse(record) }
134
+ end
135
+ ensure
136
+ loader_query.instance_variable_set(:@scope, scope_was)
137
+ end
138
+ end
95
139
  end
140
+
141
+ ret
96
142
  end
97
143
  end
98
144
 
@@ -110,6 +156,26 @@ module Switchman
110
156
  end
111
157
  end
112
158
 
159
+ # Disabling to keep closer to rails original
160
+ # rubocop:disable Naming/AccessorMethodName, Style/GuardClause
161
+ # significant changes:
162
+ # * globalize the key to lookup
163
+ def set_inverse(record)
164
+ global_key = if model.connection_class_for_self == UnshardedRecord
165
+ convert_key(record[association_key_name])
166
+ else
167
+ Shard.global_id_for(record[association_key_name], record.shard)
168
+ end
169
+
170
+ if (owners = owners_by_key[convert_key(global_key)])
171
+ # Processing only the first owner
172
+ # because the record is modified but not an owner
173
+ association = owners.first.association(reflection.name)
174
+ association.set_inverse_instance(record)
175
+ end
176
+ end
177
+ # rubocop:enable Naming/AccessorMethodName, Style/GuardClause
178
+
113
179
  # significant changes:
114
180
  # * partition_by_shard the records_for call
115
181
  # * re-globalize the fetched owner id before looking up in the map
@@ -120,7 +186,9 @@ module Switchman
120
186
  # #compare_by_identity makes such owners different hash keys
121
187
  @records_by_owner = {}.compare_by_identity
122
188
 
123
- if ::Rails.version < '7.0' && owner_keys.empty?
189
+ if ::Rails.version >= '7.0'
190
+ raw_records ||= loader_query.records_for([self])
191
+ elsif owner_keys.empty?
124
192
  raw_records ||= []
125
193
  else
126
194
  # determine the shard to search for each owner
@@ -162,7 +230,7 @@ module Switchman
162
230
  record.shard)
163
231
  end
164
232
 
165
- owners_by_key[convert_key(owner_key)].each do |owner|
233
+ owners_by_key[convert_key(owner_key)]&.each do |owner|
166
234
  entries = (@records_by_owner[owner] ||= [])
167
235
 
168
236
  if reflection.collection? || entries.empty?
@@ -99,7 +99,7 @@ module Switchman
99
99
  if reflection.options[:polymorphic]
100
100
  # a polymorphic association has to be discovered at runtime. This code ends up being something like
101
101
  # context_type.&.constantize&.connection_class_for_self
102
- "read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self"
102
+ "begin;read_attribute(:#{reflection.foreign_type})&.constantize&.connection_class_for_self;rescue NameError;::ActiveRecord::Base;end"
103
103
  else
104
104
  # otherwise we can just return a symbol for the statically known type of the association
105
105
  "::#{reflection.klass.connection_class_for_self.name}"
@@ -256,20 +256,6 @@ module Switchman
256
256
  attribute(attr_name)
257
257
  end
258
258
  end
259
-
260
- private
261
-
262
- def connection_class_for_self_for_reflection(reflection)
263
- if reflection
264
- if reflection.options[:polymorphic]
265
- read_attribute(reflection.foreign_type)&.constantize&.connection_class_for_self
266
- else
267
- reflection.klass.connection_class_for_self
268
- end
269
- else
270
- self.class.connection_class_for_self
271
- end
272
- end
273
259
  end
274
260
  end
275
261
  end
@@ -22,16 +22,20 @@ module Switchman
22
22
  @integral_id
23
23
  end
24
24
 
25
- def transaction(**)
26
- if self != ::ActiveRecord::Base && current_scope
27
- current_scope.activate do
28
- db = Shard.current(connection_class_for_self).database_server
29
- db.unguard { super }
25
+ %w[transaction insert_all upsert_all].each do |method|
26
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
+ def #{method}(*, **)
28
+ if self != ::ActiveRecord::Base && current_scope
29
+ current_scope.activate do
30
+ db = Shard.current(connection_class_for_self).database_server
31
+ db.unguard { super }
32
+ end
33
+ else
34
+ db = Shard.current(connection_class_for_self).database_server
35
+ db.unguard { super }
36
+ end
30
37
  end
31
- else
32
- db = Shard.current(connection_class_for_self).database_server
33
- db.unguard { super }
34
- end
38
+ RUBY
35
39
  end
36
40
 
37
41
  def reset_column_information
@@ -138,9 +142,33 @@ module Switchman
138
142
  else
139
143
  Shard.current(self.class.connection_class_for_self)
140
144
  end
145
+ readonly! if shadow_record? && !Switchman.config[:writable_shadow_records]
141
146
  super
142
147
  end
143
148
 
149
+ def shadow_record?
150
+ pkey = self[self.class.primary_key]
151
+ return false unless self.class.sharded_column?(self.class.primary_key) && pkey
152
+
153
+ pkey > Shard::IDS_PER_SHARD
154
+ end
155
+
156
+ def save_shadow_record(new_attrs: attributes, target_shard: Shard.current)
157
+ return if target_shard == shard
158
+
159
+ shadow_attrs = {}
160
+ new_attrs.each do |attr, value|
161
+ shadow_attrs[attr] = if self.class.sharded_column?(attr)
162
+ Shard.relative_id_for(value, shard, target_shard)
163
+ else
164
+ value
165
+ end
166
+ end
167
+ target_shard.activate do
168
+ self.class.upsert(shadow_attrs, unique_by: self.class.primary_key)
169
+ end
170
+ end
171
+
144
172
  def shard
145
173
  @shard || Shard.current(self.class.connection_class_for_self) || Shard.default
146
174
  end
@@ -20,13 +20,15 @@ module Switchman
20
20
  end
21
21
 
22
22
  module Migrator
23
- # significant change: just return MIGRATOR_SALT directly
24
- # especially if you're going through pgbouncer, the database
25
- # name you're accessing may not be consistent. it is NOT allowed
26
- # to run migrations against multiple shards in the same database
27
- # concurrently
23
+ # significant change: use the shard name instead of the database name
24
+ # in the lock id. Especially if you're going through pgbouncer, the
25
+ # database name you're accessing may not be consistent
28
26
  def generate_migrator_advisory_lock_id
29
- ::ActiveRecord::Migrator::MIGRATOR_SALT
27
+ db_name_hash = Zlib.crc32(Shard.current.name)
28
+ shard_name_hash = ::ActiveRecord::Migrator::MIGRATOR_SALT * db_name_hash
29
+ # Store in internalmetadata to allow other tools to be able to lock out migrations
30
+ ::ActiveRecord::InternalMetadata[:migrator_advisory_lock_id] = shard_name_hash
31
+ shard_name_hash
30
32
  end
31
33
 
32
34
  # significant change: strip out prefer_secondary from config
@@ -50,7 +50,7 @@ module Switchman
50
50
  ::ActiveRecord::Associations::CollectionProxy.include(ActiveRecord::Associations::CollectionProxy)
51
51
 
52
52
  ::ActiveRecord::Associations::Preloader::Association.prepend(ActiveRecord::Associations::Preloader::Association)
53
- ::ActiveRecord::Associations::Preloader::Association::LoaderQuery.prepend(ActiveRecord::Associations::Preloader::Association::LoaderQuery) unless ::Rails.version < '7.0'
53
+ ::ActiveRecord::Associations::Preloader::Association::LoaderRecords.prepend(ActiveRecord::Associations::Preloader::Association::LoaderRecords) unless ::Rails.version < '7.0'
54
54
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::AbstractAdapter)
55
55
  ::ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(ActiveRecord::ConnectionPool)
56
56
  ::ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend(ActiveRecord::QueryCache)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.1.0'
4
+ VERSION = '3.2.0'
5
5
  end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switchman
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody Cutrer
8
8
  - James Williams
9
9
  - Jacob Fugal
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-06-02 00:00:00.000000000 Z
13
+ date: 2022-11-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -300,7 +300,7 @@ licenses:
300
300
  - MIT
301
301
  metadata:
302
302
  rubygems_mfa_required: 'true'
303
- post_install_message:
303
+ post_install_message:
304
304
  rdoc_options: []
305
305
  require_paths:
306
306
  - lib
@@ -316,7 +316,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
316
316
  version: '0'
317
317
  requirements: []
318
318
  rubygems_version: 3.1.6
319
- signing_key:
319
+ signing_key:
320
320
  specification_version: 4
321
321
  summary: Rails sharding magic
322
322
  test_files: []