switchman 3.5.22 → 3.6.8

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: fa5da3b192752676ea8513c8fd05d72e347b10979b7b47ada9cdf008941fc2ac
4
- data.tar.gz: 1da3c8eba9d967ca1a3edd550ecc7dd8bb1dea6193be3a752a65e95dc0beb54d
3
+ metadata.gz: 9c258da7abce6b4b34bb1dac83e352aeb4548776a34820969460c31b4c61674e
4
+ data.tar.gz: '0512961dad99c3041d461ed8e6e183a998d70cd4229712b7246099e8c408c139'
5
5
  SHA512:
6
- metadata.gz: 7ba322eda93832f04595bb93f52153c61db3df1848404d80daf83e4b77b070f45c2d84cc2b6a5ba90c68cd5fbe26745f7ff2932b1a510ad15559f8234dab9bd2
7
- data.tar.gz: 2453c5bccf84d20766340fe60512aec89547d27287832806964688c630f2af5d90070932c49e215f4a6ce209dfb701b1afa181ea1abe4fb8a009c03b55dea88e
6
+ metadata.gz: 494681d5982a2035e85f3058162a7864b1afc89df2c29ba0d95d99bc9408fabe23b4f762b706946152a3f5e983320bf0c2704c0cc66ad13367ae062c9d20843f
7
+ data.tar.gz: ce04f224bd153d4de41df5d2b8ec28b22c25e591eb6465ffcf6e8c1a5e831992981a6fc8c8e86130914ca5d91c6442c2b4335c76f374b61ae24bfc21ad838adc
@@ -22,13 +22,59 @@ module Switchman
22
22
  @integral_id
23
23
  end
24
24
 
25
- %w[transaction insert_all upsert_all].each do |method|
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 }
30
+ end
31
+ else
32
+ db = Shard.current(connection_class_for_self).database_server
33
+ db.unguard { super }
34
+ end
35
+ end
36
+
37
+ # NOTE: `returning` values are _not_ transposed back to the current shard
38
+ %w[insert_all upsert_all].each do |method|
26
39
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
27
- def #{method}(*, **)
28
- if self != ::ActiveRecord::Base && current_scope
29
- current_scope.activate do
40
+ def #{method}(attributes, returning: nil, **)
41
+ scope = self != ::ActiveRecord::Base && current_scope
42
+ if (target_shard = scope&.primary_shard) == (current_shard = Shard.current(connection_class_for_self))
43
+ scope = nil
44
+ end
45
+ if scope
46
+ dupped = false
47
+ attributes.each_with_index do |hash, i|
48
+ if dupped || hash.any? { |k, v| sharded_column?(k) }
49
+ unless dupped
50
+ attributes = attributes.dup
51
+ dupped = true
52
+ end
53
+ attributes[i] = hash.to_h do |k, v|
54
+ if sharded_column?(k)
55
+ [k, Shard.relative_id_for(v, current_shard, target_shard)]
56
+ else
57
+ [k, v]
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ if scope
65
+ scope.activate do
30
66
  db = Shard.current(connection_class_for_self).database_server
31
- db.unguard { super }
67
+ result = db.unguard { super }
68
+ if result&.columns&.any? { |c| sharded_column?(c) }
69
+ transposed_rows = result.rows.map do |row|
70
+ row.map.with_index do |value, i|
71
+ sharded_column?(result.columns[i]) ? Shard.relative_id_for(value, target_shard, current_shard) : value
72
+ end
73
+ end
74
+ result = ::ActiveRecord::Result.new(result.columns, transposed_rows, result.column_types)
75
+ end
76
+
77
+ result
32
78
  end
33
79
  else
34
80
  db = Shard.current(connection_class_for_self).database_server
@@ -83,7 +129,7 @@ module Switchman
83
129
  config_or_env ||= if current_shard == ::Rails.env.to_sym && current_role == :primary
84
130
  :primary
85
131
  else
