switchman 3.0.4 → 3.0.6
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 +4 -4
- data/app/models/switchman/shard.rb +105 -131
- data/db/migrate/20180828183945_add_default_shard_index.rb +1 -1
- data/db/migrate/20180828192111_add_timestamps_to_shards.rb +1 -1
- data/lib/switchman/active_record/abstract_adapter.rb +0 -9
- data/lib/switchman/active_record/attribute_methods.rb +24 -3
- data/lib/switchman/active_record/base.rb +37 -20
- data/lib/switchman/active_record/connection_pool.rb +9 -29
- data/lib/switchman/active_record/migration.rb +7 -4
- data/lib/switchman/active_record/persistence.rb +7 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +1 -1
- data/lib/switchman/active_record/predicate_builder.rb +1 -1
- data/lib/switchman/active_record/relation.rb +1 -1
- data/lib/switchman/active_record/test_fixtures.rb +43 -0
- data/lib/switchman/database_server.rb +43 -45
- data/lib/switchman/engine.rb +5 -1
- data/lib/switchman/r_spec_helper.rb +2 -22
- data/lib/switchman/standard_error.rb +4 -4
- data/lib/switchman/version.rb +1 -1
- data/lib/tasks/switchman.rake +9 -5
- metadata +8 -7
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 81aeec21e056026642c6aa15bda349e97c8194212f8b79222b4581d5cb2a9909
         | 
| 4 | 
            +
              data.tar.gz: 77bfd14eaf245a399ec6a5b48a9f3106daac6df393d8c55d7dad5eae56966f15
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: bfc71a265b6aea2a48350bcce77dcdcb24cea206c2dbcd4b4e3e113f0615917b314233da2038197d394d2ae961dbc8be18f12d48ccc240d57ca96eb39d4db9e6
         | 
| 7 | 
            +
              data.tar.gz: d392e78986ef050800ad61e064c702d3cb3dc98b97e48cbf42c9a973ae52226672a33ade5944119be7365f9d7f2f17e05097e0e1f2a46b1adab27dfe5169bf87
         | 
| @@ -39,12 +39,17 @@ module Switchman | |
| 39 39 | 
             
                      # the first time we need a dummy dummy for re-entrancy to avoid looping on ourselves
         | 
| 40 40 | 
             
                      @default ||= default
         | 
| 41 41 |  | 
| 42 | 
            -
                      # Now find the actual record, if it exists | 
| 42 | 
            +
                      # Now find the actual record, if it exists
         | 
| 43 43 | 
             
                      @default = begin
         | 
| 44 44 | 
             
                        find_cached('default_shard') { Shard.where(default: true).take } || default
         | 
| 45 | 
            +
                      # If we are *super* early in boot, the connection pool won't exist; we don't want to fill in the default shard yet
         | 
| 46 | 
            +
                      rescue ::ActiveRecord::ConnectionNotEstablished
         | 
| 47 | 
            +
                        nil
         | 
| 48 | 
            +
                      # rescue the fake default if the table doesn't exist
         | 
| 45 49 | 
             
                      rescue
         | 
| 46 50 | 
             
                        default
         | 
| 47 51 | 
             
                      end
         | 
| 52 | 
            +
                      return default unless @default
         | 
| 48 53 |  | 
| 49 54 | 
             
                      # make sure this is not erroneously cached
         | 
| 50 55 | 
             
                      @default.database_server.remove_instance_variable(:@primary_shard) if @default.database_server.instance_variable_defined?(:@primary_shard)
         | 
| @@ -59,34 +64,38 @@ module Switchman | |
| 59 64 |  | 
| 60 65 | 
             
                  def current(klass = ::ActiveRecord::Base)
         | 
| 61 66 | 
             
                    klass ||= ::ActiveRecord::Base
         | 
| 62 | 
            -
                    klass. | 
| 67 | 
            +
                    klass.current_switchman_shard
         | 
| 63 68 | 
             
                  end
         | 
| 64 69 |  | 
| 65 70 | 
             
                  def activate(shards)
         | 
| 66 71 | 
             
                    activated_classes = activate!(shards)
         | 
| 67 72 | 
             
                    yield
         | 
| 68 73 | 
             
                  ensure
         | 
| 69 | 
            -
                    activated_classes | 
| 70 | 
            -
                      klass.connection_pool.shard_stack.pop
         | 
| 74 | 
            +
                    activated_classes&.each do |klass|
         | 
| 71 75 | 
             
                      klass.connected_to_stack.pop
         | 
| 72 76 | 
             
                    end
         | 
| 73 77 | 
             
                  end
         | 
| 74 78 |  | 
| 75 79 | 
             
                  def activate!(shards)
         | 
| 76 | 
            -
                    activated_classes =  | 
| 80 | 
            +
                    activated_classes = nil
         | 
| 77 81 | 
             
                    shards.each do |klass, shard|
         | 
| 78 82 | 
             
                      next if klass == UnshardedRecord
         | 
| 79 83 |  | 
| 80 84 | 
             
                      next unless klass.current_shard != shard.database_server.id.to_sym ||
         | 
| 81 | 
            -
                                  klass. | 
| 85 | 
            +
                                  klass.current_switchman_shard != shard
         | 
| 82 86 |  | 
| 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
         | 
| 87 | 
            +
                      (activated_classes ||= []) << klass
         | 
| 88 | 
            +
                      klass.connected_to_stack << { shard: shard.database_server.id.to_sym, klasses: [klass], switchman_shard: shard }
         | 
| 86 89 | 
             
                    end
         | 
| 87 90 | 
             
                    activated_classes
         | 
| 88 91 | 
             
                  end
         | 
| 89 92 |  | 
| 93 | 
            +
                  def active_shards
         | 
| 94 | 
            +
                    sharded_models.map do |klass|
         | 
| 95 | 
            +
                      [klass, current(klass)]
         | 
| 96 | 
            +
                    end.compact.to_h
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
             | 
| 90 99 | 
             
                  def lookup(id)
         | 
| 91 100 | 
             
                    id_i = id.to_i
         | 
| 92 101 | 
             
                    return current if id_i == current.id || id == 'self'
         | 
| @@ -103,6 +112,11 @@ module Switchman | |
| 103 112 | 
             
                    cached_shards[id]
         | 
| 104 113 | 
             
                  end
         | 
| 105 114 |  | 
| 115 | 
            +
                  def preload_cache
         | 
| 116 | 
            +
                    cached_shards.reverse_merge!(active_shards.values.index_by(&:id))
         | 
| 117 | 
            +
                    cached_shards.reverse_merge!(all.index_by(&:id))
         | 
| 118 | 
            +
                  end
         | 
| 119 | 
            +
             | 
| 106 120 | 
             
                  def clear_cache
         | 
| 107 121 | 
             
                    cached_shards.clear
         | 
| 108 122 | 
             
                  end
         | 
| @@ -111,14 +125,13 @@ module Switchman | |
| 111 125 | 
             
                  #
         | 
| 112 126 | 
             
                  # * +shards+ - an array or relation of Shards to iterate over
         | 
| 113 127 | 
             
                  # * +classes+ - an array of classes to activate
         | 
