switchman 3.0.1 → 4.2.5

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +16 -15
  3. data/db/migrate/20180828183945_add_default_shard_index.rb +2 -2
  4. data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
  5. data/db/migrate/20190114212900_add_unique_name_indexes.rb +10 -4
  6. data/lib/switchman/action_controller/caching.rb +2 -2
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -18
  8. data/lib/switchman/active_record/associations.rb +315 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +191 -79
  10. data/lib/switchman/active_record/base.rb +204 -50
  11. data/lib/switchman/active_record/calculations.rb +93 -50
  12. data/lib/switchman/active_record/connection_handler.rb +18 -0
  13. data/lib/switchman/active_record/connection_pool.rb +47 -34
  14. data/lib/switchman/active_record/database_configurations.rb +32 -6
  15. data/lib/switchman/active_record/finder_methods.rb +22 -16
  16. data/lib/switchman/active_record/log_subscriber.rb +3 -6
  17. data/lib/switchman/active_record/migration.rb +42 -14
  18. data/lib/switchman/active_record/model_schema.rb +1 -1
  19. data/lib/switchman/active_record/pending_migration_connection.rb +17 -0
  20. data/lib/switchman/active_record/persistence.rb +37 -2
  21. data/lib/switchman/active_record/postgresql_adapter.rb +39 -20
  22. data/lib/switchman/active_record/predicate_builder.rb +2 -2
  23. data/lib/switchman/active_record/query_cache.rb +26 -17
  24. data/lib/switchman/active_record/query_methods.rb +252 -135
  25. data/lib/switchman/active_record/reflection.rb +10 -3
  26. data/lib/switchman/active_record/relation.rb +154 -32
  27. data/lib/switchman/active_record/spawn_methods.rb +3 -7
  28. data/lib/switchman/active_record/statement_cache.rb +13 -9
  29. data/lib/switchman/active_record/table_definition.rb +1 -1
  30. data/lib/switchman/active_record/tasks/database_tasks.rb +6 -1
  31. data/lib/switchman/active_record/test_fixtures.rb +89 -0
  32. data/lib/switchman/active_support/cache.rb +25 -4
  33. data/lib/switchman/arel.rb +20 -7
  34. data/lib/switchman/call_super.rb +2 -2
  35. data/lib/switchman/database_server.rb +123 -83
  36. data/lib/switchman/default_shard.rb +14 -5
  37. data/lib/switchman/engine.rb +85 -131
  38. data/lib/switchman/environment.rb +2 -2
  39. data/lib/switchman/errors.rb +17 -2
  40. data/lib/switchman/guard_rail/relation.rb +7 -10
  41. data/lib/switchman/guard_rail.rb +5 -0
  42. data/lib/switchman/parallel.rb +68 -0
  43. data/lib/switchman/r_spec_helper.rb +17 -28
  44. data/lib/switchman/rails.rb +1 -4
  45. data/{app/models → lib}/switchman/shard.rb +229 -246
  46. data/lib/switchman/sharded_instrumenter.rb +9 -3
  47. data/lib/switchman/shared_schema_cache.rb +11 -0
  48. data/lib/switchman/standard_error.rb +15 -12
  49. data/lib/switchman/test_helper.rb +3 -3
  50. data/{app/models → lib}/switchman/unsharded_record.rb +1 -1
  51. data/lib/switchman/version.rb +1 -1
  52. data/lib/switchman.rb +46 -12
  53. data/lib/tasks/switchman.rake +101 -54
  54. metadata +34 -176
  55. data/lib/switchman/active_record/association.rb +0 -206
  56. data/lib/switchman/open4.rb +0 -80
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'etc'
3
+ require "etc"
4
4
 
5
5
  module Switchman
6
6
  class Environment
7
- def self.cpu_count(nproc_bin = 'nproc')
7
+ def self.cpu_count(nproc_bin = "nproc")
8
8
  return Etc.nprocessors if Etc.respond_to?(:nprocessors)
9
9
 
10
10
  `#{nproc_bin}`.to_i
@@ -1,7 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Switchman
4
- class NonExistentShardError < RuntimeError; end
4
+ module Errors
5
+ class ManuallyCreatedShadowRecordError < RuntimeError
6
+ DEFAULT_MSG = "It looks like you're trying to manually create a shadow record. " \
7
+ "Please use Switchman::ActiveRecord::Base#save_shadow_record instead."
5
8
 
