jetpants 0.7.10 → 0.8.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.
- data/README.rdoc +7 -3
- data/bin/jetpants +67 -33
- data/doc/commands.rdoc +2 -0
- data/doc/configuration.rdoc +3 -2
- data/doc/jetpants_collins.rdoc +1 -1
- data/doc/plugins.rdoc +4 -0
- data/doc/requirements.rdoc +1 -1
- data/lib/jetpants.rb +21 -4
- data/lib/jetpants/db.rb +4 -0
- data/lib/jetpants/db/client.rb +10 -4
- data/lib/jetpants/db/import_export.rb +28 -11
- data/lib/jetpants/db/privileges.rb +59 -10
- data/lib/jetpants/db/replication.rb +69 -20
- data/lib/jetpants/db/server.rb +41 -19
- data/lib/jetpants/db/state.rb +99 -8
- data/lib/jetpants/host.rb +33 -35
- data/lib/jetpants/monkeypatch.rb +9 -0
- data/lib/jetpants/pool.rb +14 -7
- data/lib/jetpants/shard.rb +10 -12
- data/lib/jetpants/table.rb +4 -0
- data/lib/jetpants/topology.rb +28 -9
- data/plugins/jetpants_collins/db.rb +12 -0
- data/plugins/jetpants_collins/jetpants_collins.rb +34 -7
- data/plugins/jetpants_collins/pool.rb +6 -2
- data/plugins/jetpants_collins/topology.rb +44 -6
- data/plugins/simple_tracker/topology.rb +1 -0
- metadata +33 -29
    
        data/README.rdoc
    CHANGED
    
    | @@ -76,12 +76,16 @@ If you have a question that isn't covered here, please feel free to email the au | |
| 76 76 |  | 
| 77 77 | 
             
            == CREDITS:
         | 
| 78 78 |  | 
| 79 | 
            -
            * <b>Evan Elias</b>: Lead developer | 
| 80 | 
            -
            * <b>Dallas Marlow</b>:  | 
| 79 | 
            +
            * <b>Evan Elias</b>: Lead developer
         | 
| 80 | 
            +
            * <b>Dallas Marlow</b>: Developer
         | 
| 81 | 
            +
            * <b>Bob Patterson Jr</b>: Developer
         | 
| 82 | 
            +
            * <b>Tom Christ</b>: Developer
         | 
| 83 | 
            +
             | 
| 84 | 
            +
            Special thanks to <b>Tim Ellis</b> for testing and bug reports.
         | 
| 81 85 |  | 
| 82 86 | 
             
            == LICENSE:
         | 
| 83 87 |  | 
| 84 | 
            -
            Copyright  | 
| 88 | 
            +
            Copyright 2013 Tumblr, Inc.
         | 
| 85 89 |  | 
| 86 90 | 
             
            Licensed under the Apache License, Version 2.0 (the "License");
         | 
| 87 91 | 
             
            you may not use this file except in compliance with the License.
         | 
    
        data/bin/jetpants
    CHANGED
    
    | @@ -4,8 +4,6 @@ | |
| 4 4 | 
             
            # module in a Thor command processor, providing a command-line interface to
         | 
| 5 5 | 
             
            # common Jetpants functionality.
         | 
| 6 6 |  | 
| 7 | 
            -
            jetpants_base_dir = File.expand_path(File.dirname(__FILE__) + '/..')
         | 
| 8 | 
            -
            $:.unshift File.join(jetpants_base_dir, 'lib')
         | 
| 9 7 | 
             
            %w[thor pry highline/import colored].each {|g| require g}
         | 
| 10 8 |  | 
| 11 9 | 
             
            module Jetpants
         | 
| @@ -109,7 +107,7 @@ module Jetpants | |
| 109 107 |  | 
| 110 108 | 
             
                  error "Unable to determine a node to demote and a node to promote" unless demoted.kind_of?(Jetpants::DB) && promoted.kind_of?(Jetpants::DB)
         | 
| 111 109 | 
             
                  error "Node to promote #{promoted} is not a slave of node to demote #{demoted}" unless promoted.master == demoted
         | 
| 112 | 
            -
                  error " | 
| 110 | 
            +
                  error "The chosen node cannot be promoted. Please choose another." unless promoted.promotable_to_master?
         | 
| 113 111 |  | 
| 114 112 | 
             
                  inform "Going to DEMOTE existing master #{demoted} and PROMOTE new master #{promoted}."
         | 
| 115 113 | 
             
                  error "Aborting." unless agree "Proceed? [yes/no]: "
         | 
| @@ -124,12 +122,20 @@ module Jetpants | |
| 124 122 |  | 
| 125 123 |  | 
| 126 124 | 
             
                desc 'summary', 'display information about a node in the context of its pool'
         | 
| 127 | 
            -
                method_option :node, :desc => 'IP address of node to query'
         | 
| 125 | 
            +
                method_option :node, :desc => 'IP address of node to query, or name of pool'
         | 
| 128 126 | 
             
                def summary
         | 
| 129 | 
            -
                  node =  | 
| 130 | 
            -
                  node | 
| 131 | 
            -
             | 
| 132 | 
            -
             | 
| 127 | 
            +
                  node = options[:node] || ask('Please enter node IP or name of pool: ')
         | 
| 128 | 
            +
                  if is_ip? node
         | 
| 129 | 
            +
                    node = node.to_db
         | 
| 130 | 
            +
                    node.pool(true).probe
         | 
