switchman 3.4.2 → 3.6.7

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +15 -14
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  5. data/lib/switchman/active_record/abstract_adapter.rb +4 -2
  6. data/lib/switchman/active_record/associations.rb +89 -16
  7. data/lib/switchman/active_record/attribute_methods.rb +67 -22
  8. data/lib/switchman/active_record/base.rb +112 -22
  9. data/lib/switchman/active_record/calculations.rb +93 -37
  10. data/lib/switchman/active_record/connection_handler.rb +18 -0
  11. data/lib/switchman/active_record/connection_pool.rb +18 -14
  12. data/lib/switchman/active_record/database_configurations.rb +37 -15
  13. data/lib/switchman/active_record/finder_methods.rb +44 -14
  14. data/lib/switchman/active_record/log_subscriber.rb +11 -5
  15. data/lib/switchman/active_record/migration.rb +28 -9
  16. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  17. data/lib/switchman/active_record/persistence.rb +22 -0
  18. data/lib/switchman/active_record/postgresql_adapter.rb +11 -10
  19. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  20. data/lib/switchman/active_record/query_cache.rb +49 -20
  21. data/lib/switchman/active_record/query_methods.rb +93 -30
  22. data/lib/switchman/active_record/relation.rb +22 -11
  23. data/lib/switchman/active_record/spawn_methods.rb +2 -2
  24. data/lib/switchman/active_record/statement_cache.rb +2 -2
  25. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  26. data/lib/switchman/active_record/test_fixtures.rb +26 -16
  27. data/lib/switchman/active_support/cache.rb +9 -4
  28. data/lib/switchman/arel.rb +34 -18
  29. data/lib/switchman/call_super.rb +2 -8
  30. data/lib/switchman/database_server.rb +68 -21
  31. data/lib/switchman/default_shard.rb +14 -3
  32. data/lib/switchman/engine.rb +39 -19
  33. data/lib/switchman/environment.rb +2 -2
  34. data/lib/switchman/errors.rb +4 -1
  35. data/lib/switchman/guard_rail/relation.rb +1 -2
  36. data/lib/switchman/parallel.rb +5 -5
  37. data/lib/switchman/r_spec_helper.rb +11 -11
  38. data/lib/switchman/shard.rb +166 -64
  39. data/lib/switchman/sharded_instrumenter.rb +7 -3
  40. data/lib/switchman/standard_error.rb +4 -0
  41. data/lib/switchman/test_helper.rb +2 -2
  42. data/lib/switchman/version.rb +1 -1
  43. data/lib/switchman.rb +27 -15
  44. data/lib/tasks/switchman.rake +117 -51
  45. metadata +19 -44
data/lib/switchman.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'guard_rail'
4
- require 'zeitwerk'
3
+ require "guard_rail"
4
+ require "zeitwerk"
5
5
 
6
6
  class SwitchmanInflector < Zeitwerk::GemInflector
7
7
  def camelize(basename, abspath)
8
8
  if basename =~ /\Apostgresql_(.*)/
9
- 'PostgreSQL' + super($1, abspath)
9
+ "PostgreSQL" + super($1, abspath)
10
10
  else
11
11
  super
12
12
  end
@@ -18,21 +18,33 @@ loader.inflector = SwitchmanInflector.new(__FILE__)
18
18
  loader.setup
19
19
 
20
20
  module Switchman
21
- def self.config
22
- # TODO: load from yaml
23
- @config ||= {}
24
- end
21
+ Deprecation = ::ActiveSupport::Deprecation.new("4.0", "Switchman")
25
22
 
26
- def self.cache
27
- (@cache.respond_to?(:call) ? @cache.call : @cache) || ::Rails.cache
28
- end
23
+ class << self
24
+ attr_writer :cache
29
25
 
30
- def self.cache=(cache)
31
- @cache = cache
32
- end
26
+ def config
27
+ # TODO: load from yaml
28
+ @config ||= {}
29
+ end
30
+
31
+ def cache
32
+ (@cache.respond_to?(:call) ? @cache.call : @cache) || ::Rails.cache
33
+ end
34
+
35
+ def region
36
+ config[:region]
37
+ end
33
38
 
