jetpants 0.7.10 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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. Core class implementations, shard split logic, plugin system
80
- * <b>Dallas Marlow</b>: Master promotion logic, command suite and console structure, MySQL internals expertise
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 2012 Tumblr, Inc.
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.
@@ -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 "Cannot promote a backup slave. Please choose another." if promoted.for_backups?
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 = ask_node('Please enter node IP: ', options[:node])
130
- node.pool(true).probe
131
- describe node
132
- node.pool(true).summary(true)
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
- # handle counts for shards in the middle of a split
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 = %3s GB" % [s.ip, s.min_id, s.max_id, s.data_set_size(true)]
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 = %3s GB" % [p.ip, p.name, p.data_set_size(true)]
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
- target = options[:target] || ask('Please enter comma-separated list of IP addresses to clone to: ')
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
- error "target (#{ip}) does not appear to be an IP." unless is_ip? ip
200
- ip.to_db
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
- raise "Node is not a standby or backup slave" unless (node.is_standby? || node.for_backups?)
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 rebuild_slave
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
- new_last_shard_master = Jetpants.topology.claim_spare(role: 'master')
438
- new_last_shard_master.disable_read_only! if (new_last_shard_master.running? && new_last_shard_master.read_only?)
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: 'standby_slave')
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
  )
@@ -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
 
@@ -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, but please note that this is not a "deep" merge. So if the user configuration defines a "plugins" section, this will used as-is and the global "plugins" section will be ignored. This may change in a future release.
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: 40)
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: \[])
@@ -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, but they may have a ROLE set to either MASTER or STANDBY_SLAVE. The role will be used to select spare nodes for shard splits and shard cutover.
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
 
@@ -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
 
@@ -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.
@@ -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' => 40, # max threads/conns per database
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
- %w(/etc/jetpants.yaml ~/.jetpants.yml ~/.jetpants.yaml).each do |path|
43
- overrides = YAML.load_file(File.expand_path path) rescue {}
44
- @config.merge! overrides
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
@@ -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 ########################################################
@@ -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 these to use
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
- reconnect(user: import_export_user)
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
- disable_binary_logging
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
- existing_size = t.data_set_size + t.dir_size("#{t.mysql_directory}/ibdata1")
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: ['ibdata1', 'mysql', 'test', app_schema],
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