switchman 3.0.2 → 3.1.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/lib/switchman/action_controller/caching.rb +2 -2
  6. data/lib/switchman/active_record/abstract_adapter.rb +2 -13
  7. data/lib/switchman/active_record/associations.rb +223 -0
  8. data/lib/switchman/active_record/attribute_methods.rb +144 -63
  9. data/lib/switchman/active_record/base.rb +100 -43
  10. data/lib/switchman/active_record/calculations.rb +12 -5
  11. data/lib/switchman/active_record/connection_pool.rb +9 -31
  12. data/lib/switchman/active_record/database_configurations.rb +18 -2
  13. data/lib/switchman/active_record/finder_methods.rb +2 -2
  14. data/lib/switchman/active_record/migration.rb +7 -4
  15. data/lib/switchman/active_record/model_schema.rb +1 -1
  16. data/lib/switchman/active_record/persistence.rb +7 -2
  17. data/lib/switchman/active_record/postgresql_adapter.rb +6 -2
  18. data/lib/switchman/active_record/predicate_builder.rb +1 -1
  19. data/lib/switchman/active_record/query_methods.rb +27 -14
  20. data/lib/switchman/active_record/reflection.rb +1 -1
  21. data/lib/switchman/active_record/relation.rb +25 -24
  22. data/lib/switchman/active_record/statement_cache.rb +2 -2
  23. data/lib/switchman/active_record/table_definition.rb +1 -1
  24. data/lib/switchman/active_record/test_fixtures.rb +43 -0
  25. data/lib/switchman/active_support/cache.rb +16 -0
  26. data/lib/switchman/arel.rb +28 -6
  27. data/lib/switchman/database_server.rb +71 -65
  28. data/lib/switchman/default_shard.rb +0 -2
  29. data/lib/switchman/engine.rb +67 -125
  30. data/lib/switchman/errors.rb +4 -2
  31. data/lib/switchman/guard_rail/relation.rb +6 -9
  32. data/lib/switchman/guard_rail.rb +5 -0
  33. data/lib/switchman/parallel.rb +68 -0
  34. data/lib/switchman/r_spec_helper.rb +5 -17
  35. data/lib/switchman/rails.rb +1 -4
  36. data/{app/models → lib}/switchman/shard.rb +61 -188
  37. data/lib/switchman/sharded_instrumenter.rb +1 -1
  38. data/lib/switchman/standard_error.rb +11 -12
  39. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  40. data/lib/switchman/version.rb +1 -1
  41. data/lib/switchman.rb +22 -2
  42. data/lib/tasks/switchman.rake +24 -13
  43. metadata +24 -22
  44. data/lib/switchman/active_record/association.rb +0 -206
  45. data/lib/switchman/open4.rb +0 -80
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/database_server'
4
- require 'switchman/default_shard'
5
- require 'switchman/environment'
6
- require 'switchman/errors'
7
-
8
3
  module Switchman
9
4
  class Shard < UnshardedRecord
10
5
  # ten trillion possible ids per shard. yup.
@@ -39,7 +34,7 @@ module Switchman
39
34
  # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
40
35
  @default ||= default
41
36
 
42
- # Now find the actual record, if it exists; rescue the fake default if the table doesn't exist
37
+ # Now find the actual record, if it exists
43
38
  @default = begin
44
39
  find_cached('default_shard') { Shard.where(default: true).take } || default
45
40
  rescue
@@ -59,34 +54,38 @@ module Switchman
59
54
 
60
55
  def current(klass = ::ActiveRecord::Base)
61
56
  klass ||= ::ActiveRecord::Base
62
- klass.connection_pool.shard
57
+ klass.current_switchman_shard
63
58
  end
64
59
 
65
60
  def activate(shards)
66
61
  activated_classes = activate!(shards)
67
62
  yield
68
63
  ensure
69
- activated_classes.each do |klass|
70
- klass.connection_pool.shard_stack.pop
64
+ activated_classes&.each do |klass|
71
65
  klass.connected_to_stack.pop