34
- def self.foreign_key_check(name, type, limit: nil)
35
- puts "WARNING: All foreign keys need to be 8-byte integers. #{name} looks like a foreign key. If so, please add the option: `:limit => 8`" if name.to_s =~ /_id\z/ && type.to_s == 'integer' && limit.to_i < 8
39
+ def foreign_key_check(name, type, limit: nil)
40
+ return unless name.to_s.end_with?("_id") && type.to_s == "integer" && limit.to_i < 8
41
+
42
+ puts <<~TEXT.squish
43
+ WARNING: All foreign keys need to be 8-byte integers.
44
+ #{name} looks like a foreign key.
45
+ If so, please add the option: `:limit => 8`
46
+ TEXT
47
+ end
36
48
  end
37
49
 
38
50
  class OrderOnMultiShardQuery < RuntimeError; end
@@ -2,8 +2,8 @@
2
2
 
3
3
  # In rails 7.0+ if you have only 1 db in the env it doesn't try to do explicit activation
4
4
  # (and for rails purposes we only have one db per env because each database server is a separate env)
5
- if Rails.version < '7.0'
6
- task_prefix = Rake::Task.task_defined?('app:db:migrate') ? 'app:db' : 'db'
5
+ if Rails.version < "7.0"
6
+ task_prefix = Rake::Task.task_defined?("app:db:migrate") ? "app:db" : "db"
7
7
  Rake::Task["#{task_prefix}:migrate"].clear_actions.enhance do
8
8
  ActiveRecord::Tasks::DatabaseTasks.migrate
9
9
  # Ensure this doesn't blow up when running inside the dummy app
@@ -13,28 +13,29 @@ end
13
13
 
14
14
  module Switchman
15
15
  module Rake
16
- def self.filter_database_servers(&block)
17
- chain = filter_database_servers_chain # use a local variable so that the current chain is closed over in the following lambda
18
- @filter_database_servers_chain = ->(servers) { block.call(servers, chain) }
16
+ def self.filter_database_servers
17
+ # use a local variable so that the current chain is closed over in the following lambda
18
+ chain = filter_database_servers_chain
19
+ @filter_database_servers_chain = ->(servers) { yield(servers, chain) }
19
20
  end
20
21
 
21
22
  def self.scope(base_scope = Shard,
22
- database_server: ENV.fetch('DATABASE_SERVER', nil),
23
- shard: ENV.fetch('SHARD', nil))
23
+ database_server: ENV.fetch("DATABASE_SERVER", nil),
24
+ shard: ENV.fetch("SHARD", nil))
24
25
  servers = DatabaseServer.all
25
26
 
26
27
  if database_server
27
28
  servers = database_server
28
- if servers.first == '-'
29
+ if servers.first == "-"
29
30
  negative = true
30
31
  servers = servers[1..]
31
32
  end
32
- servers = servers.split(',')
33
- open = servers.delete('open')
33
+ servers = servers.split(",")
34
+ open = servers.delete("open")
34
35
 
35
- servers = servers.map { |server| DatabaseServer.find(server) }.compact
36
+ servers = servers.filter_map { |server| DatabaseServer.find(server) }
36
37
  if open
37
- open_servers = DatabaseServer.all.select { |server| server.config[:open] }
38
+ open_servers = DatabaseServer.select { |server| server.config[:open] }
38
39
  servers.concat(open_servers)
39
40
  servers << DatabaseServer.find(nil) if open_servers.empty?
40
41
  servers.uniq!
@@ -42,9 +43,22 @@ module Switchman
42
43
  servers = DatabaseServer.all - servers if negative
43
44
  end
44
45
 
46
+ ENV["REGION"]&.split(",")&.each do |region|
47
+ method = :select!
48
+ if region[0] == "-"
49
+ method = :reject!
50
+ region = region[1..]
51
+ end
52
+ if region == "self"
53
+ servers.send(method, &:in_current_region?)
54
+ else
55
+ servers.send(method) { |server| server.in_region?(region) }
56
+ end
57
+ end
58
+
45
59
  servers = filter_database_servers_chain.call(servers)
46
60
 
47
- scope = base_scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id'))
61
+ scope = base_scope.order(::Arel.sql("database_server_id IS NOT NULL, database_server_id, id"))
48
62
  if servers != DatabaseServer.all
49
63
  database_server_ids = servers.map(&:id)
50
64
  database_server_ids << nil if servers.include?(Shard.default.database_server)
@@ -57,36 +71,81 @@ module Switchman
57
71
  end
58
72
 