| 114 | 
            -
                  #    parallel: - true/false to execute in parallel, or  | 
| 115 | 
            -
                  #                sub-processes | 
| 116 | 
            -
                  #                 | 
| 117 | 
            -
                  #                 | 
| 118 | 
            -
                  #    max_procs: - only run this many parallel processes at a time
         | 
| 128 | 
            +
                  #    parallel: - true/false to execute in parallel, or an integer of how many
         | 
| 129 | 
            +
                  #                sub-processes. Note that parallel invocation currently uses
         | 
| 130 | 
            +
                  #                forking, so should be used sparingly because you cannot get
         | 
| 131 | 
            +
                  #                results back
         | 
| 119 132 | 
             
                  #    exception: - :ignore, :raise, :defer (wait until the end and raise the first
         | 
| 120 133 | 
             
                  #                error), or a proc
         | 
| 121 | 
            -
                  def with_each_shard(*args, parallel: false,  | 
| 134 | 
            +
                  def with_each_shard(*args, parallel: false, exception: :raise, &block)
         | 
| 122 135 | 
             
                    raise ArgumentError, "wrong number of arguments (#{args.length} for 0...2)" if args.length > 2
         | 
| 123 136 |  | 
| 124 137 | 
             
                    return Array.wrap(yield) unless default.is_a?(Shard)
         | 
| @@ -133,14 +146,13 @@ module Switchman | |
| 133 146 | 
             
                      scope, classes = args
         | 
| 134 147 | 
             
                    end
         | 
| 135 148 |  | 
| 136 | 
            -
                    parallel =  | 
| 149 | 
            +
                    parallel = [Environment.cpu_count || 2, 2].min if parallel == true
         | 
| 137 150 | 
             
                    parallel = 0 if parallel == false || parallel.nil?
         | 
| 138 151 |  | 
| 139 152 | 
             
                    scope ||= Shard.all
         | 
| 140 153 | 
             
                    scope = scope.order(::Arel.sql('database_server_id IS NOT NULL, database_server_id, id')) if ::ActiveRecord::Relation === scope && scope.order_values.empty?
         | 
| 141 154 |  | 
| 142 | 
            -
                    if parallel | 
| 143 | 
            -
                      max_procs = determine_max_procs(max_procs, parallel)
         | 
| 155 | 
            +
                    if parallel > 1
         | 
| 144 156 | 
             
                      if ::ActiveRecord::Relation === scope
         | 
| 145 157 | 
             
                        # still need a post-uniq, cause the default database server could be NULL or Rails.env in the db
         | 
| 146 158 | 
             
                        database_servers = scope.reorder('database_server_id').select(:database_server_id).distinct.
         | 
| @@ -148,39 +160,11 @@ module Switchman | |
| 148 160 | 
             
                        # nothing to do
         | 
| 149 161 | 
             
                        return if database_servers.count.zero?
         | 
| 150 162 |  | 
| 151 | 
            -
                        parallel = [(max_procs.to_f / database_servers.count).ceil, parallel].min if max_procs
         | 
| 152 | 
            -
             | 
| 153 163 | 
             
                        scopes = database_servers.map do |server|
         | 
| 154 | 
            -
                           | 
| 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]
         | 
| 164 | 
            +
                          [server, server.shards.merge(scope)]
         | 
| 173 165 | 
             
                        end.to_h
         | 
| 174 166 | 
             
                      else
         | 
| 175 167 | 
             
                        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 168 | 
             
                      end
         | 
| 185 169 |  | 
| 186 170 | 
             
                      exception_pipes = []
         | 
| @@ -206,80 +190,83 @@ module Switchman | |
| 206 190 | 
             
                      end
         | 
| 207 191 |  | 
| 208 192 | 
             
                      # only one process; don't bother forking
         | 
| 209 | 
            -
                       | 
| 210 | 
            -
                        return with_each_shard(scopes.first.last.first, classes, exception: exception,
         | 
| 211 | 
            -
                                               &block)
         | 
| 212 | 
            -
                      end
         | 
| 193 | 
            +
                      return with_each_shard(scopes.first.last, classes, exception: exception, &block) if scopes.length == 1
         | 
| 213 194 |  | 
| 214 195 | 
             
                      # clear connections prior to forking (no more queries will be executed in the parent,
         | 
| 215 196 | 
             
                      # and we want them gone so that we don't accidentally use them post-fork doing something
         | 
| 216 197 | 
             
                      # silly like dealloc'ing prepared statements)
         | 
| 217 198 | 
             
                      ::ActiveRecord::Base.clear_all_connections!
         | 
| 218 199 |  | 
| 219 | 
            -
                      scopes.each do |server,  | 
| 220 | 
            -
                         | 
| 221 | 
            -
             | 
| 222 | 
            -
             | 
| 223 | 
            -
             | 
| 224 | 
            -
             | 
| 225 | 
            -
             | 
| 226 | 
            -
             | 
| 227 | 
            -
                           | 
| 228 | 
            -
                           | 
| 229 | 
            -
                           | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
| 232 | 
            -
             | 
| 233 | 
            -
             | 
| 234 | 
            -
             | 
| 235 | 
            -
             | 
| 236 | 
            -
             | 
| 237 | 
            -
             | 
| 238 | 
            -
             | 
| 239 | 
            -
             | 
| 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)
         | 
| 200 | 
            +
                      scopes.each do |server, subscope|
         | 
| 201 | 
            +
                        name = server.id
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                        exception_pipe = IO.pipe
         | 
| 204 | 
            +
                        exception_pipes << exception_pipe
         | 