| 131 | 
            +
                    describe node
         | 
| 132 | 
            +
                    node.pool(true).summary(true)
         | 
| 133 | 
            +
                  else
         | 
| 134 | 
            +
                    pool = Jetpants.topology.pool(node)
         | 
| 135 | 
            +
                    raise "#{node} is neither an IP address nor a pool name" unless pool
         | 
| 136 | 
            +
                    pool.probe
         | 
| 137 | 
            +
                    pool.summary(true)
         | 
| 138 | 
            +
                  end
         | 
| 133 139 | 
             
                end
         | 
| 134 140 |  | 
| 135 141 |  | 
| @@ -140,7 +146,7 @@ module Jetpants | |
| 140 146 | 
             
                  Jetpants.pools.each &:summary
         | 
| 141 147 |  | 
| 142 148 | 
             
                  # We could do this more compactly using DB#role, but it would incorrectly
         | 
| 143 | 
            -
                  #  | 
| 149 | 
            +
                  # double-count nodes involved in a shard split
         | 
| 144 150 | 
             
                  counts = {master: 0, active_slave: 0, standby_slave: 0, backup_slave: 0}
         | 
| 145 151 | 
             
                  Jetpants.pools.each do |p|
         | 
| 146 152 | 
             
                    counts[:master] += 1
         | 
| @@ -170,10 +176,10 @@ module Jetpants | |
| 170 176 | 
             
                def pools_compact
         | 
| 171 177 | 
             
                  puts
         | 
| 172 178 | 
             
                  Jetpants.shards.each do |s| 
         | 
| 173 | 
            -
                    puts "[%-15s] %8s to %-11s = % | 
| 179 | 
            +
                    puts "[%-15s] %8s to %-11s = %4s GB" % [s.ip, s.min_id, s.max_id, s.data_set_size(true)]
         | 
| 174 180 | 
             
                  end
         | 
| 175 181 | 
             
                  Jetpants.functional_partitions.each do |p| 
         | 
| 176 | 
            -
                    puts "[%-15s] %-23s = % | 
| 182 | 
            +
                    puts "[%-15s] %-23s = %4s GB" % [p.ip, p.name, p.data_set_size(true)]
         | 
| 177 183 | 
             
                  end
         | 
| 178 184 | 
             
                  puts
         | 
| 179 185 | 
             
                end
         | 
| @@ -187,17 +193,32 @@ module Jetpants | |
| 187 193 |  | 
| 188 194 | 
             
                desc 'clone_slave', 'clone a standby slave'
         | 
| 189 195 | 
             
                method_option :source, :desc => 'IP of node to clone from'
         | 
| 190 | 
            -
                method_option :target, :desc => 'IP of node to clone to'
         | 
| 196 | 
            +
                method_option :target, :desc => 'IP of node(s) to clone to'
         | 
| 191 197 | 
             
                def clone_slave
         | 
| 192 198 | 
             
                  puts "This task clones the data set of a standby slave."
         | 
| 193 199 | 
             
                  source = ask_node('Please enter IP of node to clone from: ', options[:source])
         | 
| 200 | 
            +
                  source.master.probe if source.master # fail early if there are any replication issues in this pool
         | 
| 194 201 | 
             
                  describe source
         | 
| 195 202 |  | 
| 196 | 
            -
                   | 
| 203 | 
            +
                  puts "You may clone to particular IP address(es), or can type \"spare\" to claim a node from the spare pool."
         | 
| 204 | 
            +
                  target = options[:target] || ask('Please enter comma-separated list of targets (IPs or "spare") to clone to: ')
         | 
| 205 | 
            +
                  target = 'spare' if target.strip == '' || target.split(',').length == 0
         | 
| 206 | 
            +
                  spares_needed = target.split(',').count {|t| t.strip.upcase == 'SPARE'}
         | 
| 207 | 
            +
                  if spares_needed > 0
         | 
| 208 | 
            +
                    spares_available = Jetpants.topology.count_spares(role: :standby_slave, like: source)
         | 
| 209 | 
            +
                    raise "Not enough spare machines with role of standby slave! Requested #{spares_needed} but only have #{spares_available} available." if spares_needed > spares_available
         | 
| 210 | 
            +
                    claimed_spares = Jetpants.topology.claim_spares(spares_needed, role: :standby_slave, like: source)
         | 
| 211 | 
            +
                  end
         | 
| 212 | 
            +
             | 
| 197 213 | 
             
                  targets = target.split(',').map do |ip|
         | 
| 198 214 | 
             
                    ip.strip!
         | 
| 199 | 
            -
                     | 
| 200 | 
            -
             | 
| 215 | 
            +
                    if is_ip? ip
         | 
| 216 | 
            +
                      ip.to_db
         | 
| 217 | 
            +
                    elsif ip == '' || ip.upcase == 'SPARE'
         | 
| 218 | 
            +
                      claimed_spares.shift
         | 
| 219 | 
            +
                    else
         | 
| 220 | 
            +
                      error "target (#{ip}) does not appear to be an IP."
         | 
| 221 | 
            +
                    end
         | 
| 201 222 | 
             
                  end
         | 
| 202 223 |  | 
| 203 224 | 
             
                  source.start_mysql if ! source.running?
         | 
| @@ -205,6 +226,7 @@ module Jetpants | |
| 205 226 |  | 
| 206 227 | 
             
                  targets.each do |t|
         | 