59
73
  def self.options
60
- { parallel: ENV['PARALLEL'].to_i }
74
+ { exception: (ENV["FAIL_FAST"] == "0") ? :defer : :raise, parallel: ENV["PARALLEL"].to_i }
61
75
  end
62
76
 
63
77
  # classes - an array or proc, to activate as the current shard during the
64
78
  # task.
65
79
  def self.shardify_task(task_name, classes: [::ActiveRecord::Base])
80
+ log_format = ENV.fetch("LOG_FORMAT", nil)
66
81
  old_task = ::Rake::Task[task_name]
67
82
  old_actions = old_task.actions.dup
68
83
  old_task.actions.clear
69
84
 
70
85
  old_task.enhance do |*task_args|
71
86
  if ::Rails.env.test?
72
- require 'switchman/test_helper'
87
+ require "switchman/test_helper"
73
88
  TestHelper.recreate_persistent_test_shards(dont_create: true)
74
89
  end
75
90
 
76
91
  ::GuardRail.activate(:deploy) do
77
92
  Shard.default.database_server.unguard do
78
93
  classes = classes.call if classes.respond_to?(:call)
79
- 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
80
109
  shard = Shard.current
81
- 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
82
120
 
83
121
  shard.database_server.unguard do
84
122
  old_actions.each { |action| action.call(*task_args) }
85
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
86
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
87
144
  end
88
145
  rescue => e
89
- warn "Exception from #{e.current_shard.id}: #{e.current_shard.description}:\n#{e.full_message}" if options[:parallel] != 0
146
+ if options[:parallel] != 0
147
+ warn "Exception from #{e.current_shard.id}: #{e.current_shard.description}:\n#{e.full_message}"
148
+ end
90
149
  raise
91
150
  end
92
151
  end
@@ -98,7 +157,7 @@ module Switchman
98
157
  end
99
158
 
100
159
  def self.shard_scope(scope, raw_shard_ids)
101
- raw_shard_ids = raw_shard_ids.split(',')
160
+ raw_shard_ids = raw_shard_ids.split(",")
102
161
 
103
162
  shard_ids = []
104
163
  negative_shard_ids = []
@@ -108,13 +167,13 @@ module Switchman
108
167
 
109
168
  raw_shard_ids.each do |id|
110
169
  case id
111
- when 'default'
170
+ when "default"
112
171
  shard_ids << Shard.default.id
113
- when '-default'
172
+ when "-default"
114
173
  negative_shard_ids << Shard.default.id
115
- when 'primary'
174
+ when "primary"
116
175
  shard_ids.concat(Shard.primary.pluck(:id))
117
- when '-primary'
176
+ when "-primary"
118
177
  negative_shard_ids.concat(Shard.primary.pluck(:id))
119
178
  when /^(-?)(\d+)?\.\.(\.)?(\d+)?$/
120
179
  negative, start, open, finish = $1.present?, $2, $3.present?, $4
@@ -122,8 +181,8 @@ module Switchman
122
181
 
123
182
  range = []
124
183
  range << "id>=#{start}" if start
125
- range << "id<#{'=' unless open}#{finish}" if finish
126
- (negative ? negative_ranges : ranges) << "(#{range.join(' AND ')})"
184
+ range << "id<#{"=" unless open}#{finish}" if finish
185
+ (negative ? negative_ranges : ranges) << "(#{range.join(" AND ")})"
127
186
  when /^-(\d+)$/
128
187
  negative_shard_ids << $1.to_i
129
188
  when /^\d+$/
@@ -151,21 +210,21 @@ module Switchman
151
210
  select = []
152
211
  if index != 1
153
212
  subscope = subscope.offset(per_chunk * (index - 1))
154
- select << 'MIN(id) AS min_id'
213
+ select << "MIN(id) AS min_id"
155
214
  end
156
215
  if index != denominator
157
216
  subscope = subscope.limit(per_chunk)
158
- select << 'MAX(id) AS max_id'
217
+ select << "MAX(id) AS max_id"
159
218
  end
160
219
 
161
- result = Shard.from(subscope).select(select.join(', ')).to_a.first
220
+ result = Shard.from(subscope).select(select.join(", ")).to_a.first
162
221
  range = case index
163
222
  when 1
164
- "id<=#{result['max_id']}"
223
+ "id<=#{result["max_id"]}"
165
224
  when denominator