72
66
  end
73
67
  end
74
68
 
75
69
  def activate!(shards)
76
- activated_classes = []
70
+ activated_classes = nil
77
71
  shards.each do |klass, shard|
78
72
  next if klass == UnshardedRecord
79
73
 
80
74
  next unless klass.current_shard != shard.database_server.id.to_sym ||
81
- klass.connection_pool.shard != shard
75
+ klass.current_switchman_shard != shard
82
76
 
83
- activated_classes << klass
84
- klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass] }
85
- klass.connection_pool.shard_stack << shard
77
+ (activated_classes ||= []) << klass
78
+ klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
86
79
  end
87
80
  activated_classes
88
81
  end
89
82
 
83
+ def active_shards
84
+ sharded_models.map do |klass|
85
+ [klass, current(klass)]
86
+ end.compact.to_h
87
+ end
88
+
90
89
  def lookup(id)
91
90
  id_i = id.to_i
92
91
  return current if id_i == current.id || id == 'self'
@@ -103,6 +102,11 @@ module Switchman
103
102
  cached_shards[id]
104
103
  end
105
104
 
105
+ def preload_cache
106
+ cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
107
+ cached_shards.reverse_merge!(all.index_by(&:id))
108
+ end
109
+
106
110
  def clear_cache
107
111
  cached_shards.clear
108
112
  end
@@ -111,14 +115,12 @@ module Switchman
111
115
  #
112
116
  # * +shards+ - an array or relation of Shards to iterate over
113
117
  # * +classes+ - an array of classes to activate
114
- # parallel: - true/false to execute in parallel, or a integer of how many
115
- # sub-processes per database server. Note that parallel
116
- # invocation currently uses forking, so should be used sparingly
117
- # because errors are not raised, and you cannot get results back
118
- # max_procs: - only run this many parallel processes at a time
118
+ # parallel: - true/false to execute in parallel, or an integer of how many
119
+ # sub-processes. Note that parallel invocation currently uses
120
+ # forking.
119
121
  # exception: - :ignore, :raise, :defer (wait until the end and raise the first
120
122
  # error), or a proc
121
- def with_each_shard(*args, parallel: false, max_procs: nil, exception: :raise, &block)
123
+ def with_each_shard(*args, parallel: false, exception: :raise, &block)
122
124
  raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
123
125
 
124
126
  return Array.wrap(yield) unless default.is_a?(Shard)
@@ -133,14 +135,13 @@ module Switchman
133
135
  scope, classes = args
134
136
  end
135
137
 
136
- parallel = 1 if parallel == true
138
+ parallel = [Environment.cpu_count || 2, 2].min if parallel == true
137
139
  parallel = 0 if parallel == false || parallel.nil?
138
140
 
139
141
  scope ||= Shard.all
140
142
  scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
141
143
 
142
- if parallel.positive?
143
- max_procs = determine_max_procs(max_procs, parallel)
144
+ if parallel > 1
144
145
  if ::ActiveRecord::Relation === scope
145
146
  # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
146
147
  database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
@@ -148,67 +149,11 @@ module Switchman
148
149
  # nothing to do
149
150
  return if database_servers.count.zero?
150
151
 
151
- parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
152
-
153
- scopes = database_servers.map do |server|
154
- server_scope = server.shards.merge(scope)
155
- if parallel == 1
156
- subscopes = [server_scope]
157
- else
158
- subscopes = []
159
- total = server_scope.count
160
- ranges = []
161
- server_scope.find_ids_in_ranges(batch_size: (total.to_f / parallel).ceil) do |min, max|
162
- ranges << [min, max]
163
- end
164
- # create a half-open range on the last one
165
- ranges.last[1] = nil
166
- ranges.each do |min, max|
167
- subscope = server_scope.where('id>=?', min)
168
- subscope = subscope.where('id<=?', max) if max
169
- subscopes << subscope
170
- end
171
- end
172
- [server, subscopes]
173
- end.to_h
152
+ scopes = database_servers.to_h do |server|
153
+ [server, scope.merge(server.shards)]
154
+ end
174
155
  else