6
- class ParallelShardExecError < RuntimeError; end
9
+ def initialize(msg = DEFAULT_MSG)
10
+ super
11
+ end
12
+ end
13
+
14
+ class NonExistentShardError < RuntimeError; end
15
+
16
+ class ParallelShardExecError < RuntimeError; end
17
+
18
+ class ShadowRecordError < RuntimeError; end
19
+
20
+ class UnshardedTableError < RuntimeError; end
21
+ end
7
22
  end
@@ -5,21 +5,18 @@ module Switchman
5
5
  module Relation
6
6
  def exec_queries(*args)
7
7
  if lock_value
8
- db = Shard.current(connection_classes).database_server
9
- return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
8
+ db = Shard.current(connection_class_for_self).database_server
9
+ db.unguard { super }
10
+ else
11
+ super
10
12
  end
11
- super
12
13
  end
13
14
 
14
15
  %w[update_all delete_all].each do |method|
15
16
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
16
- def #{method}(*args)
17
- db = Shard.current(connection_classes).database_server
18
- if ::GuardRail.environment != db.guard_rail_environment
19
- db.unguard { super }
20
- else
21
- super
22
- end
17
+ def #{method}(*args, **kwargs)
18
+ db = Shard.current(connection_class_for_self).database_server
19
+ db.unguard { super }
23
20
  end
24
21
  RUBY
25
22
  end
@@ -3,6 +3,11 @@
3
3
  module Switchman
4
4
  module GuardRail
5
5
  module ClassMethods
6
+ def environment
7
+ # no overrides so we get the global role, not the role for the default shard
8
+ ::ActiveRecord::Base.current_role(without_overrides: true)
9
+ end
10
+
6
11
  def activate(role)
7
12
  DatabaseServer.send(:reference_role, role)
8
13
  super
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parallel"
4
+
5
+ module Switchman
6
+ module Parallel
7
+ module UndumpableException
8
+ def initialize(original)
9
+ super
10
+ @active_shards = original.instance_variable_get(:@active_shards)
11
+ current_shard
12
+ end
13
+ end
14
+
15
+ class QuietExceptionWrapper
16
+ attr_accessor :name
17
+
18
+ def initialize(name, wrapper)
19
+ @name = name
20
+ @wrapper = wrapper
21
+ end
22
+
23
+ def exception
24
+ @wrapper.exception
25
+ end
26
+ end
27
+
28
+ class UndumpableResult
29
+ attr_reader :name
30
+
31
+ def initialize(result)
32
+ @name = result.inspect
33
+ end
34
+
35
+ def inspect
36
+ "#<UndumpableResult:#{name}>"
37
+ end
38
+ end
39
+
40
+ class ResultWrapper
41
+ attr_reader :result
42
+
43
+ def initialize(result)
44
+ @result =
45
+ begin
46
+ Marshal.dump(result) && result
47
+ rescue
48
+ UndumpableResult.new(result)
49
+ end
50
+ end
51
+ end
52
+
53
+ class TransformingIO
54
+ delegate_missing_to :@original_io
55
+
56
+ def initialize(transformer, original_io)
57
+ @transformer = transformer
58
+ @original_io = original_io
59
+ end
60
+
61
+ def puts(*args)
62
+ args.flatten.each { |arg| @original_io.puts @transformer.call(arg) }
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Parallel::UndumpableException.prepend(Switchman::Parallel::UndumpableException)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'switchman/test_helper'
3
+ require "switchman/test_helper"
4
4
 
5
5
  module Switchman
6
6
  # including this module in your specs will give you several shards to
@@ -34,9 +34,9 @@ module Switchman
34
34
  groups = group.class.descendant_filtered_examples.map(&:example_group).uniq
35
35
  next unless groups.any? { |descendant_group| RSpecHelper.included_in?(descendant_group) }
36
36
 
37
- puts 'Setting up sharding for all specs...'
37
+ puts "Setting up sharding for all specs..."
38
38
  Shard.delete_all
39
- Switchman.cache.delete('default_shard')
39
+ Switchman.cache.delete("default_shard")
40
40
 
41
41
  @@shard1, @@shard2 = TestHelper.recreate_persistent_test_shards
42
42
  @@default_shard = Shard.default
@@ -48,7 +48,7 @@ module Switchman
48
48
  @@shard1 = @@shard1.create_new_shard
49
49
  @@shard2 = @@shard2.create_new_shard