166
- "id>=#{result['min_id']}"
225
+ "id>=#{result["min_id"]}"
167
226
  else
168
- "(id>=#{result['min_id']} AND id<=#{result['max_id']})"
227
+ "(id>=#{result["min_id"]} AND id<=#{result["max_id"]})"
169
228
  end
170
229
 
171
230
  (numerator.negative? ? negative_ranges : ranges) << range
@@ -186,16 +245,16 @@ module Switchman
186
245
 
187
246
  conditions = []
188
247
  positive_queries = []
189
- positive_queries << ranges.join(' OR ') unless ranges.empty?
248
+ positive_queries << ranges.join(" OR ") unless ranges.empty?
190
249
  unless shard_ids.empty?
191
- positive_queries << 'id IN (?)'
250
+ positive_queries << "id IN (?)"
192
251
  conditions << shard_ids
193
252
  end
194
- positive_query = positive_queries.join(' OR ')
253
+ positive_query = positive_queries.join(" OR ")
195
254
  scope = scope.where(positive_query, *conditions) unless positive_queries.empty?
196
255
 
197
- scope = scope.where("NOT (#{negative_ranges.join(' OR')})") unless negative_ranges.empty?
198
- scope = scope.where('id NOT IN (?)', negative_shard_ids) unless negative_shard_ids.empty?
256
+ scope = scope.where("NOT (#{negative_ranges.join(" OR")})") unless negative_ranges.empty?
257
+ scope = scope.where("id NOT IN (?)", negative_shard_ids) unless negative_shard_ids.empty?
199
258
  scope
200
259
  end
201
260
 
@@ -206,21 +265,28 @@ module Switchman
206
265
 
207
266
  module ActiveRecord
208
267
  module PostgreSQLDatabaseTasks