175
156
  scopes = scope.group_by(&:database_server)
176
- if parallel > 1
177
- parallel = [(max_procs.to_f / scopes.count).ceil, parallel].min if max_procs
178
- scopes = scopes.map do |(server, shards)|
179
- [server, shards.in_groups(parallel, false).compact]
180
- end.to_h
181
- else
182
- scopes = scopes.map { |(server, shards)| [server, [shards]] }.to_h
183
- end
184
- end
185
-
186
- exception_pipes = []
187
- pids = []
188
- out_fds = []
189
- err_fds = []
190
- pid_to_name_map = {}
191
- fd_to_name_map = {}
192
- errors = []
193
-
194
- wait_for_output = lambda do
195
- ready, = IO.select(out_fds + err_fds)
196
- ready.each do |fd|
197
- if fd.eof?
198
- fd.close
199
- out_fds.delete(fd)
200
- err_fds.delete(fd)
201
- next
202
- end
203
- line = fd.readline
204
- puts "#{fd_to_name_map[fd]}: #{line}"
205
- end
206
- end
207
-
208
- # only one process; don't bother forking
209
- if scopes.length == 1 && parallel == 1
210
- return with_each_shard(scopes.first.last.first, classes, exception: exception,
211
- &block)
212
157
  end
213
158
 
214
159
  # clear connections prior to forking (no more queries will be executed in the parent,
@@ -216,95 +161,36 @@ module Switchman
216
161
  # silly like dealloc'ing prepared statements)
217
162
  ::ActiveRecord::Base.clear_all_connections!
218
163
 