| 207 228 | 
             
                    error "target #{t} already has a master; please clear out node (including in asset tracker) before proceeding" if t.master
         | 
| 229 | 
            +
                    error "target #{t} is running a different version of MySQL than source #{source}! Cannot proceed with clone operation." if t.version_cmp(source) != 0
         | 
| 208 230 | 
             
                  end
         | 
| 209 231 |  | 
| 210 232 | 
             
                  source.enslave_siblings!(targets)
         | 
| @@ -213,11 +235,6 @@ module Jetpants | |
| 213 235 | 
             
                  puts "Cloning complete."
         | 
| 214 236 | 
             
                  Jetpants.topology.write_config
         | 
| 215 237 | 
             
                end
         | 
| 216 | 
            -
                def self.after_clone_slave
         | 
| 217 | 
            -
                  reminders(
         | 
| 218 | 
            -
                    'Add the new host(s) to trending and monitoring.'
         | 
| 219 | 
            -
                  )
         | 
| 220 | 
            -
                end
         | 
| 221 238 |  | 
| 222 239 |  | 
| 223 240 | 
             
                desc 'activate_slave', 'turn a standby slave into an active slave'
         | 
| @@ -256,18 +273,26 @@ module Jetpants | |
| 256 273 | 
             
                desc 'destroy_slave', 'remove a standby slave from its pool'
         | 
| 257 274 | 
             
                method_option :node, :desc => 'IP of standby slave to remove'
         | 
| 258 275 | 
             
                def destroy_slave
         | 
| 276 | 
            +
                  # Permit slaves with broken replication to be destroyed
         | 
| 277 | 
            +
                  Jetpants.verify_replication = false
         | 
| 259 278 | 
             
                  puts "This task removes a standby/backup slave from its pool entirely. THIS IS PERMANENT, ie, it does a RESET SLAVE on the target."
         | 
| 260 279 | 
             
                  node = ask_node('Please enter node IP: ', options[:node])
         | 
| 261 280 | 
             
                  describe node
         | 
| 262 | 
            -
                   | 
| 281 | 
            +
                  if node.running? && node.available?
         | 
| 282 | 
            +
                    raise "Node is not a standby or backup slave" unless (node.is_standby? || node.for_backups?)
         | 
| 283 | 
            +
                  else
         | 
| 284 | 
            +
                    puts "Please note that we cannot run a RESET SLAVE on the node, because MySQL is not running or is otherwise unreachable."
         | 
| 285 | 
            +
                    puts "If the node is not permanently dead, you will have to run this manually."
         | 
| 286 | 
            +
                  end
         | 
| 263 287 | 
             
                  raise "Aborting" unless ask('Please type YES in all capital letters to confirm removing node from its pool: ') == 'YES'
         | 
| 264 288 | 
             
                  node.pool.remove_slave!(node)
         | 
| 289 | 
            +
                  node.revoke_all_access!
         | 
| 265 290 | 
             
                end
         | 
| 266 291 |  | 
| 267 292 |  | 
| 268 293 | 
             
                desc 'defrag_slave', 'export and re-import data set on a standby slave'
         | 
| 269 294 | 
             
                method_option :node, :desc => 'IP of standby slave to defragment'
         | 
| 270 | 
            -
                def  | 
| 295 | 
            +
                def defrag_slave
         | 
| 271 296 | 
             
                  puts "This task exports all data on a standby/backup slave and then re-imports it."
         | 
| 272 297 | 
             
                  node = ask_node('Please enter node IP: ', options[:node])
         | 
| 273 298 | 
             
                  describe node
         | 
| @@ -358,7 +383,6 @@ module Jetpants | |
| 358 383 | 
             
                end
         | 
| 359 384 | 
             
                def self.after_shard_split
         | 
| 360 385 | 
             
                  reminders(
         | 
| 361 | 
            -
                    'Trending and monitoring setup, as needed.',
         | 
| 362 386 | 
             
                    'Proceed to next step: jetpants shard_split_child_reads'
         | 
| 363 387 | 
             
                  )
         | 
| 364 388 | 
             
                end
         | 
| @@ -415,16 +439,25 @@ module Jetpants | |
| 415 439 | 
             
                desc 'shard_cutover', 'truncate the current last shard range, and add a new shard after it'
         | 
| 416 440 | 
             
                method_option :cutover_id, :desc => 'Minimum ID of new last shard being created'
         | 
| 417 441 | 
             
                def shard_cutover
         | 
| 418 | 
            -
                  # ensure the spares are available before beginning
         | 
| 419 | 
            -
                  raise "Not enough total spare machines!" unless Jetpants.topology.count_spares >= Jetpants.standby_slaves_per_pool + 1
         | 
| 420 | 
            -
                  raise "Not enough standby_slave role spare machines!" unless Jetpants.topology.count_spares(role: 'standby_slave') >= Jetpants.standby_slaves_per_pool
         | 
| 421 | 
            -
                  raise "Cannot find a spare master-role machine!" unless Jetpants.topology.count_spares(role: 'master') >= 1
         | 
| 422 | 
            -
             | 
| 423 442 | 
             
                  cutover_id = options[:cutover_id] || ask('Please enter min ID of the new shard to be created: ')
         | 
| 424 443 | 
             
                  cutover_id = cutover_id.to_i
         | 
| 425 444 | 
             
                  last_shard = Jetpants.topology.shards.select {|s| s.max_id == 'INFINITY' && s.in_config?}.first
         | 