209
- def structure_dump(filename, extra_flags = nil)
210
- set_psql_env
211
- args = ['--schema-only', '--no-privileges', '--no-owner', '--file', filename]
212
- args.concat(Array(extra_flags)) if extra_flags
213
- shard = Shard.current.name
214
- serialized_search_path = shard
215
- args << "--schema=#{Shellwords.escape(shard)}"
216
-
217
- ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables
218
- args += ignore_tables.flat_map { |table| ['-T', table] } if ignore_tables.any?
219
-
220
- args << db_config.database
221
- run_cmd('pg_dump', args, 'dumping')
222
- remove_sql_header_comments(filename)
223
- File.open(filename, 'a') { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
268
+ if ::Rails.version < "7.0"
269
+ def structure_dump(filename, extra_flags = nil)
270
+ set_psql_env
271
+ args = ["--schema-only", "--no-privileges", "--no-owner", "--file", filename]
272
+ args.concat(Array(extra_flags)) if extra_flags
273
+ shard = Shard.current.name
274
+ serialized_search_path = shard
275
+ args << "--schema=#{Shellwords.escape(shard)}"
276
+
277
+ ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables
278
+ args += ignore_tables.flat_map { |table| ["-T", table] } if ignore_tables.any?
279
+
280
+ args << db_config.database
281
+ run_cmd("pg_dump", args, "dumping")
282
+ remove_sql_header_comments(filename)
283
+ File.open(filename, "a") { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
284
+ end
285
+ else
286
+ def structure_dump(...)
287
+ ::ActiveRecord.dump_schemas = Switchman::Shard.current.name
288
+ super
289
+ end
224
290
  end
225
291
  end
226
292
  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: 3.4.2
4
+ version: 3.6.7
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: 2023-04-17 00:00:00.000000000 Z
13
+ date: 2024-09-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 6.1.4
22
22
  - - "<"
23
23
  - !ruby/object:Gem::Version
24
- version: '7.1'
24
+ version: '7.2'
25
25
  type: :runtime
26
26
  prerelease: false
27
27
  version_requirements: !ruby/object:Gem::Requirement
@@ -31,7 +31,7 @@ dependencies:
31
31
  version: 6.1.4
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '7.1'
34
+ version: '7.2'
35
35
  - !ruby/object:Gem::Dependency
36
36
  name: guardrail
37
37
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +69,7 @@ dependencies:
69
69
  version: '6.1'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '7.1'
72
+ version: '7.2'
73
73
  type: :runtime
74
74
  prerelease: false
75
75
  version_requirements: !ruby/object:Gem::Requirement
@@ -79,35 +79,21 @@ dependencies:
79
79
  version: '6.1'
80
80
  - - "<"
81
81
  - !ruby/object:Gem::Version
82
- version: '7.1'
82
+ version: '7.2'
83
83
  - !ruby/object:Gem::Dependency
84
- name: appraisal
84
+ name: debug
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 2.3.0
89
+ version: '1.8'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 2.3.0
97
- - !ruby/object:Gem::Dependency
98
- name: byebug
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
96
+ version: '1.8'
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: pg
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -122,20 +108,6 @@ dependencies:
122
108
  - - "~>"
123
109
  - !ruby/object:Gem::Version
124
110
  version: '1.2'
125
- - !ruby/object:Gem::Dependency
126
- name: pry
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
111
  - !ruby/object:Gem::Dependency
140
112
  name: rake
141
113
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +142,14 @@ dependencies:
170
142
  requirements:
171
143
  - - "~>"
172
144
  - !ruby/object:Gem::Version
173
- version: '4.0'
145
+ version: '6.0'
174
146
  type: :development
175
147
  prerelease: false
176
148
  version_requirements: !ruby/object:Gem::Requirement
177
149
  requirements:
178
150
  - - "~>"
179
151
  - !ruby/object:Gem::Version
180
- version: '4.0'
152
+ version: '6.0'
181
153
  - !ruby/object:Gem::Dependency
182
154
  name: rubocop
183
155
  requirement: !ruby/object:Gem::Requirement
@@ -193,19 +165,19 @@ dependencies:
193
165
  - !ruby/object:Gem::Version
194
166
  version: '1.10'
195
167
  - !ruby/object:Gem::Dependency
196
- name: rubocop-performance
168
+ name: rubocop-inst
197
169
  requirement: !ruby/object:Gem::Requirement
198
170
  requirements:
199
171
  - - "~>"
200
172
  - !ruby/object:Gem::Version
201
- version: '1.16'
173
+ version: '1'
202
174
  type: :development
203
175
  prerelease: false
204
176
  version_requirements: !ruby/object:Gem::Requirement
205
177
  requirements:
206
178
  - - "~>"
207
179
  - !ruby/object:Gem::Version
208
- version: '1.16'
180
+ version: '1'
209
181
  - !ruby/object:Gem::Dependency
210
182
  name: rubocop-rake
211
183
  requirement: !ruby/object:Gem::Requirement
@@ -269,6 +241,7 @@ files:
269
241
  - lib/switchman/active_record/attribute_methods.rb
270
242
  - lib/switchman/active_record/base.rb
271
243
  - lib/switchman/active_record/calculations.rb
244
+ - lib/switchman/active_record/connection_handler.rb
272
245
  - lib/switchman/active_record/connection_pool.rb
273
246
  - lib/switchman/active_record/database_configurations.rb
274
247
  - lib/switchman/active_record/database_configurations/database_config.rb
@@ -276,6 +249,7 @@ files:
276
249
  - lib/switchman/active_record/log_subscriber.rb
277
250
  - lib/switchman/active_record/migration.rb
278
251
  - lib/switchman/active_record/model_schema.rb
252
+ - lib/switchman/active_record/pending_migration_connection.rb
279
253
  - lib/switchman/active_record/persistence.rb
280
254
  - lib/switchman/active_record/postgresql_adapter.rb
281
255
  - lib/switchman/active_record/predicate_builder.rb
@@ -315,6 +289,7 @@ licenses:
315
289
  - MIT
316
290
  metadata:
317
291
  rubygems_mfa_required: 'true'
292
+ source_code_uri: https://github.com/instructure/switchman
318
293
  post_install_message:
319
294
  rdoc_options: []
320
295
  require_paths:
@@ -323,14 +298,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
323
298
  requirements:
324
299
  - - ">="
325
300
  - !ruby/object:Gem::Version
326
- version: '2.7'
301
+ version: '3.0'
327
302
  required_rubygems_version: !ruby/object:Gem::Requirement
328
303
  requirements:
329
304
  - - ">="
330
305
  - !ruby/object:Gem::Version
331
306
  version: '0'
332
307
  requirements: []
333
- rubygems_version: 3.1.6
308
+ rubygems_version: 3.2.33
334
309
  signing_key:
335
310
  specification_version: 4
336
311
  summary: Rails sharding magic