86
- "#{current_shard}/#{current_role}".to_sym
132
+ :"#{current_shard}/#{current_role}"
87
133
  end
88
134
 
89
135
  super(config_or_env)
@@ -153,8 +199,13 @@ module Switchman
153
199
 
154
200
  def self.prepended(klass)
155
201
  klass.singleton_class.prepend(ClassMethods)
156
- klass.scope :non_shadow, ->(key = primary_key) { where("#{key}<=? AND #{key}>?", Shard::IDS_PER_SHARD, 0) }
157
- klass.scope :shadow, ->(key = primary_key) { where("#{key}>?", Shard::IDS_PER_SHARD) }
202
+ klass.scope :non_shadow, lambda { |key = primary_key|
203
+ where(key => (QueryMethods::NonTransposingValue.new(0)..
204
+ QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)))
205
+ }
206
+ klass.scope :shadow, lambda { |key = primary_key|
207
+ where(key => QueryMethods::NonTransposingValue.new(Shard::IDS_PER_SHARD)..)
208
+ }
158
209
  end
159
210
 
160
211
  def _run_initialize_callbacks
@@ -64,7 +64,7 @@ module Switchman
64
64
  end
65
65
 
66
66
  def tls_key
67
- "#{object_id}_shard".to_sym
67
+ :"#{object_id}_shard"
68
68
  end
69
69
  end
70
70
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Switchman
4
+ module ActiveRecord
5
+ module PendingMigrationConnection
6
+ module ClassMethods
7
+ def current_role
8
+ ::ActiveRecord::Base.current_role
9
+ end
10
+
11
+ def current_switchman_shard
12
+ ::ActiveRecord::Base.current_switchman_shard
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,6 +3,19 @@
3
3
  module Switchman
4
4
  module ActiveRecord
5
5
  module QueryMethods
6
+ # Use this class to prevent a value from getting transposed across shards
7
+ class NonTransposingValue < SimpleDelegator
8
+ def class
9
+ __getobj__.class
10
+ end
11
+
12
+ def is_a?(other)
13
+ return true if other == NonTransposingValue
14
+
15
+ __getobj__.is_a?(other)
16
+ end
17
+ end
18
+
6
19
  # shard_value is one of:
7
20
  # A shard
8
21
  # An array or relation of shards
@@ -398,7 +411,9 @@ module Switchman
398
411
  end
399
412
 
400
413
  def transpose_predicate_value(value, current_shard, target_shard, attribute_type)
401
- if value.is_a?(::ActiveRecord::StatementCache::Substitute)
414
+ if value.is_a?(NonTransposingValue)
415
+ value
416
+ elsif value.is_a?(::ActiveRecord::StatementCache::Substitute)
402
417
  value.sharded = true # mark for transposition later
403
418
  value.primary = true if attribute_type == :primary
404
419
  value
@@ -44,8 +44,14 @@ module Switchman
44
44
  primary_shard.activate(klass.connection_class_for_self) { super }
45
45
  end
46
46
 
47
- def explain
48
- activate { |relation| relation.call_super(:explain, Relation) }
47
+ if ::Rails.version > "7.1.2"
48
+ def transaction(...)
49
+ primary_shard.activate(klass.connection_class_for_self) { super }
50
+ end
51
+ end
52
+
53
+ def explain(*args)
54
+ activate { |relation| relation.call_super(:explain, Relation, *args) }
49
55
  end
50
56
 
51
57
  def load(&block)
@@ -58,10 +64,9 @@ module Switchman
58
64
  end
59
65
 
60
66
  %I[update_all delete_all].each do |method|