| 205 | 
            +
                        pid, io_in, io_out, io_err = Open4.pfork4(lambda do
         | 
| 206 | 
            +
                          Switchman.config[:on_fork_proc]&.call
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                          # set a pretty name for the process title, up to 128 characters
         | 
| 209 | 
            +
                          # (we don't actually know the limit, depending on how the process
         | 
| 210 | 
            +
                          # was started)
         | 
| 211 | 
            +
                          # first, simplify the binary name by stripping directories,
         | 
| 212 | 
            +
                          # then truncate arguments as necessary
         | 
| 213 | 
            +
                          bin = File.basename($0) # Process.argv0 doesn't work on Ruby 2.5 (https://bugs.ruby-lang.org/issues/15887)
         | 
| 214 | 
            +
                          max_length = 128 - bin.length - name.length - 3
         | 
| 215 | 
            +
                          args = ARGV.join(' ')
         | 
| 216 | 
            +
                          args = args[0..max_length] if max_length >= 0
         | 
| 217 | 
            +
                          new_title = [bin, args, name].join(' ')
         | 
| 218 | 
            +
                          Process.setproctitle(new_title)
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                          with_each_shard(subscope, classes, exception: exception, &block)
         | 
| 263 221 | 
             
                          exception_pipe.last.close
         | 
| 264 | 
            -
             | 
| 265 | 
            -
                           | 
| 266 | 
            -
             | 
| 267 | 
            -
             | 
| 268 | 
            -
                           | 
| 269 | 
            -
             | 
| 270 | 
            -
                           | 
| 271 | 
            -
             | 
| 272 | 
            -
                           | 
| 273 | 
            -
                             | 
| 274 | 
            -
             | 
| 275 | 
            -
             | 
| 276 | 
            -
                             | 
| 277 | 
            -
                            #  | 
| 278 | 
            -
                             | 
| 279 | 
            -
                             | 
| 280 | 
            -
                             | 
| 222 | 
            +
                        rescue Exception => e # rubocop:disable Lint/RescueException
         | 
| 223 | 
            +
                          begin
         | 
| 224 | 
            +
                            dumped = Marshal.dump(e)
         | 
| 225 | 
            +
                            dumped = nil if dumped.length > 64 * 1024
         | 
| 226 | 
            +
                          rescue
         | 
| 227 | 
            +
                            dumped = nil
         | 
| 228 | 
            +
                          end
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                          if dumped.nil?
         | 
| 231 | 
            +
                            # couldn't dump the exception; create a copy with just
         | 
| 232 | 
            +
                            # the message and the backtrace
         | 
| 233 | 
            +
                            e2 = e.class.new(e.message)
         | 
| 234 | 
            +
                            backtrace = e.backtrace
         | 
| 235 | 
            +
                            # truncate excessively long backtraces
         | 
| 236 | 
            +
                            backtrace = backtrace[0...25] + ['...'] + backtrace[-25..] if backtrace.length > 50
         | 
| 237 | 
            +
                            e2.set_backtrace(backtrace)
         | 
| 238 | 
            +
                            e2.instance_variable_set(:@active_shards, e.instance_variable_get(:@active_shards))
         | 
| 239 | 
            +
                            dumped = Marshal.dump(e2)
         | 
| 281 240 | 
             
                          end
         | 
| 241 | 
            +
                          exception_pipe.last.set_encoding(dumped.encoding)
         | 
| 242 | 
            +
                          exception_pipe.last.write(dumped)
         | 
| 243 | 
            +
                          exception_pipe.last.flush
         | 
| 244 | 
            +
                          exception_pipe.last.close
         | 
| 245 | 
            +
                          exit! 1
         | 
| 246 | 
            +
                        end)
         | 
| 247 | 
            +
                        exception_pipe.last.close
         | 
| 248 | 
            +
                        pids << pid
         | 
| 249 | 
            +
                        io_in.close # don't care about writing to stdin
         | 
| 250 | 
            +
                        out_fds << io_out
         | 
| 251 | 
            +
                        err_fds << io_err
         | 
| 252 | 
            +
                        pid_to_name_map[pid] = name
         | 
| 253 | 
            +
                        fd_to_name_map[io_out] = name
         | 
| 254 | 
            +
                        fd_to_name_map[io_err] = name
         | 
| 255 | 
            +
             | 
| 256 | 
            +
                        while pids.count >= parallel
         | 
| 257 | 
            +
                          while out_fds.count >= parallel
         | 
| 258 | 
            +
                            # wait for output if we've hit the parallel limit
         | 
| 259 | 
            +
                            wait_for_output.call
         | 
| 260 | 
            +
                          end
         | 
| 261 | 
            +
                          # we've gotten all the output from one fd so wait for its child process to exit
         | 
| 262 | 
            +
                          found_pid, status = Process.wait2
         | 
| 263 | 
            +
                          pids.delete(found_pid)
         | 
| 264 | 
            +
                          errors << pid_to_name_map[found_pid] if status.exitstatus != 0
         | 
| 282 265 | 
             
                        end
         | 
| 266 | 
            +
                        # we've gotten all the output from one fd so wait for its child process to exit
         | 
| 267 | 
            +
                        found_pid, status = Process.wait2
         | 
| 268 | 
            +
                        pids.delete(found_pid)
         | 
| 269 | 
            +
                        errors << pid_to_name_map[found_pid] if status.exitstatus != 0
         | 
| 283 270 | 
             
                      end
         | 
| 284 271 |  | 
| 285 272 | 
             
                      wait_for_output.call while out_fds.any? || err_fds.any?
         | 
| @@ -402,7 +389,7 @@ module Switchman | |
| 402 389 | 
             
                      signed_id_operation(local_id) do |id|
         | 
| 403 390 | 
             
                        return nil if id > IDS_PER_SHARD
         | 
| 404 391 |  | 
| 405 | 
            -
                        $1.to_i * IDS_PER_SHARD + id
         | 
| 392 | 
            +
                        ($1.to_i * IDS_PER_SHARD) + id
         | 
| 406 393 | 
             
                      end
         | 
| 407 394 | 
             
                    when Integer, /^-?\d+$/
         | 
| 408 395 | 
             
                      any_id.to_i
         | 
| @@ -480,23 +467,6 @@ module Switchman | |
| 480 467 | 
             
                    shard || source_shard || Shard.current
         | 
| 481 468 | 
             
                  end
         | 
| 482 469 |  | 
| 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 470 | 
             
                  private
         | 
| 501 471 |  | 
| 502 472 | 
             
                  def add_sharded_model(klass)
         | 
| @@ -596,6 +566,10 @@ module Switchman | |
| 596 566 | 
             
                  Shard.default
         | 
| 597 567 | 
             
                end
         | 
| 598 568 |  | 
| 569 | 
            +
                def original_id
         | 
| 570 | 
            +
                  id
         | 
| 571 | 
            +
                end
         | 
| 572 | 
            +
             | 
| 599 573 | 
             
                def activate(*classes, &block)
         | 
| 600 574 | 
             
                  shards = hashify_classes(classes)
         | 
| 601 575 | 
             
                  Shard.activate(shards, &block)
         | 
| @@ -668,7 +642,7 @@ module Switchman | |
| 668 642 | 
             
                  return nil unless local_id
         | 
| 669 643 |  | 
| 670 644 | 
             
                  self.class.signed_id_operation(local_id) do |abs_id|
         | 
| 671 | 
            -
                    abs_id + id * IDS_PER_SHARD
         | 
| 645 | 
            +
                    abs_id + (id * IDS_PER_SHARD)
         | 
| 672 646 | 
             
                  end
         | 
| 673 647 | 
             
                end
         | 
| 674 648 |  | 
| @@ -2,7 +2,7 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            class AddDefaultShardIndex < ActiveRecord::Migration[4.2]
         | 
| 4 4 | 
             
              def change
         | 
| 5 | 
            -
                Switchman::Shard.where(default: nil).update_all(default: false)
         | 
| 5 | 
            +
                Switchman::Shard.where(default: nil).update_all(default: false) if Switchman::Shard.current.default?
         | 
| 6 6 | 
             
                change_column_default :switchman_shards, :default, false
         | 
| 7 7 | 
             
                change_column_null :switchman_shards, :default, false
         | 
| 8 8 | 
             
                options = if connection.adapter_name == 'PostgreSQL'
         | 
| @@ -4,7 +4,7 @@ class AddTimestampsToShards < ActiveRecord::Migration[4.2] | |
| 4 4 | 
             
              disable_ddl_transaction!
         | 
| 5 5 |  | 
| 6 6 | 
             
              def change
         | 