| 426 445 | 
             
                  last_shard_master = last_shard.master
         | 
| 427 446 |  | 
| 447 | 
            +
                  # Simple sanity-check that the cutover ID is greater than the current last shard's MIN id.
         | 
| 448 | 
            +
                  # (in a later release, this may be improved to also ensure it's greater than any sharding
         | 
| 449 | 
            +
                  # key value in use on the last shard.)
         | 
| 450 | 
            +
                  raise "Specified cutover ID is too low!" unless cutover_id > last_shard.min_id
         | 
| 451 | 
            +
                  
         | 
| 452 | 
            +
                  # Ensure enough spare nodes are available before beginning.
         | 
| 453 | 
            +
                  # We supply the *previous* last shard as context for counting/claiming spares
         | 
| 454 | 
            +
                  # because the new last shard can't be created yet (chicken-and-egg problem -- master
         | 
| 455 | 
            +
                  # must exist before we create the pool). The assumption is the hardware spec
         | 
| 456 | 
            +
                  # of the new last shard and previous last shard will be the same.
         | 
| 457 | 
            +
                  raise "Not enough total spare machines!" unless Jetpants.topology.count_spares(like: last_shard_master) >= Jetpants.standby_slaves_per_pool + 1
         | 
| 458 | 
            +
                  raise "Not enough standby_slave role spare machines!" unless Jetpants.topology.count_spares(role: :standby_slave, like: last_shard_master) >= Jetpants.standby_slaves_per_pool
         | 
| 459 | 
            +
                  raise "Cannot find a spare master-role machine!" unless Jetpants.topology.count_spares(role: :master, like: last_shard_master) >= 1
         | 
| 460 | 
            +
                  
         | 
| 428 461 | 
             
                  # In asset tracker, remove the last shard pool and replace it with a new pool. The new pool
         | 
| 429 462 | 
             
                  # has the same master/slaves but now has a non-infinity max ID.
         | 
| 430 463 | 
             
                  last_shard.state = :recycle
         | 
| @@ -433,11 +466,13 @@ module Jetpants | |
| 433 466 | 
             
                  last_shard_replace.sync_configuration
         | 
| 434 467 | 
             
                  Jetpants.topology.pools << last_shard_replace
         | 
| 435 468 |  | 
| 436 | 
            -
                  # Now put another new shard after that one
         | 
| 437 | 
            -
                   | 
| 438 | 
            -
                   | 
| 469 | 
            +
                  # Now put another new shard after that one. (See earlier comment as to why we're supplying
         | 
| 470 | 
            +
                  # the pool from the old last shard. This param is just used to select the right type of hardware,
         | 
| 471 | 
            +
                  # NOT to actually set the pool of the returned object.)
         | 
| 472 | 
            +
                  new_last_shard_master = Jetpants.topology.claim_spare(role: :master, like: last_shard_master)
         | 
| 473 | 
            +
                  new_last_shard_master.disable_read_only! if new_last_shard_master.running?
         | 
| 439 474 | 
             
                  if Jetpants.standby_slaves_per_pool > 0
         | 