219
- scopes.each do |server, subscopes|
220
- subscopes.each_with_index do |subscope, idx|
221
- name = if subscopes.length > 1
222
- "#{server.id} #{idx + 1}"
223
- else
224
- server.id
225
- end
226
-
227
- exception_pipe = IO.pipe
228
- exception_pipes << exception_pipe
229
- pid, io_in, io_out, io_err = Open4.pfork4(lambda do
230
- Switchman.config[:on_fork_proc]&.call
231
-
232
- # set a pretty name for the process title, up to 128 characters
233
- # (we don't actually know the limit, depending on how the process
234
- # was started)
235
- # first, simplify the binary name by stripping directories,
236
- # then truncate arguments as necessary
237
- bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
238
- max_length = 128 - bin.length - name.length - 3
239
- args = ARGV.join(' ')
240
- args = args[0..max_length] if max_length >= 0
241
- new_title = [bin, args, name].join(' ')
242
- Process.setproctitle(new_title)
243
-
244
- with_each_shard(subscope, classes, exception: exception, &block)
245
- exception_pipe.last.close
246
- rescue => e
247
- begin
248
- dumped = Marshal.dump(e)
249
- rescue
250
- # couldn't dump the exception; create a copy with just
251
- # the message and the backtrace
252
- e2 = e.class.new(e.message)
253
- e2.set_backtrace(e.backtrace)
254
- e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
255
- dumped = Marshal.dump(e2)
256
- end
257
- exception_pipe.last.set_encoding(dumped.encoding)
258
- exception_pipe.last.write(dumped)
259
- exception_pipe.last.flush
260
- exception_pipe.last.close
261
- exit! 1
262
- end)
263
- exception_pipe.last.close
264
- pids << pid
265
- io_in.close # don't care about writing to stdin
266
- out_fds << io_out
267
- err_fds << io_err
268
- pid_to_name_map[pid] = name
269
- fd_to_name_map[io_out] = name
270
- fd_to_name_map[io_err] = name
271
-
272
- while max_procs && pids.count >= max_procs
273
- while max_procs && out_fds.count >= max_procs
274
- # wait for output if we've hit the max_procs limit
275
- wait_for_output.call
276
- end
277
- # we've gotten all the output from one fd so wait for its child process to exit
278
- found_pid, status = Process.wait2
279
- pids.delete(found_pid)
280
- errors << pid_to_name_map[found_pid] if status.exitstatus != 0
281
- end
164
+ parent_process_name = `ps -ocommand= -p#{Process.pid}`.slice(/#{$0}.*/)
165
+ ret = ::Parallel.map(scopes, in_processes: scopes.length > 1 ? parallel : 0) do |server, subscope|
166
+ name = server.id
167
+ # rubocop:disable Style/GlobalStdStream
168
+ $stdout = Parallel::PrefixingIO.new(name, STDOUT)
169
+ $stderr = Parallel::PrefixingIO.new(name, STDERR)
170
+ # rubocop:enable Style/GlobalStdStream
171
+ begin
172
+ max_length = 128 - name.length - 3
173
+ short_parent_name = parent_process_name[0..max_length] if max_length >= 0
174
+ new_title = [short_parent_name, name].join(' ')
175
+ Process.setproctitle(new_title)
176
+ Switchman.config[:on_fork_proc]&.call
177
+ with_each_shard(subscope, classes, exception: exception, &block).map { |result| Parallel::ResultWrapper.new(result) }
178
+ rescue => e
179
+ logger.error e.full_message
180
+ Parallel::QuietExceptionWrapper.new(name, ::Parallel::ExceptionWrapper.new(e))
282
181
  end
283
- end
284
-
285
- wait_for_output.call while out_fds.any? || err_fds.any?
286
- pids.each do |pid|
287
- _, status = Process.waitpid2(pid)
288
- errors << pid_to_name_map[pid] if status.exitstatus != 0
289
- end
290
-
291
- # check for an exception; we only re-raise the first one
292
- exception_pipes.each do |exception_pipe|
293
- serialized_exception = exception_pipe.first.read
294
- next if serialized_exception.empty?
295
-
296
- ex = Marshal.load(serialized_exception) # rubocop:disable Security/MarshalLoad
297
- raise ex
298
- ensure
299
- exception_pipe.first.close
300
- end
182
+ end.flatten
301
183
 
184
+ errors = ret.select { |val| val.is_a?(Parallel::QuietExceptionWrapper) }
302
185
  unless errors.empty?
303
- raise ParallelShardExecError,
304
- "The following subprocesses did not exit cleanly: #{errors.sort.join(', ')}"
186
+ raise errors.first.exception if errors.length == 1
187
+
188
+ raise Errors::ParallelShardExecError,
189
+ "The following database server(s) did not finish processing cleanly: #{errors.map(&:name).sort.join(', ')}",
190
+ cause: errors.first.exception
305
191
  end
306
192
 
307
- return
193
+ return ret.map(&:result)
308
194
  end
309
195
 
310
196
  classes ||= []
@@ -402,7 +288,7 @@ module Switchman
402
288
  signed_id_operation(local_id) do |id|
403
289
  return nil if id > IDS_PER_SHARD
404
290
 
405
- $1.to_i * IDS_PER_SHARD + id
291
+ ($1.to_i * IDS_PER_SHARD) + id
406
292
  end
407
293
  when Integer, /^-?\d+$/
408
294
  any_id.to_i
@@ -480,32 +366,15 @@ module Switchman
480
366
  shard || source_shard || Shard.current
481
367
  end
482
368
 
483
- # given the provided option, determines whether we need to (and whether
484
- # it's possible) to determine a reasonable default.
485
- def determine_max_procs(max_procs_input, parallel_input = 2)
486
- max_procs = nil
487
- if max_procs_input
488
- max_procs = max_procs_input.to_i
489
- max_procs = nil if max_procs.zero?
490
- else
491
- return 1 if parallel_input.nil? || parallel_input < 1
492
-
493
- cpus = Environment.cpu_count
494
- max_procs = cpus * parallel_input if cpus&.positive?
495
- end
496
-
497
- max_procs
498
- end
499
-
500
369
  private
501
370
 
502
371
  def add_sharded_model(klass)
503
372
  @sharded_models = (sharded_models + [klass]).freeze
504
- initialize_sharding
373
+ configure_connects_to
505
374
  end
506
375
 
507
- def initialize_sharding
508
- full_connects_to_hash = DatabaseServer.all.map { |db| [db.id.to_sym, db.connects_to_hash] }.to_h
376
+ def configure_connects_to
377
+ full_connects_to_hash = DatabaseServer.all.to_h { |db| [db.id.to_sym, db.connects_to_hash] }
509
378
  sharded_models.each do |klass|
510
379
  connects_to_hash = full_connects_to_hash.deep_dup
511
380
  if klass == UnshardedRecord
@@ -596,6 +465,10 @@ module Switchman
596
465
  Shard.default
597
466
  end
598
467
 
468
+ def original_id
469
+ id
470
+ end
471
+
599
472
  def activate(*classes, &block)
600
473
  shards = hashify_classes(classes)
601
474
  Shard.activate(shards, &block)
@@ -668,7 +541,7 @@ module Switchman
668
541
  return nil unless local_id
669
542
 
670
543
  self.class.signed_id_operation(local_id) do |abs_id|
671
- abs_id + id * IDS_PER_SHARD
544
+ abs_id + (id * IDS_PER_SHARD)
672
545
  end
673
546
  end
674
547
 
@@ -16,7 +16,7 @@ module Switchman
16
16
  payload[:shard] = {
17
17
  database_server_id: shard.database_server.id,
18
18
  id: shard.id,
19
- env: shard.database_server.guard_rail_environment
19
+ env: ::Rails.version < '7.0' ? @shard_host.pool.connection_klass&.current_role : @shard_host.pool.connection_class&.current_role
20
20
  }
21
21
  end
22
22
  super name, payload
@@ -3,20 +3,19 @@
3
3
  module Switchman
4
4
  module StandardError
5
5
  def initialize(*args)
6
- # Shard.current can throw this when switchman isn't working right; if we try to
7
- # do our stuff here, it'll cause a SystemStackError, which is a pain to deal with
8
- if is_a?(::ActiveRecord::ConnectionNotEstablished)
9
- super
10
- return
11
- end
6
+ super
7
+ # These seem to get themselves into a bad state if we try to lookup shards while processing
8
+ return if is_a?(IO::EAGAINWaitReadable)
12
9
 
13
- if defined?(Shard)
14
- @active_shards = Shard.sharded_models.map do |klass|
15
- [klass, Shard.current(klass)]
16
- end.compact.to_h
17
- end
10
+ return if Thread.current[:switchman_error_handler]
18
11
 
19
- super
12
+ begin
13
+ Thread.current[:switchman_error_handler] = true
14
+
15
+ @active_shards ||= Shard.active_shards
16
+ ensure
17
+ Thread.current[:switchman_error_handler] = nil
18
+ end
20
19
  end
21
20
 
22
21
  def current_shard(klass = ::ActiveRecord::Base)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Switchman
4
4
  class UnshardedRecord < ::ActiveRecord::Base
5
- sharded_model
5
+ self.abstract_class = true
6
6
  end
7
7
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- VERSION = '3.0.2'
4
+ VERSION = '3.1.0'
5
5
  end
data/lib/switchman.rb CHANGED
@@ -1,8 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'guard_rail'
4
- require 'switchman/open4'
5
- require 'switchman/engine'
4
+ require 'zeitwerk'
5
+
6
+ class SwitchmanInflector < Zeitwerk::GemInflector
7
+ def camelize(basename, abspath)
8
+ if basename =~ /\Apostgresql_(.*)/
9
+ 'PostgreSQL' + super($1, abspath)
10
+ else
11
+ super
12
+ end
13
+ end
14
+ end
15
+
16
+ loader = Zeitwerk::Loader.for_gem
17
+ loader.inflector = SwitchmanInflector.new(__FILE__)
18
+ loader.setup
6
19
 
7
20
  module Switchman
8
21
  def self.config
@@ -18,5 +31,12 @@ module Switchman
18
31
  @cache = cache
19
32
  end
20
33
 
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
36
+ end
37
+
21
38
  class OrderOnMultiShardQuery < RuntimeError; end
22
39
  end
40
+
41
+ # Load the engine and everything associated at gem load time
42
+ Switchman::Engine
@@ -1,5 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # In rails 7.0+ if you have only 1 db in the env it doesn't try to do explicit activation
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'
7
+ ::Rake::Task["#{task_prefix}:migrate"].clear_actions.enhance do
8
+ ::ActiveRecord::Tasks::DatabaseTasks.migrate
9
+ # Ensure this doesn't blow up when running inside the dummy app
10
+ Rake::Task["#{task_prefix}:_dump"].invoke
11
+ end
12
+ end
13
+
3
14
  module Switchman
4
15
  module Rake
5
16
  def self.filter_database_servers(&block)
@@ -35,9 +46,9 @@ module Switchman
35
46
 
36
47
  scope = base_scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id'))
37
48
  if servers != DatabaseServer.all
38
- conditions = ['database_server_id IN (?)', servers.map(&:id)]
39
- conditions.first << ' OR database_server_id IS NULL' if servers.include?(Shard.default.database_server)
40
- scope = scope.where(conditions)
49
+ database_server_ids = servers.map(&:id)
50
+ database_server_ids << nil if servers.include?(Shard.default.database_server)
51
+ scope = scope.where(database_server_id: database_server_ids)
41
52
  end
42
53
 
43
54
  scope = shard_scope(scope, shard) if shard
@@ -46,13 +57,11 @@ module Switchman
46
57
  end
47
58
 
48
59
  def self.options
49
- { parallel: ENV['PARALLEL'].to_i, max_procs: ENV['MAX_PARALLEL_PROCS'] }
60
+ { parallel: ENV['PARALLEL'].to_i }
50
61
  end
51
62
 
52
63
  # classes - an array or proc, to activate as the current shard during the
53
- # task. tasks which modify the schema may want to pass all categories in
54
- # so that schema updates for non-default tables happen against all shards.
55
- # this is handled automatically for the default migration tasks, below.
64
+ # task.
56
65
  def self.shardify_task(task_name, classes: [::ActiveRecord::Base])
57
66
  old_task = ::Rake::Task[task_name]
58
67
  old_actions = old_task.actions.dup
@@ -77,17 +86,15 @@ module Switchman
77
86
  nil
78
87
  end
79
88
  rescue => e
80
- puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel] != 0
89
+ warn "Exception from #{e.current_shard.id}: #{e.current_shard.description}:\n#{e.full_message}" if options[:parallel] != 0
81
90
  raise