| 7 | 
            -
                add_timestamps :switchman_shards, null: true
         | 
| 7 | 
            +
                add_timestamps :switchman_shards, null: true, if_not_exists: true
         | 
| 8 8 | 
             
                now = Time.now.utc
         | 
| 9 9 | 
             
                Switchman::Shard.update_all(updated_at: now, created_at: now) if Switchman::Shard.current.default?
         | 
| 10 10 | 
             
                change_column_null :switchman_shards, :updated_at, false
         | 
| @@ -40,15 +40,6 @@ module Switchman | |
| 40 40 | 
             
                  ensure
         | 
| 41 41 | 
             
                    @last_query_at = Time.now
         | 
| 42 42 | 
             
                  end
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                  private
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                  def id_value_for_database(value)
         | 
| 47 | 
            -
                    return super unless value.class.sharded_primary_key?
         | 
| 48 | 
            -
             | 
| 49 | 
            -
                    # do this the Rails 4.2 way, so that if Shard.current != self.shard, the id gets transposed
         | 
| 50 | 
            -
                    quote(value.id)
         | 
| 51 | 
            -
                  end
         | 
| 52 43 | 
             
                end
         | 
| 53 44 | 
             
              end
         | 
| 54 45 | 
             
            end
         | 
| @@ -40,7 +40,11 @@ module Switchman | |
| 40 40 | 
             
                      if sharded_column?(attr_name)
         | 
| 41 41 | 
             
                        owner << <<-RUBY
         | 
| 42 42 | 
             
                          def global_#{attr_name}
         | 
| 43 | 
            -
                             | 
| 43 | 
            +
                            raw_value = original_#{attr_name}
         | 
| 44 | 
            +
                            return nil if raw_value.nil?
         | 
| 45 | 
            +
                            return raw_value if raw_value > ::Switchman::Shard::IDS_PER_SHARD
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                            ::Switchman::Shard.global_id_for(raw_value, shard)
         | 
| 44 48 | 
             
                          end
         | 
| 45 49 | 
             
                        RUBY
         | 
| 46 50 | 
             
                      else
         | 
| @@ -52,7 +56,9 @@ module Switchman | |
| 52 56 | 
             
                      if sharded_column?(attr_name)
         | 
| 53 57 | 
             
                        owner << <<-RUBY
         | 
| 54 58 | 
             
                          def local_#{attr_name}
         | 
| 55 | 
            -
                             | 
| 59 | 
            +
                            raw_value = original_#{attr_name}
         | 
| 60 | 
            +
                            return nil if raw_value.nil?
         | 
| 61 | 
            +
                            return raw_value % ::Switchman::Shard::IDS_PER_SHARD
         | 
| 56 62 | 
             
                          end
         | 
| 57 63 | 
             
                        RUBY
         | 
| 58 64 | 
             
                      else
         | 
| @@ -104,7 +110,22 @@ module Switchman | |
| 104 110 | 
             
                          alias_method 'original_#{attr_name}', '#{attr_name}'
         | 
| 105 111 | 
             
                          # and replace with one that transposes the id
         | 
| 106 112 | 
             
                          def #{attr_name}
         | 
| 107 | 
            -
                             | 
| 113 | 
            +
                            raw_value = original_#{attr_name}
         | 
| 114 | 
            +
                            return nil if raw_value.nil?
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                            abs_raw_value = raw_value.abs
         | 