| 440 | 
            -
                    new_last_shard_slaves = Jetpants.topology.claim_spares(Jetpants.standby_slaves_per_pool, role:  | 
| 475 | 
            +
                    new_last_shard_slaves = Jetpants.topology.claim_spares(Jetpants.standby_slaves_per_pool, role: :standby_slave, like: last_shard_master)
         | 
| 441 476 | 
             
                    new_last_shard_slaves.each do |x| 
         | 
| 442 477 | 
             
                      x.change_master_to new_last_shard_master
         | 
| 443 478 | 
             
                      x.resume_replication
         | 
| @@ -458,7 +493,6 @@ module Jetpants | |
| 458 493 | 
             
                end
         | 
| 459 494 | 
             
                def self.after_shard_cutover
         | 
| 460 495 | 
             
                  reminders(
         | 
| 461 | 
            -
                    'Trending and monitoring setup, as needed.',
         | 
| 462 496 | 
             
                    'Commit/push the configuration in version control.',
         | 
| 463 497 | 
             
                    'Deploy the configuration to all machines.',
         | 
| 464 498 | 
             
                  )
         | 
    
        data/doc/commands.rdoc
    CHANGED
    
    | @@ -16,6 +16,8 @@ The copy method in \Jetpants uses a combination of tar, netcat (nc), and whichev | |
| 16 16 |  | 
| 17 17 | 
             
            This command does not require an asset tracker plugin, but DOES require that all your database nodes have installed whichever compression binary you specified in the Jetpants config file.
         | 
| 18 18 |  | 
| 19 | 
            +
            If you are using an asset tracker, when prompted for which nodes to clone TO, you may type "spare" (or equivalently just hit ENTER without typing any input) to claim a spare node with role STANDBY_SLAVE. You may clone to multiple spares at once by supplying comma-separated input like "spare, spare, spare". You can mix-and-match with supplying IP addresses of particular hosts as well.
         | 
| 20 | 
            +
             | 
| 19 21 |  | 
| 20 22 | 
             
            == Master/slave state changes
         | 
| 21 23 |  | 
    
        data/doc/configuration.rdoc
    CHANGED
    
    | @@ -4,13 +4,13 @@ | |
| 4 4 |  | 
| 5 5 | 
             
            At least one of these files must exist for \Jetpants to function properly.
         | 
| 6 6 |  | 
| 7 | 
            -
            If both exist, the user configuration will be merged on top of the global configuration,  | 
| 7 | 
            +
            If both exist, the user configuration will be merged on top of the global configuration, this is a "deep" merge. So if the user a "plugins" section this will be combined with the global "plugins" section.
         | 
| 8 8 |  | 
| 9 9 | 
             
            For an example global configuration file, please see the included <tt>etc/jetpants.yaml.sample</tt> file.
         | 
| 10 10 |  | 
| 11 11 | 
             
            == Configuration settings
         | 
| 12 12 |  | 
| 13 | 
            -
            max_concurrency::           Maximum threads to use in import/export operations, or equivalently the maximum connection pool size per database host (default:  | 
| 13 | 
            +
            max_concurrency::           Maximum threads to use in import/export operations, or equivalently the maximum connection pool size per database host. You will need to tune this to your specific database hardware, especially number of CPU cores and number/type of disks (default: 20)
         | 
| 14 14 | 
             
            standby_slaves_per_pool::   Minimum number of standby slaves to keep in every pool (default: 2)
         | 
| 15 15 | 
             
            mysql_schema::              database name (mandatory)
         | 
| 16 16 | 
             
            mysql_app_user::            mysql user that your application uses (mandatory)
         | 
| @@ -24,6 +24,7 @@ compress_with::             command line to perform compression during large fil | |
| 24 24 | 
             
            decompress_with::           command line to perform decompression during large file copy operations; see below (default: false)
         | 
| 25 25 | 
             
            export_location::           directory to use for data dumping (default: '/tmp')
         | 
| 26 26 | 
             
            verify_replication::        raise exception if the actual replication topology differs from Jetpants' understanding of it (ie, disagreement between asset tracker and probed state), or if MySQL's two replication threads are in different states (one running and the other stopped) on a DB node. (default: true. master promotion tool ignores this, since the demoted master may legitimately be dead/offline)
         | 
| 27 | 
            +
            private_interface::         name of private interface on your servers, such as eth0 or bond0. Not used by any core \Jetpants commands, but can be useful in plugings that wrap tcpdump, or code calling the Jetpants::Host.local method. (default: 'bond0')
         | 
| 27 28 | 
             
            plugins::                   hash of plugin name => arbitrary plugin data, usually a nested hash of settings (default: \{})
         | 
| 28 29 | 
             
            ssh_keys::                  array of SSH private key file locations, if not using standard id_dsa or id_rsa. Passed directly to Net::SSH.start's :keys parameter (default: nil)
         | 
| 29 30 | 
             
            sharded_tables::            array of name => \{sharding_key=>X, chunks=>Y} hashes, describing all tables on shards. Required by shard split/rebuild processes (default: \[])
         | 
    
        data/doc/jetpants_collins.rdoc
    CHANGED
    
    | @@ -38,7 +38,7 @@ This plugin also makes some assumptions about the way in which you use \Collins, | |
| 38 38 | 
             
            * All MySQL database server hosts that are in-use will have a STATUS of either ALLOCATED or MAINTENANCE.
         | 
| 39 39 | 
             
            * All MySQL database server hosts that are in-use will have a POOL set matching the name of their pool/shard, and a SECONDARY_ROLE set matching their \Jetpants role within the pool (MASTER, ACTIVE_SLAVE, STANDBY_SLAVE, or BACKUP_SLAVE).
         | 
| 40 40 | 
             
            * You can initially assign PRIMARY_ROLE, STATUS, POOL, and SECONDARY_ROLE to database servers somewhat automatically; see GETTING STARTED, below.
         | 
| 41 | 
            -
            * All database server hosts that are "spares" (not yet in use, but ready for use in shard splits, shard cutover, or slave cloning) need to have a STATUS of PROVISIONED. These nodes must meet the requirements of spares as defined by the REQUIREMENTS doc that comes with \Jetpants. They should NOT have a POOL set | 
| 41 | 
            +
            * All database server hosts that are "spares" (not yet in use, but ready for use in shard splits, shard cutover, or slave cloning) need to have a STATUS of PROVISIONED. These nodes must meet the requirements of spares as defined by the REQUIREMENTS doc that comes with \Jetpants. They should NOT have a POOL or SECONDARY_ROLE set in advance; if they do, it will be ignored -- we treat all spares as identical. That said, you can implement custom logic to respect POOL or SECONDARY_ROLE (or any other Collins attribute) by overriding Topology#process_spare_selector_options in a custom plugin loaded after jetpants_collins.
         | 
| 42 42 | 
             
            * Database server hosts may optionally have an attribute called SLAVE_WEIGHT. The default weight, if omitted, is 100. This field has no effect in \Jetpants, but can be used by your custom configuration generator as needed, if your application supports a notion of different weights for slave selection.
         | 
| 43 43 | 
             
            * Arbitrary metadata regarding pools and shards will be stored in assets with a TYPE of CONFIGURATION. These assets will have a POOL matching the pool's name, a TAG matching the pool's name but prefixed with 'mysql-', a STATUS reflecting the pool's state, and a PRIMARY_ROLE of either MYSQL_POOL or MYSQL_SHARD depending on the type of pool. You can make jetpants_collins create these automatically; see GETTING STARTED, below.
         | 
| 44 44 |  | 
    
        data/doc/plugins.rdoc
    CHANGED
    
    | @@ -56,6 +56,10 @@ You may also want to override or implement these, though it's not strictly manda | |
| 56 56 | 
             
            * Jetpants::Pool#after_remove_slave!
         | 
| 57 57 | 
             
              * If your implementation of Jetpants::Pool#sync_configuration works by iterating over the current slaves of the pool's master, it won't correctly handle the <tt>jetpants destroy_slave</tt> command,
         | 
| 58 58 | 
             
                or anything else that calls Jetpants::DB#disable_replication! which does a RESET SLAVE. You can instead handle that special case here.
         | 
| 59 | 
            +
            * Jetpants::DB#stop_query_kiler and Jetpants::DB#start_query_killer
         | 
| 60 | 
            +
              * If you use a query killer, you'll want to override these methods to start and stop it. stop_query_killer is called prior to a long-running query (SELECT ... INTO OUTFILE, LOAD DATA INFILE, SELECT COUNT(*), etc) and start_query_killer is called after the operation is complete.
         | 
| 61 | 
            +
            * Jetpants::DB#disable_monitoring and Jetpants::DB#enable_monitoring
         | 
| 62 | 
            +
              * If you use monitoring software, you can override these methods to prevent alerts from \Jetpants operations. \Jetpants calls disable_monitoring prior to performing maintenance operations on a node, such as stopping MySQL prior to cloning a slave, splitting a shard, etc. It calls enable_monitoring once the operation is complete.
         | 
| 59 63 |  | 
| 60 64 | 
             
            == How to write a plugin
         | 
| 61 65 |  | 
    
        data/doc/requirements.rdoc
    CHANGED
    
    | @@ -14,7 +14,7 @@ Plugins may freely override these assumptions, and upstream patches are very wel | |
| 14 14 | 
             
              * For example, \jetpants requires the mysql2 gem, which in turn needs MySQL development C headers in order to compile.
         | 
| 15 15 | 
             
            * MySQL (or compatible, like Percona Server), version 5.1 or higher.
         | 
| 16 16 | 
             
            * Required Linux binaries that must be installed and in root's PATH on all of your database machines:
         | 
| 17 | 
            -
              * <tt>service</tt>, a wrapper around init scripts, supporting syntax <tt>service mysql start</tt>, <tt>service mysql status</tt>, etc. Some distros include this by default (typically as /sbin/service or /usr/sbin/service) while others offer it as a package. Implementation varies slightly between distros; currently \Jetpants expects <tt>service mysql status</tt> output to include either "not running" (RHEL/Centos) or "stop/waiting" (Ubuntu) in the output if the MySQL server is not running.
         | 
| 17 | 
            +
              * <tt>service</tt>, a wrapper around init scripts, supporting syntax <tt>service mysql start</tt>, <tt>service mysql status</tt>, etc. Some distros include this by default (typically as /sbin/service or /usr/sbin/service) while others offer it as a package. Implementation varies slightly between distros; currently \Jetpants expects <tt>service mysql status</tt> output to include either "not running" (RHEL/Centos) or "stop/waiting" (Ubuntu) in the output if the MySQL server is not running. Additionally, <tt>service</tt> must pass extra parameters through to the daemon -- <tt>service mysql start --skip-networking</tt> should pass <tt>--skip-networking</tt> to mysqld.
         | 
| 18 18 | 
             
              * <tt>nc</tt>, also known as netcat, a tool for piping data to or from a socket.
         | 
| 19 19 | 
             
              * Whichever compression tool you've specified in the \Jetpants config file for the <tt>compress_with</tt> and <tt>decompress_with</tt> options, if any. (if omitted, compression will not be used for file copy operations.)
         | 
| 20 20 | 
             
            * InnoDB / Percona XtraDB for storage engine. \Jetpants has not been tested with MyISAM, since \Jetpants is geared towards huge tables, and MyISAM is generally a bad fit.
         | 
    
        data/lib/jetpants.rb
    CHANGED
    
    | @@ -21,7 +21,7 @@ module Jetpants | |
| 21 21 | 
             
              # Establish default configuration values, and then merge in whatever we find globally
         | 
| 22 22 | 
             
              # in /etc/jetpants.yaml and per-user in ~/.jetpants.yaml
         | 
| 23 23 | 
             
              @config = {
         | 
| 24 | 
            -
                'max_concurrency'         =>   | 
| 24 | 
            +
                'max_concurrency'         =>  20,         # max threads/conns per database
         | 
| 25 25 | 
             
                'standby_slaves_per_pool' =>  2,          # number of standby slaves in every pool
         | 
| 26 26 | 
             
                'mysql_schema'            =>  'test',     # database name
         | 
| 27 27 | 
             
                'mysql_app_user'          =>  'appuser',  # mysql user for application
         | 
| @@ -38,10 +38,27 @@ module Jetpants | |
| 38 38 | 
             
                'sharded_tables'          =>  [],         # array of name => {sharding_key=>X, chunks=>Y} hashes
         | 
| 39 39 | 
             
                'compress_with'           =>  false,      # command line to use for compression in large file transfers
         | 
| 40 40 | 
             
                'decompress_with'         =>  false,      # command line to use for decompression in large file transfers
         | 
| 41 | 
            +
                'private_interface'       =>  'bond0',    # network interface corresponding to private IP
         | 
| 41 42 | 
             
              }
         | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 43 | 
            +
             | 
| 44 | 
            +
              config_paths = ["/etc/jetpants.yaml", "~/.jetpants.yml", "~/.jetpants.yaml"]
         | 
| 45 | 
            +
              config_loaded = false
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              config_paths.each do |path|
         | 
| 48 | 
            +
                begin
         | 
| 49 | 
            +
                  overrides = YAML.load_file(File.expand_path path)
         | 
| 50 | 
            +
                  @config.deep_merge! overrides
         | 
| 51 | 
            +
                  config_loaded = true
         | 
| 52 | 
            +
                rescue Errno::ENOENT => error
         | 
| 53 | 
            +
                rescue ArgumentError => error
         | 
| 54 | 
            +
                  puts "YAML parsing error in configuration file #{path} : #{error.message}\n\n"
         | 
| 55 | 
            +
                  exit
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              unless config_loaded
         | 
| 60 | 
            +
                puts "Could not find any readable configuration files at either /etc/jetpants.yaml or ~/.jetpants.yaml\n\n"
         | 
| 61 | 
            +
                exit
         | 
| 45 62 | 
             
              end
         | 
| 46 63 |  | 
| 47 64 | 
             
              class << self
         | 
    
        data/lib/jetpants/db.rb
    CHANGED
    
    | @@ -66,6 +66,10 @@ module Jetpants | |
| 66 66 | 
             
                  # These get set upon DB#connect being run
         | 
| 67 67 | 
             
                  @user = nil
         | 
| 68 68 | 
             
                  @schema = nil
         | 
| 69 | 
            +
                  
         | 
| 70 | 
            +
                  # This is ephemeral, only known to Jetpants if you previously called
         | 
| 71 | 
            +
                  # DB#start_mysql or DB#restart_mysql in this process
         | 
| 72 | 
            +
                  @options = []
         | 
| 69 73 | 
             
                end
         | 
| 70 74 |  | 
| 71 75 | 
             
                ###### Host methods ########################################################
         | 
    
        data/lib/jetpants/db/client.rb
    CHANGED
    
    | @@ -41,14 +41,19 @@ module Jetpants | |
| 41 41 | 
             
                # Initializes (or re-initializes) the connection pool upon first use or upon
         | 
| 42 42 | 
             
                # requesting a different user or schema. Note that we only maintain one connection
         | 
| 43 43 | 
             
                # pool per DB.
         | 
| 44 | 
            -
                # Valid options include :user, :pass, :schema, :max_conns or omit | 
| 45 | 
            -
                # defaults.
         | 
| 44 | 
            +
                # Valid options include :user, :pass, :schema, :max_conns, :after_connect or omit
         | 
| 45 | 
            +
                # these to use defaults.
         | 
| 46 46 | 
             
                def connect(options={})
         | 
| 47 | 
            +
                  if @options.include? '--skip-networking'
         | 
| 48 | 
            +
                    output 'Skipping connection attempt because server started with --skip-networking'
         | 
| 49 | 
            +
                    return nil
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                  
         | 
| 47 52 | 
             
                  options[:user]    ||= app_credentials[:user]
         | 
| 48 53 | 
             
                  options[:schema]  ||= app_schema
         | 
| 49 54 |  | 
| 50 55 | 
             
                  return @db if @db && @user == options[:user] && @schema == options[:schema]
         | 
| 51 | 
            -
             | 
| 56 | 
            +
                  
         | 
| 52 57 | 
             
                  disconnect if @db
         | 
| 53 58 |  | 
| 54 59 | 
             
                  @db = Sequel.connect(
         | 
| @@ -58,7 +63,8 @@ module Jetpants | |
| 58 63 | 
             
                    :user             =>  options[:user],
         | 
| 59 64 | 
             
                    :password         =>  options[:pass] || app_credentials[:pass],
         | 
| 60 65 | 
             
                    :database         =>  options[:schema],
         | 
| 61 | 
            -
                    :max_connections  =>  options[:max_conns] || Jetpants.max_concurrency | 
| 66 | 
            +
                    :max_connections  =>  options[:max_conns] || Jetpants.max_concurrency,
         | 
| 67 | 
            +
                    :after_connect    =>  options[:after_connect] )
         | 
| 62 68 | 
             
                  @user = options[:user]
         | 
| 63 69 | 
             
                  @schema = options[:schema]
         | 
| 64 70 | 
             
                  @db
         | 
| @@ -101,14 +101,29 @@ module Jetpants | |
| 101 101 | 
             
                # run after export_data (in the same process), import_data will 
         | 
| 102 102 | 
             
                # automatically confirm that the import counts match the previous export
         | 
| 103 103 | 
             
                # counts.
         | 
| 104 | 
            +
                #
         | 
| 104 105 | 
             
                # Creates a 'jetpants' db user with FILE permissions for the duration of the
         | 
| 105 106 | 
             
                # import.
         | 
| 107 | 
            +
                #
         | 
| 108 | 
            +
                # Note: import will be substantially faster if you disable binary logging
         | 
| 109 | 
            +
                # before the import, and re-enable it after the import. You also must set
         | 
| 110 | 
            +
                # InnoDB's autoinc lock mode to 2 in order to do a chunked import with
         | 
| 111 | 
            +
                # auto-increment tables.  You can achieve all this by calling
         | 
| 112 | 
            +
                # DB#restart_mysql '--skip-log-bin', '--skip-log-slave-updates', '--innodb-autoinc-lock-mode=2'
         | 
| 113 | 
            +
                # prior to importing data, and then clear those settings by calling
         | 
| 114 | 
            +
                # DB#restart_mysql with no params after done importing data.
         | 
| 106 115 | 
             
                def import_data(tables, min_id=false, max_id=false)
         | 
| 116 | 
            +
                  disable_read_only!
         | 
| 107 117 | 
             
                  import_export_user = 'jetpants'
         | 
| 108 118 | 
             
                  create_user(import_export_user)
         | 
| 109 119 | 
             
                  grant_privileges(import_export_user)               # standard privs
         | 
| 110 120 | 
             
                  grant_privileges(import_export_user, '*', 'FILE')  # FILE global privs
         | 
| 111 | 
            -
                   | 
| 121 | 
            +
                  
         | 
| 122 | 
            +
                  # Disable unique checks upon connecting. This has to be done at the :after_connect level in Sequel
         | 
| 123 | 
            +
                  # to guarantee it's being run on every connection in the conn pool. This is mysql2-specific.
         | 
| 124 | 
            +
                  disable_unique_checks_proc = Proc.new {|mysql2_client| mysql2_client.query 'SET unique_checks = 0'}
         | 
| 125 | 
            +
                  
         | 
| 126 | 
            +
                  reconnect(user: import_export_user, after_connect: disable_unique_checks_proc)
         | 
| 112 127 |  | 
| 113 128 | 
             
                  import_counts = {}
         | 
| 114 129 | 
             
                  tables.each {|t| import_counts[t.name] = import_table_data t, min_id, max_id}
         | 
| @@ -258,9 +273,7 @@ module Jetpants | |
| 258 273 |  | 
| 259 274 | 
             
                  disable_monitoring
         | 
| 260 275 | 
             
                  stop_query_killer
         | 
| 261 | 
            -
                   | 
| 262 | 
            -
                  restart_mysql
         | 
| 263 | 
            -
                  pause_replication if is_slave?
         | 
| 276 | 
            +
                  restart_mysql '--skip-log-bin', '--skip-log-slave-updates', '--innodb-autoinc-lock-mode=2', '--skip-slave-start'
         | 
| 264 277 |  | 
| 265 278 | 
             
                  # Automatically detect missing min/max. Assumes that all tables' primary keys
         | 
| 266 279 | 
             
                  # are on the same scale, so this may be non-ideal, but better than just erroring.
         | 
| @@ -283,11 +296,9 @@ module Jetpants | |
| 283 296 | 
             
                  import_schemata!
         | 
| 284 297 | 
             
                  alter_schemata if respond_to? :alter_schemata
         | 
| 285 298 | 
             
                  import_data tables, min_id, max_id
         | 
| 286 | 
            -
             | 
| 287 | 
            -
                  resume_replication if is_slave?
         | 
| 288 | 
            -
                  enable_binary_logging
         | 
| 299 | 
            +
                  
         | 
| 289 300 | 
             
                  restart_mysql
         | 
| 290 | 
            -
                  catch_up_to_master
         | 
| 301 | 
            +
                  catch_up_to_master if is_slave?
         | 
| 291 302 | 
             
                  start_query_killer
         | 
| 292 303 | 
             
                  enable_monitoring
         | 
| 293 304 | 
             
                end
         | 
| @@ -303,15 +314,21 @@ module Jetpants | |
| 303 314 | 
             
                  destinations = {}
         | 
| 304 315 | 
             
                  targets.each do |t| 
         | 
| 305 316 | 
             
                    destinations[t] = t.mysql_directory
         | 
| 306 | 
            -
                     | 
| 307 | 
            -
                    raise "Over 100 MB of existing MySQL data on target #{t}, aborting copy!" if existing_size > 100000000
         | 
| 317 | 
            +
                    raise "Over 100 MB of existing MySQL data on target #{t}, aborting copy!" if t.data_set_size > 100000000
         | 
| 308 318 | 
             
                  end
         | 
| 309 319 | 
             
                  [self, targets].flatten.concurrent_each {|t| t.stop_query_killer; t.stop_mysql}
         | 
| 310 320 | 
             
                  targets.concurrent_each {|t| t.ssh_cmd "rm -rf #{t.mysql_directory}/ib_logfile*"}
         | 
| 321 | 
            +
                  
         | 
| 322 | 
            +
                  # Construct the list of files and dirs to copy. We include ib_lru_dump if present
         | 
| 323 | 
            +
                  # (ie, if using Percona Server with innodb_buffer_pool_restore_at_startup enabled)
         | 
| 324 | 
            +
                  # since this will greatly improve warm-up time of the cloned nodes
         | 
| 325 | 
            +
                  files = ['ibdata1', 'mysql', 'test', app_schema]
         | 
| 326 | 
            +
                  files << 'ib_lru_dump' if ssh_cmd("test -f #{mysql_directory}/ib_lru_dump 2>/dev/null; echo $?").chomp.to_i == 0
         | 
| 327 | 
            +
                  
         | 
| 311 328 | 
             
                  fast_copy_chain(mysql_directory, 
         | 
| 312 329 | 
             
                                  destinations,
         | 
| 313 330 | 
             
                                  port: 3306,
         | 
| 314 | 
            -
                                  files:  | 
| 331 | 
            +
                                  files: files,
         | 
| 315 332 | 
             
                                  overwrite: true)
         | 
| 316 333 | 
             
                  [self, targets].flatten.concurrent_each {|t| t.start_mysql; t.start_query_killer}
         | 
| 317 334 | 
             
                end
         |