61
- arg_params = (RUBY_VERSION <= "2.8") ? "*args" : "*args, **kwargs"
62
67
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
63
- def #{method}(#{arg_params})
64
- result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, #{arg_params}) }
68
+ def #{method}(*args, **kwargs)
69
+ result = self.activate(unordered: true) { |relation| relation.call_super(#{method.inspect}, Relation, *args, **kwargs) }
65
70
  result = result.sum if result.is_a?(Array)
66
71
  result
67
72
  end
@@ -22,12 +22,14 @@ module Switchman
22
22
 
23
23
  def lookup_store(*store_options)
24
24
  store = super
25
- # can't use defined?, because it's a _ruby_ autoloaded constant,
26
- # so just checking that will cause it to get required
27
- if store.instance_of?(ActiveSupport::Cache::RedisCacheStore) &&
28
- !::ActiveSupport::Cache::RedisCacheStore <= RedisCacheStore
25
+ # must use the string name, otherwise it will try to auto-load the constant
26
+ # and we don't want to require redis in this file (since it's not a hard dependency)
27
+ # rubocop:disable Style/ClassEqualityComparison
28
+ if store.class.name == "ActiveSupport::Cache::RedisCacheStore" &&
29
+ !(::ActiveSupport::Cache::RedisCacheStore <= RedisCacheStore)
29
30
  ::ActiveSupport::Cache::RedisCacheStore.prepend(RedisCacheStore)
30
31
  end
32
+ # rubocop:enable Style/ClassEqualityComparison
31
33
  store.options[:namespace] ||= -> { Shard.current.default? ? nil : "shard_#{Shard.current.id}" }
32
34
  store
33
35
  end
@@ -36,7 +38,7 @@ module Switchman
36
38
  module RedisCacheStore
37
39
  def clear(namespace: nil, **)
38
40
  # RedisCacheStore tries to be smart and only clear the cache under your namespace, if you have one set
39
- # unfortunately, it uses the keys command, which is extraordinarily inefficient in a large redis instance
41
+ # unfortunately, it doesn't work using redis clustering because of the way redis keys are distributed
40
42
  # fortunately, we can assume we control the entire instance, because we set up the namespacing, so just
41
43
  # always unset it temporarily for clear calls
42
44
  namespace = nil # rubocop:disable Lint/ShadowedArgument
@@ -12,14 +12,8 @@ module Switchman
12
12
  method.super_method
13
13
  end
14
14
 
15
- if RUBY_VERSION <= "2.8"
16
- def call_super(method, above_module, *args, &block)
17
- super_method_above(method, above_module).call(*args, &block)
18
- end
19
- else
20
- def call_super(method, above_module, *args, **kwargs, &block)
21
- super_method_above(method, above_module).call(*args, **kwargs, &block)
22
- end
15
+ def call_super(method, above_module, ...)
16
+ super_method_above(method, above_module).call(...)
23
17
  end
24
18
  end
25
19
  end
@@ -78,6 +78,11 @@ module Switchman
78
78
  ::ActiveRecord::MigrationContext.prepend(ActiveRecord::MigrationContext)
79
79
  ::ActiveRecord::Migrator.prepend(ActiveRecord::Migrator)
80
80
 
81
+ if ::Rails.version > "7.1.3"
82
+ ::ActiveRecord::PendingMigrationConnection.singleton_class
83
+ .include(ActiveRecord::PendingMigrationConnection::ClassMethods)
84
+ end
85
+
81
86
  ::ActiveRecord::Reflection::AbstractReflection.include(ActiveRecord::Reflection::AbstractReflection)
82
87
  ::ActiveRecord::Reflection::AssociationReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
83
88
  ::ActiveRecord::Reflection::ThroughReflection.prepend(ActiveRecord::Reflection::AssociationScopeCache)
@@ -13,9 +13,8 @@ module Switchman
13
13
  end
14
14
 
15
15
  %w[update_all delete_all].each do |method|
16
- arg_params = (RUBY_VERSION <= "2.8") ? "*args" : "*args, **kwargs"
17
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
18
- def #{method}(#{arg_params})
17
+ def #{method}(*args, **kwargs)
19
18
  db = Shard.current(connection_class_for_self).database_server
20
19
  db.unguard { super }
21
20
  end
@@ -50,16 +50,16 @@ module Switchman
50
50
  end
51
51
  end
52
52
 
53
- class PrefixingIO
53
+ class TransformingIO
54
54
  delegate_missing_to :@original_io
55
55
 
56
- def initialize(prefix, original_io)
57
- @prefix = prefix
56
+ def initialize(transformer, original_io)
57
+ @transformer = transformer
58
58
  @original_io = original_io
59
59
  end
60
60
 
61
61
  def puts(*args)
62
- args.flatten.each { |arg| @original_io.puts "#{@prefix}: #{arg}" }
62
+ args.flatten.each { |arg| @original_io.puts @transformer.call(arg) }
63
63
  end
64
64
  end
65
65
  end
@@ -126,6 +126,7 @@ module Switchman
126
126
  ds.shards.delete_all unless use_transactional_tests
127
127
  ds.destroy
128
128
  end
129
+ ds.remove_instance_variable(:@primary_shard_id) if ds.instance_variable_defined?(:@primary_shard_id)
129
130
  end
130
131
  end
131
132
 
@@ -165,7 +165,7 @@ module Switchman
165
165
  # forking.
166
166
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
167
167
  # error), or a proc
168
- # output: - :simple, :decorated (with database_server_id:shard_name)
168
+ # output: - :simple, :decorated (with database_server_id:shard_name), custom lambda transformer
169
169
  def with_each_shard(*args, parallel: false, exception: :raise, output: :simple)
170
170
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
171
171
 
@@ -181,6 +181,13 @@ module Switchman
181
181
  scope, classes = args
182
182
  end
183
183
 
184
+ output = if output == :decorated
185
+ ->(arg) { "#{Shard.current.description}: #{arg}" }
186
+ elsif output == :simple
187
+ nil
188
+ else
189
+ output
190
+ end
184
191
  parallel = [Environment.cpu_count || 2, 2].min if parallel == true
185
192
  parallel = 0 if parallel == false || parallel.nil?
186
193
 
@@ -224,7 +231,10 @@ module Switchman
224
231
  new_title = [short_parent_name, name].join(" ")
225
232
  Process.setproctitle(new_title)
226
233
  Switchman.config[:on_fork_proc]&.call
227
- with_each_shard(subscope, classes, exception: exception, output: :decorated) do
234
+ with_each_shard(subscope,
235
+ classes,
236
+ exception: exception,
237
+ output: output || :decorated) do
228
238
  last_description = Shard.current.description
229
239
  Parallel::ResultWrapper.new(yield)
230
240
  end
@@ -258,9 +268,9 @@ module Switchman
258
268
  next unless shard.database_server
259
269
 
260
270
  shard.activate(*classes) do
261
- if output == :decorated
262
- $stdout = Parallel::PrefixingIO.new(shard.description, $stdout)
263
- $stderr = Parallel::PrefixingIO.new(shard.description, $stderr)
271
+ if output
272
+ $stdout = Parallel::TransformingIO.new(output, $stdout)
273
+ $stderr = Parallel::TransformingIO.new(output, $stderr)
264
274
  end
265
275
 
266
276
  result.concat Array.wrap(yield)
@@ -3,7 +3,7 @@
3
3
  module Switchman
4
4
  class ShardedInstrumenter < ::SimpleDelegator
5
5
  def initialize(instrumenter, shard_host)
6
- super instrumenter
6
+ super(instrumenter)
7
7
  @shard_host = shard_host
8
8
  end
9
9
 
@@ -23,7 +23,7 @@ module Switchman
23
23
  end
24
24
  }
25
25
  end
26
- super name, payload
26
+ super(name, payload)
27
27
  end
28
28
  end
29
29
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = "3.5.22"
4
+ VERSION = "3.6.8"
5
5
  end
@@ -71,12 +71,13 @@ module Switchman
71
71
  end
72
72
 
73
73
  def self.options
74
- { parallel: ENV["PARALLEL"].to_i }
74
+ { exception: (ENV["FAIL_FAST"] == "0") ? :defer : :raise, parallel: ENV["PARALLEL"].to_i }
75
75
  end
76
76
 
77
77
  # classes - an array or proc, to activate as the current shard during the
78
78
  # task.
79
79
  def self.shardify_task(task_name, classes: [::ActiveRecord::Base])
80
+ log_format = ENV.fetch("LOG_FORMAT", nil)
80
81
  old_task = ::Rake::Task[task_name]
81
82
  old_actions = old_task.actions.dup
82
83
  old_task.actions.clear
@@ -90,14 +91,56 @@ module Switchman
90
91
  ::GuardRail.activate(:deploy) do
91
92
  Shard.default.database_server.unguard do
92
93
  classes = classes.call if classes.respond_to?(:call)
93
- Shard.with_each_shard(scope, classes, **options) do
94
+
95
+ # We don't want the shard status messages to be wrapped using a custom log transfomer
96
+ original_stderr = $stderr
97
+ original_stdout = $stdout
98
+ output = if log_format == "json"
99
+ lambda { |msg|
100
+ JSON.dump(shard: Shard.current.id,
101
+ database_server: Shard.current.database_server.id,
102
+ type: "log",
103
+ message: msg)
104
+ }
105
+ else
106
+ nil
107
+ end
108
+ Shard.with_each_shard(scope, classes, output: output, **options) do
94
109
  shard = Shard.current
95
- puts "#{shard.id}: #{shard.description}"
110
+
111
+ if log_format == "json"
112
+ original_stdout.puts JSON.dump(
113
+ shard: shard.id,
114
+ database_server: shard.database_server.id,
115
+ type: "started"
116
+ )
117
+ else
118
+ original_stdout.puts "#{shard.id}: #{shard.description}"
119
+ end
96
120
 
97
121
  shard.database_server.unguard do
98
122
  old_actions.each { |action| action.call(*task_args) }
99
123
  end
124
+
125
+ if log_format == "json"
126
+ original_stdout.puts JSON.dump(
127
+ shard: shard.id,
128
+ database_server: shard.database_server.id,
129
+ type: "completed"
130
+ )
131
+ end
100
132
  nil
133
+ rescue => e
134
+ if log_format == "json"
135
+ original_stderr.puts JSON.dump(
136
+ shard: shard.id,
137
+ database_server: shard.database_server.id,
138
+ type: "failed",
139
+ message: e.full_message
140
+ )
141
+ end
142
+
143
+ raise
101
144
  end
102
145
  rescue => e
103
146
  if options[:parallel] != 0
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: 3.5.22
4
+ version: 3.6.8
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: 2024-02-26 00:00:00.000000000 Z
13
+ date: 2025-01-27 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -249,6 +249,7 @@ files:
249
249
  - lib/switchman/active_record/log_subscriber.rb
250
250
  - lib/switchman/active_record/migration.rb
251
251
  - lib/switchman/active_record/model_schema.rb
252
+ - lib/switchman/active_record/pending_migration_connection.rb
252
253
  - lib/switchman/active_record/persistence.rb
253
254
  - lib/switchman/active_record/postgresql_adapter.rb
254
255
  - lib/switchman/active_record/predicate_builder.rb
@@ -297,14 +298,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
297
298
  requirements:
298
299
  - - ">="
299
300
  - !ruby/object:Gem::Version
300
- version: '2.7'
301
+ version: '3.0'
301
302
  required_rubygems_version: !ruby/object:Gem::Requirement
302
303
  requirements:
303
304
  - - ">="
304
305
  - !ruby/object:Gem::Version
305
306
  version: '0'
306
307
  requirements: []
307
- rubygems_version: 3.1.6
308
+ rubygems_version: 3.2.33
308
309
  signing_key:
309
310
  specification_version: 4
310
311
  summary: Rails sharding magic