82
-
83
- #::ActiveRecord::Base.configurations = old_configurations
84
91
  end
85
92
  end
86
93
  end
87
94
  end
88
95
 
89
96
  %w[db:migrate db:migrate:up db:migrate:down db:rollback].each do |task_name|
90
- shardify_task(task_name, classes: -> { Shard.sharded_models })
97
+ shardify_task(task_name)
91
98
  end
92
99
 
93
100
  def self.shard_scope(scope, raw_shard_ids)
@@ -201,14 +208,18 @@ module Switchman
201
208
  module PostgreSQLDatabaseTasks
202
209
  def structure_dump(filename, extra_flags = nil)
203
210
  set_psql_env
204
- args = ['-s', '-x', '-O', '-f', filename]
211
+ args = ['--schema-only', '--no-privileges', '--no-owner', '--file', filename]
205
212
  args.concat(Array(extra_flags)) if extra_flags
206
213
  shard = Shard.current.name
207
214
  serialized_search_path = shard
208
215
  args << "--schema=#{Shellwords.escape(shard)}"
209
216
 
210
- args << configuration['database']
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
211
221
  run_cmd('pg_dump', args, 'dumping')
222
+ remove_sql_header_comments(filename)
212
223
  File.open(filename, 'a') { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
213
224
  end
214
225
  end