| 117 | 
            +
                            current_shard = ::Switchman::Shard.current(#{connection_classes_code_for_reflection(reflection)})
         | 
| 118 | 
            +
                            same_shard = shard == current_shard
         | 
| 119 | 
            +
                            return raw_value if same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                            value_shard_id = abs_raw_value / ::Switchman::Shard::IDS_PER_SHARD
         | 
| 122 | 
            +
                            # this is a stupid case when someone stuffed a global id for the current shard in instead
         | 
| 123 | 
            +
                            # of a local id
         | 
| 124 | 
            +
                            return raw_value % ::Switchman::Shard::IDS_PER_SHARD if value_shard_id == current_shard.id
         | 
| 125 | 
            +
                            return raw_value if !same_shard && abs_raw_value > ::Switchman::Shard::IDS_PER_SHARD
         | 
| 126 | 
            +
                            return shard.global_id_for(raw_value) if !same_shard && abs_raw_value < ::Switchman::Shard::IDS_PER_SHARD
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                            ::Switchman::Shard.relative_id_for(raw_value, shard, current_shard)
         | 
| 108 129 | 
             
                          end
         | 
| 109 130 |  | 
| 110 131 | 
             
                          alias_method 'original_#{attr_name}=', '#{attr_name}='
         | 
| @@ -77,17 +77,27 @@ module Switchman | |
| 77 77 |  | 
| 78 78 | 
             
                      default_shard
         | 
| 79 79 | 
             
                    end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                    def current_switchman_shard
         | 
| 82 | 
            +
                      connected_to_stack.reverse_each do |hash|
         | 
| 83 | 
            +
                        return hash[:switchman_shard] if hash[:switchman_shard] && hash[:klasses].include?(connection_classes)
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      Shard.default
         | 
| 87 | 
            +
                    end
         | 
| 80 88 | 
             
                  end
         | 
| 81 89 |  | 
| 82 | 
            -
                  def self. | 
| 90 | 
            +
                  def self.prepended(klass)
         | 
| 83 91 | 
             
                    klass.singleton_class.prepend(ClassMethods)
         | 
| 84 | 
            -
             | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def _run_initialize_callbacks
         | 
| 95 | 
            +
                    @shard ||= if self.class.sharded_primary_key?
         | 
| 96 | 
            +
                                 Shard.shard_for(self[self.class.primary_key], Shard.current(self.class.connection_classes))
         | 
| 97 | 
            +
                               else
         | 
| 98 | 
            +
                                 Shard.current(self.class.connection_classes)
         | 
| 99 | 
            +
                               end
         | 
| 100 | 
            +
                    super
         | 
| 91 101 | 
             
                  end
         | 
| 92 102 |  | 
| 93 103 | 
             
                  def shard
         | 
| @@ -107,12 +117,12 @@ module Switchman | |
| 107 117 |  | 
| 108 118 | 
             
                  def save(*, **)
         | 
| 109 119 | 
             
                    @shard_set_in_stone = true
         | 
| 110 | 
            -
                     | 
| 120 | 
            +
                    super
         | 
| 111 121 | 
             
                  end
         | 
| 112 122 |  | 
| 113 123 | 
             
                  def save!(*, **)
         | 
| 114 124 | 
             
                    @shard_set_in_stone = true
         | 
| 115 | 
            -
                     | 
| 125 | 
            +
                    super
         | 
| 116 126 | 
             
                  end
         | 
| 117 127 |  | 
| 118 128 | 
             
                  def destroy
         | 
| @@ -134,6 +144,12 @@ module Switchman | |
| 134 144 | 
             
                    end
         | 
| 135 145 | 
             
                  end
         | 
| 136 146 |  | 
| 147 | 
            +
                  def with_transaction_returning_status
         | 
| 148 | 
            +
                    shard.activate(self.class.connection_classes) do
         | 
| 149 | 
            +
                      super
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
             | 
| 137 153 | 
             
                  def hash
         | 
| 138 154 | 
             
                    self.class.sharded_primary_key? ? self.class.hash ^ global_id.hash : super
         | 
| 139 155 | 
             
                  end
         | 
| @@ -149,19 +165,20 @@ module Switchman | |
| 149 165 | 
             
                    copy
         | 
| 150 166 | 
             
                  end
         | 
| 151 167 |  | 
| 152 | 
            -
                  def  | 
| 153 | 
            -
                     | 
| 168 | 
            +
                  def update_columns(*)
         | 
| 169 | 
            +
                    db = shard.database_server
         | 
| 170 | 
            +
                    return db.unguard { super } if ::GuardRail.environment != db.guard_rail_environment
         | 
| 154 171 |  | 
| 155 | 
            -
                     | 
| 156 | 
            -
                    self.class.connection.quote(id)
         | 
| 172 | 
            +
                    super
         | 
| 157 173 | 
             
                  end
         | 
| 158 174 |  | 
| 159 | 
            -
                  def  | 
| 160 | 
            -
                     | 
| 161 | 
            -
             | 
| 162 | 
            -
                       | 
| 175 | 
            +
                  def id_for_database
         | 
| 176 | 
            +
                    if self.class.sharded_primary_key?
         | 
| 177 | 
            +
                      # It's an int, so so it's safe to just return it without passing it through anything else
         | 
| 178 | 
            +
                      # In theory we should do `@attributes[@primary_key].type.serialize(id)`, but that seems to have surprising side-effects
         | 
| 179 | 
            +
                      id
         | 
| 163 180 | 
             
                    else
         | 
| 164 | 
            -
                       | 
| 181 | 
            +
                      super
         | 
| 165 182 | 
             
                    end
         | 
| 166 183 | 
             
                  end
         | 
| 167 184 |  | 
| @@ -182,7 +199,7 @@ module Switchman | |
| 182 199 | 
             
                        reflection.klass.connection_classes
         | 
| 183 200 | 
             
                      end
         | 
| 184 201 | 
             
                    else
         | 
| 185 | 
            -
                      connection_classes
         | 
| 202 | 
            +
                      self.class.connection_classes
         | 
| 186 203 | 
             
                    end
         | 
| 187 204 | 
             
                  end
         | 
| 188 205 | 
             
                end
         | 
| @@ -5,18 +5,6 @@ require 'switchman/errors' | |
| 5 5 | 
             
            module Switchman
         | 
| 6 6 | 
             
              module ActiveRecord
         | 
| 7 7 | 
             
                module ConnectionPool
         | 
| 8 | 
            -
                  def shard
         | 
| 9 | 
            -
                    shard_stack.last || Shard.default
         | 
| 10 | 
            -
                  end
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                  def shard_stack
         | 
| 13 | 
            -
                    unless (shard_stack = Thread.current.thread_variable_get(tls_key))
         | 
| 14 | 
            -
                      shard_stack = Concurrent::Array.new
         | 
| 15 | 
            -
                      Thread.current.thread_variable_set(tls_key, shard_stack)
         | 
| 16 | 
            -
                    end
         | 
| 17 | 
            -
                    shard_stack
         | 
| 18 | 
            -
                  end
         | 
| 19 | 
            -
             | 
| 20 8 | 
             
                  def default_schema
         | 
| 21 9 | 
             
                    connection unless @schemas
         | 
| 22 10 | 
             
                    # default shard will not switch databases immediately, so it won't be set yet
         | 
| @@ -26,15 +14,15 @@ module Switchman | |
| 26 14 |  | 
| 27 15 | 
             
                  def checkout_new_connection
         | 
| 28 16 | 
             
                    conn = super
         | 
| 29 | 
            -
                    conn.shard =  | 
| 17 | 
            +
                    conn.shard = current_shard
         | 
| 30 18 | 
             
                    conn
         | 
| 31 19 | 
             
                  end
         | 
| 32 20 |  | 
| 33 21 | 
             
                  def connection(switch_shard: true)
         | 
| 34 22 | 
             
                    conn = super()
         | 
| 35 | 
            -
                    raise NonExistentShardError if  | 
| 23 | 
            +
                    raise NonExistentShardError if current_shard.new_record?
         | 
| 36 24 |  | 
| 37 | 
            -
                    switch_database(conn) if conn.shard !=  | 
| 25 | 
            +
                    switch_database(conn) if conn.shard != current_shard && switch_shard
         | 
| 38 26 | 
             
                    conn
         | 
| 39 27 | 
             
                  end
         | 
| 40 28 |  | 
| @@ -44,26 +32,18 @@ module Switchman | |
| 44 32 | 
             
                    flush
         | 
| 45 33 | 
             
                  end
         | 
| 46 34 |  | 
| 47 | 
            -
                  def remove_shard!(shard)
         | 
| 48 | 
            -
                    synchronize do
         | 
| 49 | 
            -
                      # The shard might be currently active, so we need to update our own shard
         | 
| 50 | 
            -
                      self.shard = Shard.default if self.shard == shard
         | 
| 51 | 
            -
                      # Update out any connections that may be using this shard
         | 
| 52 | 
            -
                      @connections.each do |conn|
         | 
| 53 | 
            -
                        # This will also update the connection's shard to the default shard
         | 
| 54 | 
            -
                        switch_database(conn) if conn.shard == shard
         | 
| 55 | 
            -
                      end
         | 
| 56 | 
            -
                    end
         | 
| 57 | 
            -
                  end
         | 
| 58 | 
            -
             | 
| 59 35 | 
             
                  def switch_database(conn)
         | 
| 60 | 
            -
                    @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && ! | 
| 36 | 
            +
                    @schemas = conn.current_schemas if !@schemas && conn.adapter_name == 'PostgreSQL' && !current_shard.database_server.config[:shard_name]
         | 
| 61 37 |  | 
| 62 | 
            -
                    conn.shard =  | 
| 38 | 
            +
                    conn.shard = current_shard
         | 
| 63 39 | 
             
                  end
         | 
| 64 40 |  | 
| 65 41 | 
             
                  private
         | 
| 66 42 |  | 
| 43 | 
            +
                  def current_shard
         | 
| 44 | 
            +
                    connection_klass.current_switchman_shard
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 67 47 | 
             
                  def tls_key
         | 
| 68 48 | 
             
                    "#{object_id}_shard".to_sym
         | 
| 69 49 | 
             
                  end
         | 
| @@ -14,16 +14,19 @@ module Switchman | |
| 14 14 |  | 
| 15 15 | 
             
                  def connection
         | 
| 16 16 | 
             
                    conn = super
         | 
| 17 | 
            -
                    ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base. | 
| 17 | 
            +
                    ::ActiveRecord::Base.connection_pool.switch_database(conn) if conn.shard != ::ActiveRecord::Base.current_switchman_shard
         | 
| 18 18 | 
             
                    conn
         | 
| 19 19 | 
             
                  end
         | 
| 20 20 | 
             
                end
         | 
| 21 21 |  | 
| 22 22 | 
             
                module Migrator
         | 
| 23 | 
            -
                  # significant change:  | 
| 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
         | 
| 24 28 | 
             
                  def generate_migrator_advisory_lock_id
         | 
| 25 | 
            -
                     | 
| 26 | 
            -
                    ::ActiveRecord::Migrator::MIGRATOR_SALT * shard_name_hash
         | 
| 29 | 
            +
                    ::ActiveRecord::Migrator::MIGRATOR_SALT
         | 
| 27 30 | 
             
                  end
         | 
| 28 31 |  | 
| 29 32 | 
             
                  # significant change: strip out prefer_secondary from config
         | 
| @@ -11,6 +11,13 @@ module Switchman | |
| 11 11 | 
             
                  def update_columns(*)
         | 
| 12 12 | 
             
                    shard.activate(self.class.connection_classes) { super }
         | 
| 13 13 | 
             
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def delete
         | 
| 16 | 
            +
                    db = shard.database_server
         | 
| 17 | 
            +
                    return db.unguard { super } unless ::GuardRail.environment == db.guard_rail_environment
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    super
         | 
| 20 | 
            +
                  end
         | 
| 14 21 | 
             
                end
         | 
| 15 22 | 
             
              end
         | 
| 16 23 | 
             
            end
         | 
| @@ -0,0 +1,43 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Switchman
         | 
| 4 | 
            +
              module ActiveRecord
         | 
| 5 | 
            +
                module TestFixtures
         | 
| 6 | 
            +
                  FORBIDDEN_DB_ENVS = %i[development production].freeze
         | 
| 7 | 
            +
                  def setup_fixtures(config = ::ActiveRecord::Base)
         | 
| 8 | 
            +
                    super
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    return unless run_in_transaction?
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                    # Replace the one that activerecord natively uses with a switchman-optimized one
         | 
| 13 | 
            +
                    ::ActiveSupport::Notifications.unsubscribe(@connection_subscriber)
         | 
| 14 | 
            +
                    # Code adapted from the code in rails proper
         | 
| 15 | 
            +
                    @connection_subscriber = ::ActiveSupport::Notifications.subscribe('!connection.active_record') do |_, _, _, _, payload|
         | 
| 16 | 
            +
                      spec_name = payload[:spec_name] if payload.key?(:spec_name)
         | 
| 17 | 
            +
                      shard = payload[:shard] if payload.key?(:shard)
         | 
| 18 | 
            +
                      setup_shared_connection_pool
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      if spec_name && !FORBIDDEN_DB_ENVS.include?(shard)
         | 
| 21 | 
            +
                        begin
         | 
| 22 | 
            +
                          connection = ::ActiveRecord::Base.connection_handler.retrieve_connection(spec_name, shard: shard)
         | 
| 23 | 
            +
                        rescue ::ActiveRecord::ConnectionNotEstablished, ::ActiveRecord::NoDatabaseError
         | 
| 24 | 
            +
                          connection = nil
         | 
| 25 | 
            +
                        end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                        if connection && !@fixture_connections.include?(connection)
         | 
| 28 | 
            +
                          connection.begin_transaction joinable: false, _lazy: false
         | 
| 29 | 
            +
                          connection.pool.lock_thread = true if lock_threads
         | 
| 30 | 
            +
                          @fixture_connections << connection
         | 
| 31 | 
            +
                        end
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def enlist_fixture_connections
         | 
| 37 | 
            +
                    setup_shared_connection_pool
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    ::ActiveRecord::Base.connection_handler.connection_pool_list.reject { |cp| FORBIDDEN_DB_ENVS.include?(cp.db_config.env_name.to_sym) }.map(&:connection)
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
            end
         | 
| @@ -59,7 +59,7 @@ module Switchman | |
| 59 59 | 
             
                  end
         | 
| 60 60 |  | 
| 61 61 | 
             
                  def database_servers
         | 
| 62 | 
            -
                     | 
| 62 | 
            +
                    if !@database_servers || @database_servers.empty?
         | 
| 63 63 | 
             
                      @database_servers = {}.with_indifferent_access
         | 
| 64 64 | 
             
                      ::ActiveRecord::Base.configurations.configurations.each do |config|
         | 
| 65 65 | 
             
                        if config.name.include?('/')
         | 
| @@ -186,58 +186,56 @@ module Switchman | |
| 186 186 |  | 
| 187 187 | 
             
                  name ||= "#{config[:database]}_shard_#{id}"
         | 
| 188 188 |  | 
| 189 | 
            +
                  schema_already_existed = false
         | 
| 190 | 
            +
                  shard = nil
         | 
| 189 191 | 
             
                  Shard.connection.transaction do
         | 
| 190 | 
            -
                     | 
| 191 | 
            -
             | 
| 192 | 
            -
             | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 195 | 
            -
             | 
| 196 | 
            -
                       | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
                          if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
         | 
| 201 | 
            -
                            schema_already_existed = true
         | 
| 202 | 
            -
                            raise 'This schema already exists; cannot overwrite'
         | 
| 203 | 
            -
                          end
         | 
| 204 | 
            -
                          Array(create_statement.call).each do |stmt|
         | 
| 205 | 
            -
                            ::ActiveRecord::Base.connection.execute(stmt)
         | 
| 206 | 
            -
                          end
         | 
| 192 | 
            +
                    self.class.creating_new_shard = true
         | 
| 193 | 
            +
                    DatabaseServer.send(:reference_role, :deploy)
         | 
| 194 | 
            +
                    ::ActiveRecord::Base.connected_to(shard: self.id.to_sym, role: :deploy) do
         | 
| 195 | 
            +
                      shard = Shard.create!(id: id,
         | 
| 196 | 
            +
                                            name: name,
         | 
| 197 | 
            +
                                            database_server_id: self.id)
         | 
| 198 | 
            +
                      if create_statement
         | 
| 199 | 
            +
                        if ::ActiveRecord::Base.connection.select_value("SELECT 1 FROM pg_namespace WHERE nspname=#{::ActiveRecord::Base.connection.quote(name)}")
         | 
| 200 | 
            +
                          schema_already_existed = true
         | 
| 201 | 
            +
                          raise 'This schema already exists; cannot overwrite'
         | 
| 207 202 | 
             
                        end
         | 
| 208 | 
            -
                         | 
| 209 | 
            -
                           | 
| 210 | 
            -
             | 
| 203 | 
            +
                        Array(create_statement.call).each do |stmt|
         | 
| 204 | 
            +
                          ::ActiveRecord::Base.connection.execute(stmt)
         | 
| 205 | 
            +
                        end
         | 
| 206 | 
            +
                      end
         | 
| 207 | 
            +
                      if config[:adapter] == 'postgresql'
         | 
| 208 | 
            +
                        old_proc = ::ActiveRecord::Base.connection.raw_connection.set_notice_processor do
         | 
| 211 209 | 
             
                        end
         | 
| 212 | 
            -
             | 
| 213 | 
            -
             | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
             | 
| 220 | 
            -
             | 
| 221 | 
            -
                             | 
| 222 | 
            -
                            reset_column_information
         | 
| 223 | 
            -
                            ::ActiveRecord::Base.descendants.reject do |m|
         | 
| 224 | 
            -
                              m <= UnshardedRecord || !m.table_exists?
         | 
| 225 | 
            -
                            end.each(&:define_attribute_methods)
         | 
| 210 | 
            +
                      end
         | 
| 211 | 
            +
                      old_verbose = ::ActiveRecord::Migration.verbose
         | 
| 212 | 
            +
                      ::ActiveRecord::Migration.verbose = false
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                      unless schema == false
         | 
| 215 | 
            +
                        shard.activate do
         | 
| 216 | 
            +
                          reset_column_information
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                          ::ActiveRecord::Base.connection.transaction(requires_new: true) do
         | 
| 219 | 
            +
                            ::ActiveRecord::Base.connection.migration_context.migrate
         | 
| 226 220 | 
             
                          end
         | 
| 221 | 
            +
                          reset_column_information
         | 
| 222 | 
            +
                          ::ActiveRecord::Base.descendants.reject do |m|
         | 
| 223 | 
            +
                            m <= UnshardedRecord || !m.table_exists?
         | 
| 224 | 
            +
                          end.each(&:define_attribute_methods)
         | 
| 227 225 | 
             
                        end
         | 
| 228 | 
            -
                      ensure
         | 
| 229 | 
            -
                        ::ActiveRecord::Migration.verbose = old_verbose
         | 
| 230 | 
            -
                        ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
         | 
| 231 226 | 
             
                      end
         | 
| 232 | 
            -
                      shard
         | 
| 233 | 
            -
                    rescue
         | 
| 234 | 
            -
                      shard.destroy
         | 
| 235 | 
            -
                      shard.drop_database rescue nil unless schema_already_existed
         | 
| 236 | 
            -
                      reset_column_information unless schema == false rescue nil
         | 
| 237 | 
            -
                      raise
         | 
| 238 227 | 
             
                    ensure
         | 
| 239 | 
            -
                       | 
| 228 | 
            +
                      ::ActiveRecord::Migration.verbose = old_verbose
         | 
| 229 | 
            +
                      ::ActiveRecord::Base.connection.raw_connection.set_notice_processor(&old_proc) if old_proc
         | 
| 240 230 | 
             
                    end
         | 
| 231 | 
            +
                    shard
         | 
| 232 | 
            +
                  rescue
         | 
| 233 | 
            +
                    shard&.destroy
         | 
| 234 | 
            +
                    shard&.drop_database rescue nil unless schema_already_existed
         | 
| 235 | 
            +
                    reset_column_information unless schema == false rescue nil
         | 
| 236 | 
            +
                    raise
         | 
| 237 | 
            +
                  ensure
         | 
| 238 | 
            +
                    self.class.creating_new_shard = false
         | 
| 241 239 | 
             
                  end
         | 
| 242 240 | 
             
                end
         | 
| 243 241 |  | 
    
        data/lib/switchman/engine.rb
    CHANGED
    
    | @@ -90,6 +90,7 @@ module Switchman | |
| 90 90 | 
             
                    require 'switchman/active_record/statement_cache'
         | 
| 91 91 | 
             
                    require 'switchman/active_record/tasks/database_tasks'
         | 
| 92 92 | 
             
                    require 'switchman/active_record/type_caster'
         | 
| 93 | 
            +
                    require 'switchman/active_record/test_fixtures'
         | 
| 93 94 | 
             
                    require 'switchman/arel'
         | 
| 94 95 | 
             
                    require 'switchman/call_super'
         | 
| 95 96 | 
             
                    require 'switchman/rails'
         | 
| @@ -101,7 +102,7 @@ module Switchman | |
| 101 102 | 
             
                    self.default_shard = ::Rails.env.to_sym
         | 
| 102 103 | 
             
                    self.default_role = :primary
         | 
| 103 104 |  | 
| 104 | 
            -
                     | 
| 105 | 
            +
                    prepend ActiveRecord::Base
         | 
| 105 106 | 
             
                    include ActiveRecord::AttributeMethods
         | 
| 106 107 | 
             
                    include ActiveRecord::Persistence
         | 
| 107 108 | 
             
                    singleton_class.prepend ActiveRecord::ModelSchema::ClassMethods
         | 
| @@ -150,9 +151,12 @@ module Switchman | |
| 150 151 | 
             
                    ::ActiveRecord::Relation.include(CallSuper)
         | 
| 151 152 |  | 
| 152 153 | 
             
                    ::ActiveRecord::PredicateBuilder::AssociationQueryValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
         | 
| 154 | 
            +
                    ::ActiveRecord::PredicateBuilder::PolymorphicArrayValue.prepend(ActiveRecord::PredicateBuilder::AssociationQueryValue)
         | 
| 153 155 |  | 
| 154 156 | 
             
                    ::ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(ActiveRecord::Tasks::DatabaseTasks)
         | 
| 155 157 |  | 
| 158 | 
            +
                    ::ActiveRecord::TestFixtures.prepend(ActiveRecord::TestFixtures)
         | 
| 159 | 
            +
             | 
| 156 160 | 
             
                    ::ActiveRecord::TypeCaster::Map.include(ActiveRecord::TypeCaster::Map)
         | 
| 157 161 | 
             
                    ::ActiveRecord::TypeCaster::Connection.include(ActiveRecord::TypeCaster::Connection)
         | 
| 158 162 |  | 
| @@ -107,32 +107,11 @@ module Switchman | |
| 107 107 | 
             
                    raise 'Sharding did not set up correctly' if @@sharding_failed
         | 
| 108 108 |  | 
| 109 109 | 
             
                    Shard.clear_cache
         | 
| 110 | 
            -
                    if use_transactional_tests
         | 
| 111 | 
            -
                      Shard.default(reload: true)
         | 
| 112 | 
            -
                      @shard1 = Shard.find(@shard1.id)
         | 
| 113 | 
            -
                      @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 | 
            -
                    end
         | 
| 122 110 | 
             
                  end
         | 
| 123 111 |  | 
| 124 112 | 
             
                  klass.after do
         | 
| 125 113 | 
             
                    next if @@sharding_failed
         | 
| 126 114 |  | 
| 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 115 | 
             
                    # clean up after specs
         | 
| 137 116 | 
             
                    DatabaseServer.all.each do |ds|
         | 
| 138 117 | 
             
                      if ds.fake? && ds != @shard2.database_server
         | 
| @@ -143,7 +122,8 @@ module Switchman | |
| 143 122 | 
             
                  end
         | 
| 144 123 |  | 
| 145 124 | 
             
                  klass.after(:all) do
         | 
| 146 | 
            -
                     | 
| 125 | 
            +
                    # Don't truncate because that can create some fun cross-connection lock contention
         | 
| 126 | 
            +
                    Shard.delete_all
         | 
| 147 127 | 
             
                    Switchman.cache.delete('default_shard')
         | 
| 148 128 | 
             
                    Shard.default(reload: true)
         | 
| 149 129 | 
             
                  end
         | 
| @@ -10,10 +10,10 @@ module Switchman | |
| 10 10 | 
             
                    return
         | 
| 11 11 | 
             
                  end
         | 
| 12 12 |  | 
| 13 | 
            -
                   | 
| 14 | 
            -
                    @active_shards = Shard. | 
| 15 | 
            -
             | 
| 16 | 
            -
                     | 
| 13 | 
            +
                  begin
         | 
| 14 | 
            +
                    @active_shards = Shard.active_shards if defined?(Shard)
         | 
| 15 | 
            +
                  rescue ::ActiveRecord::ConnectionNotEstablished
         | 
| 16 | 
            +
                    # If we hit an error really early in boot, activerecord may not be initialized yet
         | 
| 17 17 | 
             
                  end
         | 
| 18 18 |  | 
| 19 19 | 
             
                  super
         | 
    
        data/lib/switchman/version.rb
    CHANGED
    
    
    
        data/lib/tasks/switchman.rake
    CHANGED
    
    | @@ -46,7 +46,7 @@ module Switchman | |
| 46 46 | 
             
                end
         | 
| 47 47 |  | 
| 48 48 | 
             
                def self.options
         | 
| 49 | 
            -
                  { parallel: ENV['PARALLEL'].to_i | 
| 49 | 
            +
                  { parallel: ENV['PARALLEL'].to_i }
         | 
| 50 50 | 
             
                end
         | 
| 51 51 |  | 
| 52 52 | 
             
                # classes - an array or proc, to activate as the current shard during the
         | 
| @@ -80,14 +80,14 @@ module Switchman | |
| 80 80 | 
             
                        puts "Exception from #{e.current_shard.id}: #{e.current_shard.description}" if options[:parallel] != 0
         | 
| 81 81 | 
             
                        raise
         | 
| 82 82 |  | 
| 83 | 
            -
                         | 
| 83 | 
            +
                        # ::ActiveRecord::Base.configurations = old_configurations
         | 
| 84 84 | 
             
                      end
         | 
| 85 85 | 
             
                    end
         | 
| 86 86 | 
             
                  end
         | 
| 87 87 | 
             
                end
         | 
| 88 88 |  | 
| 89 89 | 
             
                %w[db:migrate db:migrate:up db:migrate:down db:rollback].each do |task_name|
         | 
| 90 | 
            -
                  shardify_task(task_name | 
| 90 | 
            +
                  shardify_task(task_name)
         | 
| 91 91 | 
             
                end
         | 
| 92 92 |  | 
| 93 93 | 
             
                def self.shard_scope(scope, raw_shard_ids)
         | 
| @@ -201,14 +201,18 @@ module Switchman | |
| 201 201 | 
             
                module PostgreSQLDatabaseTasks
         | 
| 202 202 | 
             
                  def structure_dump(filename, extra_flags = nil)
         | 
| 203 203 | 
             
                    set_psql_env
         | 
| 204 | 
            -
                    args = ['- | 
| 204 | 
            +
                    args = ['--schema-only', '--no-privileges', '--no-owner', '--file', filename]
         | 
| 205 205 | 
             
                    args.concat(Array(extra_flags)) if extra_flags
         | 
| 206 206 | 
             
                    shard = Shard.current.name
         | 
| 207 207 | 
             
                    serialized_search_path = shard
         | 
| 208 208 | 
             
                    args << "--schema=#{Shellwords.escape(shard)}"
         | 
| 209 209 |  | 
| 210 | 
            -
                     | 
| 210 | 
            +
                    ignore_tables = ::ActiveRecord::SchemaDumper.ignore_tables
         | 
| 211 | 
            +
                    args += ignore_tables.flat_map { |table| ['-T', table] } if ignore_tables.any?
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                    args << db_config.database
         | 
| 211 214 | 
             
                    run_cmd('pg_dump', args, 'dumping')
         | 
| 215 | 
            +
                    remove_sql_header_comments(filename)
         | 
| 212 216 | 
             
                    File.open(filename, 'a') { |f| f << "SET search_path TO #{serialized_search_path};\n\n" }
         | 
| 213 217 | 
             
                  end
         | 
| 214 218 | 
             
                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.0. | 
| 4 | 
            +
              version: 3.0.6
         | 
| 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:  | 
| 13 | 
            +
            date: 2022-02-11 00:00:00.000000000 Z
         | 
| 14 14 | 
             
            dependencies:
         | 
| 15 15 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 16 16 | 
             
              name: activerecord
         | 
| @@ -18,7 +18,7 @@ dependencies: | |
| 18 18 | 
             
                requirements:
         | 
| 19 19 | 
             
                - - ">="
         | 
| 20 20 | 
             
                  - !ruby/object:Gem::Version
         | 
| 21 | 
            -
                    version:  | 
| 21 | 
            +
                    version: 6.1.4
         | 
| 22 22 | 
             
                - - "<"
         | 
| 23 23 | 
             
                  - !ruby/object:Gem::Version
         | 
| 24 24 | 
             
                    version: '6.2'
         | 
| @@ -28,7 +28,7 @@ dependencies: | |
| 28 28 | 
             
                requirements:
         | 
| 29 29 | 
             
                - - ">="
         | 
| 30 30 | 
             
                  - !ruby/object:Gem::Version
         | 
| 31 | 
            -
                    version:  | 
| 31 | 
            +
                    version: 6.1.4
         | 
| 32 32 | 
             
                - - "<"
         | 
| 33 33 | 
             
                  - !ruby/object:Gem::Version
         | 
| 34 34 | 
             
                    version: '6.2'
         | 
| @@ -86,14 +86,14 @@ dependencies: | |
| 86 86 | 
             
                requirements:
         | 
| 87 87 | 
             
                - - "~>"
         | 
| 88 88 | 
             
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            -
                    version:  | 
| 89 | 
            +
                    version: 2.3.0
         | 
| 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:  | 
| 96 | 
            +
                    version: 2.3.0
         | 
| 97 97 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 98 98 | 
             
              name: byebug
         | 
| 99 99 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -275,6 +275,7 @@ files: | |
| 275 275 | 
             
            - lib/switchman/active_record/statement_cache.rb
         | 
| 276 276 | 
             
            - lib/switchman/active_record/table_definition.rb
         | 
| 277 277 | 
             
            - lib/switchman/active_record/tasks/database_tasks.rb
         | 
| 278 | 
            +
            - lib/switchman/active_record/test_fixtures.rb
         | 
| 278 279 | 
             
            - lib/switchman/active_record/type_caster.rb
         | 
| 279 280 | 
             
            - lib/switchman/active_support/cache.rb
         | 
| 280 281 | 
             
            - lib/switchman/arel.rb
         | 
| @@ -313,7 +314,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 313 314 | 
             
                - !ruby/object:Gem::Version
         | 
| 314 315 | 
             
                  version: '0'
         | 
| 315 316 | 
             
            requirements: []
         | 
| 316 | 
            -
            rubygems_version: 3. | 
| 317 | 
            +
            rubygems_version: 3.1.4
         | 
| 317 318 | 
             
            signing_key:
         | 
| 318 319 | 
             
            specification_version: 4
         | 
| 319 320 | 
             
            summary: Rails sharding magic
         |