50
50
  rescue => e
51
- warn 'Sharding setup FAILED!:'
51
+ warn "Sharding setup FAILED!:"
52
52
  while e
53
53
  warn "\n#{e}\n"
54
54
  warn e.backtrace
@@ -66,14 +66,17 @@ module Switchman
66
66
  # we'll re-persist in the group's `before :all`; we don't want them to exist
67
67
  # in the db before then
68
68
  Shard.delete_all
69
- Switchman.cache.delete('default_shard')
69
+ Switchman.cache.delete("default_shard")
70
70
  Shard.default(reload: true)
71
- puts 'Done!'
71
+ puts "Done!"
72
72
 
73
+ main_pid = Process.pid
73
74
  at_exit do
75
+ next unless main_pid == Process.pid
76
+
74
77
  # preserve rspec's exit status
75
78
  status = $!.is_a?(::SystemExit) ? $!.status : nil
76
- puts 'Tearing down sharding for all specs'
79
+ puts "Tearing down sharding for all specs"
77
80
  @@shard1.database_server.destroy unless @@shard1.database_server == Shard.default.database_server
78
81
  unless @@keep_the_shards
79
82
  @@shard1.drop_database
@@ -92,7 +95,7 @@ module Switchman
92
95
  dup = @@default_shard.dup
93
96
  dup.id = @@default_shard.id
94
97
  dup.save!
95
- Switchman.cache.delete('default_shard')
98
+ Switchman.cache.delete("default_shard")
96
99
  Shard.default(reload: true)
97
100
  dup = @@shard1.dup
98
101
  dup.id = @@shard1.id
@@ -104,47 +107,33 @@ module Switchman
104
107
  end
105
108
 
106
109
  klass.before do
107
- raise 'Sharding did not set up correctly' if @@sharding_failed
110
+ raise "Sharding did not set up correctly" if @@sharding_failed
108
111
 
109
112
  Shard.clear_cache
110
113
  if use_transactional_tests
111
114
  Shard.default(reload: true)
112
115
  @shard1 = Shard.find(@shard1.id)
113
116
  @shard2 = Shard.find(@shard2.id)
114
- shards = [@shard2]
115
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
116
- shards.each do |shard|
117
- shard.activate do
118
- ::ActiveRecord::Base.connection.begin_transaction joinable: false
119
- end
120
- end
121
117
  end
122
118
  end
123
119
 
124
120
  klass.after do
125
121
  next if @@sharding_failed
126
122
 
127
- if use_transactional_tests
128
- shards = [@shard2]
129
- shards << @shard1 unless @shard1.database_server == Shard.default.database_server
130
- shards.each do |shard|
131
- shard.activate do
132
- ::ActiveRecord::Base.connection.rollback_transaction if ::ActiveRecord::Base.connection.transaction_open?
133
- end
134
- end
135
- end
136
123
  # clean up after specs
137
- DatabaseServer.all.each do |ds|
124
+ DatabaseServer.each do |ds|
138
125
  if ds.fake? && ds != @shard2.database_server
139
126
  ds.shards.delete_all unless use_transactional_tests
140
127
  ds.destroy
141
128
  end
129
+ ds.remove_instance_variable(:@primary_shard_id) if ds.instance_variable_defined?(:@primary_shard_id)
142
130
  end
143
131
  end
144
132
 
145
133
  klass.after(:all) do
146
- Shard.connection.update("TRUNCATE #{Shard.quoted_table_name} CASCADE")
147
- Switchman.cache.delete('default_shard')
134
+ # Don't truncate because that can create some fun cross-connection lock contention
135
+ Shard.delete_all
136
+ Switchman.cache.delete("default_shard")
148
137
  Shard.default(reload: true)
149
138
  end
150
139
  end
@@ -4,10 +4,7 @@ module Switchman
4
4
  module Rails
5
5
  module ClassMethods
6
6
  def self.prepended(klass)
7
- # in Rails 4+, the Rails.cache= method was used during bootstrap to set
8
- # Rails.cache(_without_sharding) to the value from the config file. but now
9
- # that that's done (the bootstrap happened before this module is included
10
- # into Rails), we want to make sure no one tries to assign to Rails.cache,
7
+ # we want to make sure no one tries to assign to Rails.cache,
11
8
  # because it would be wrong w.r.t. sharding.
12
9
  klass.send(:remove_method, :cache=)
13
10
  end