jetpants 0.7.2 → 0.7.4

Sign up to get free protection for your applications and to get access to all the features.
data/bin/jetpants CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  jetpants_base_dir = File.expand_path(File.dirname(__FILE__) + '/..')
3
3
  $:.unshift File.join(jetpants_base_dir, 'lib')
4
- %w[thor pry highline/import terminal-table colored].each {|g| require g}
4
+ %w[thor pry highline/import colored].each {|g| require g}
5
5
 
6
6
  module Jetpants
7
7
 
@@ -63,24 +63,25 @@ module Jetpants
63
63
  demoted.probe
64
64
  else
65
65
  demoted = ask_node 'Please enter the IP address of the node to demote:'
66
- if demoted.running?
67
- error 'Cannot demote a node that has no slaves!' unless demoted.has_slaves?
66
+ end
67
+
68
+ if demoted.running?
69
+ error 'Cannot demote a node that has no slaves!' unless demoted.has_slaves?
70
+ else
71
+ inform "Unable to connect to node #{demoted} to demote"
72
+ error "Unable to perform promotion" unless agree "Please confirm that #{demoted} is offline [yes/no]: "
73
+
74
+ # An asset-tracker plugin may have been populated the slave list anyway
75
+ if demoted.slaves && demoted.slaves.count > 0
76
+ demoted.slaves.each {|s| s.probe}
68
77
  else
69
- inform "Unable to connect to node #{demoted} to demote"
70
- error "Unable to perform promotion" unless agree "Please confirm that #{demoted} is offline [yes/no]: "
71
-
72
- # An asset-tracker plugin may have been populated the slave list anyway
73
- if demoted.slaves && demoted.slaves.count > 0
74
- demoted.slaves.each {|s| s.probe}
75
- else
76
- replicas = ask("Please enter a comma-seperated list of IP addresses of all current replicas of #{demoted}: ").split /\s*,\s*/
77
- error "No replicas entered" unless replicas && replicas.count > 0
78
- error "User supplied list of replicas appears to be invalid - #{replicas}" unless replicas.all? {|replica| is_ip? replica}
79
- demoted.instance_eval {@slaves = replicas.map &:to_db}
80
- demoted.slaves.each do |replica|
81
- # Validate that they are really slaves of demoted
82
- error "#{replica} does not appear to be a valid replica of #{demoted}" unless replica.master == demoted
83
- end
78
+ replicas = ask("Please enter a comma-seperated list of IP addresses of all current replicas of #{demoted}: ").split /\s*,\s*/
79
+ error "No replicas entered" unless replicas && replicas.count > 0
80
+ error "User supplied list of replicas appears to be invalid - #{replicas}" unless replicas.all? {|replica| is_ip? replica}
81
+ demoted.instance_eval {@slaves = replicas.map &:to_db}
82
+ demoted.slaves.each do |replica|
83
+ # Validate that they are really slaves of demoted
84
+ error "#{replica} does not appear to be a valid replica of #{demoted}" unless replica.master == demoted
84
85
  end
85
86
  end
86
87
  end
@@ -117,103 +118,51 @@ module Jetpants
117
118
  end
118
119
 
119
120
 
120
- desc 'show_slaves', 'show the current slaves of a master'
121
- method_option :node, :desc => 'node to query for slaves'
122
- def show_slaves
123
- node = options[:node] || ask('Please enter IP of node to query for slaves: ')
124
- error "node (#{node}) does not appear to be an IP." unless is_ip? node
125
- node = Jetpants::DB.new node
126
- slaves = node.slaves rescue error("unable to connect to node #{node}")
127
-
128
- inform "node (#{node}) is a current slave of #{node.master}" if node.is_slave?
129
-
130
- if node.has_slaves?
131
- current_slaves = Terminal::Table.new :title => "slaves of master (#{node})", :headings => ["slave", "seconds behind master"] do |rows|
132
- slaves.each do |slave|
133
- rows << [slave, slave.seconds_behind_master]
134
- end
135
- end
136
- puts current_slaves
137
- else
138
- inform "node (#{node}) currently has no slaves."
139
- end
121
+ desc 'summary', 'display information about a node in the context of its pool'
122
+ method_option :node, :desc => 'IP address of node to query'
123
+ def summary
124
+ node = ask_node('Please enter node IP: ', options[:node])
125
+ node.pool(true).probe
126
+ describe node
127
+ node.pool(true).summary(true)
140
128
  end
141
129
 
142
130
 
143
- desc 'show_master', 'show the current master of a node'
144
- method_option :node, :desc => 'node to query for master'
145
- method_option :siblings, :desc => 'show nodes current slave siblings'
146
- def show_master
147
- node = options[:node] || ask('Please enter the IP address of a node to query for master: ')
148
- error "node (#{node}) does not appear to be an IP." unless is_ip? node
149
- node = Jetpants::DB.new node
150
-
151
- if node.has_slaves?
152
- inform "node (#{node}) is a master to the following nodes: #{node.slaves.join(', ')}"
153
- end rescue error("unable to connect to node #{node}")
131
+ desc 'pools', 'display a full summary of every pool in the topology'
132
+ def pools
133
+ Jetpants.pools.concurrent_each &:probe
134
+ Jetpants.pools.each &:summary
154
135
 
155
- if node.is_slave?
156
- inform "node (#{node}) is a slave of master (#{node.master})"
157
- if options[:siblings]
158
- current_siblings = Terminal::Table.new :title => "siblings of slave (#{node})", :headings => ["sibling slave", "seconds behind master (#{node.master})"] do |rows|
159
- node.master.slaves.reject {|slave| slave == node}.each do |slave|
160
- rows << [slave, slave.seconds_behind_master]
161
- end
162
- end
163
- puts current_siblings
164
- end
165
- else
166
- inform "node (#{node}) does not appear to be a slave"
136
+ counts = {master: 0, active_slave: 0, standby_slave: 0, backup_slave: 0}
137
+ Jetpants.pools.map(&:nodes).flatten.each {|n| counts[n.role] += 1}
138
+
139
+ puts
140
+ puts "%4d global pools" % Jetpants.functional_partitions.count
141
+ puts "%4d shard pools" % Jetpants.shards.count
142
+ puts "---- --------------"
143
+ puts "%4d total pools" % Jetpants.pools.count
144
+ puts
145
+
146
+ total = 0
147
+ counts.each do |role, count|
148
+ puts "%4d %ss" % [count, role.to_s.tr('_', ' ')]
149
+ total += count
167
150
  end
151
+ puts "---- --------------"
152
+ puts "%4d total nodes" % total
153
+ puts
168
154
  end
169
-
170
-
171
- desc 'node_info', 'show information about a given node'
172
- method_option :node, :desc => 'node to query for information'
173
- def node_info
174
- node = options[:node] || ask('Please enter node: ')
175
- error "node address (#{node}) does not appear to be an ip" unless is_ip? node
176
-
177
- node = Jetpants::DB.new node
178
- role = case
179
- when node.has_slaves?
180
- :master
181
- when node.is_slave?
182
- :slave
183
- else
184
- :node
185
- end rescue error("unable to connect to node #{node}")
186
155
 
187
- node_info = Terminal::Table.new :title => "info on #{role}: #{node}"
188
-
189
- binary_log, binary_log_position = node.binlog_coordinates
190
- node_info << [:binary_log, binary_log]
191
- node_info << [:binary_log_position, binary_log_position]
192
- node_info << [:read_only, node.read_only? ? 'true' : 'false']
193
-
194
- if node.is_slave?
195
- slave_status = node.slave_status
196
- slave_siblings = node.master.slaves.reject {|slave| slave == node}
197
-
198
- node_info << [:master, node.master]
199
- node_info << [:sibling_slaves, slave_siblings.join(', ')]
200
- node_info << [:replicating, node.replicating? ? 'true' : 'false']
201
- if node.replicating?
202
- node_info << [:seconds_behind_master, node.seconds_behind_master]
203
- node_info << [:master_log, slave_status[:master_log_file]]
204
- node_info << [:master_position, slave_status[:exec_master_log_pos]]
205
- end
156
+ desc 'pools_compact', 'display a compact summary (master, name, and size) of every pool in the topology'
157
+ def pools_compact
158
+ puts
159
+ Jetpants.shards.each do |s|
160
+ puts "[%-12s] %8s to %-11s = %3s GB" % [s.ip, s.min_id, s.max_id, s.data_set_size(true)]
206
161
  end
207
- puts node_info
208
-
209
- if node.has_slaves?
210
- current_slaves = Terminal::Table.new :title => "slaves of #{node}", :headings => ["slave", "seconds behind master"] do |rows|
211
- node.slaves.each do |slave|
212
- rows << [slave, slave.seconds_behind_master]
213
- end
214
- end
215
- puts current_slaves
162
+ Jetpants.functional_partitions.each do |p|
163
+ puts "[%-12s] %-23s = %3s GB" % [p.ip, p.name, p.data_set_size(true)]
216
164
  end
165
+ puts
217
166
  end
218
167
 
219
168
 
@@ -228,9 +177,9 @@ module Jetpants
228
177
  method_option :target, :desc => 'IP of node to clone to'
229
178
  def clone_slave
230
179
  puts "This task clones the data set of a standby slave."
231
- source = options[:source] || ask('Please enter IP of node to clone from: ')
232
- error "source (#{source}) does not appear to be an IP." unless is_ip? source
233
- source = Jetpants::DB.new source
180
+ source = ask_node('Please enter IP of node to clone from: ', options[:source])
181
+ describe source
182
+
234
183
  target = options[:target] || ask('Please enter comma-separated list of IP addresses to clone to: ')
235
184
  targets = target.split(',').map do |ip|
236
185
  ip.strip!
@@ -257,13 +206,14 @@ module Jetpants
257
206
  method_option :node, :desc => 'IP of standby slave to activate'
258
207
  def activate_slave
259
208
  puts "This task turns a standby slave into an active slave, OR alters an active slave's weight."
260
- node = options[:node] || ask('Please enter node IP: ')
209
+ node = ask_node('Please enter node IP: ', options[:node])
210
+ describe node
211
+
261
212
  weight = options[:weight] || ask('Please enter weight, or ENTER for default of 100: ')
262
213
  weight = 100 if weight == ''
263
214
  weight = weight.to_i
264
- error "node address (#{node}) does not appear to be an IP" unless is_ip? node
265
215
  error "Adding a slave of weight 0 makes no sense, use pull_slave instead" if weight == 0
266
- node = node.to_db
216
+
267
217
  node.pool.mark_slave_active(node, weight)
268
218
  Jetpants.topology.write_config
269
219
  end
@@ -277,9 +227,9 @@ module Jetpants
277
227
  method_option :node, :desc => 'IP of active slave to pull'
278
228
  def pull_slave
279
229
  puts "This task turns an active slave into a standby slave."
280
- node = options[:node] || ask('Please enter node IP: ')
281
- error "node address (#{node}) does not appear to be an ip" unless is_ip? node
282
- node = node.to_db
230
+ node = ask_node('Please enter node IP: ', options[:node])
231
+ describe node
232
+ raise "Node is not an active slave" unless node.role == :active_slave
283
233
  node.pool.mark_slave_standby(node)
284
234
  Jetpants.topology.write_config
285
235
  end
@@ -288,12 +238,11 @@ module Jetpants
288
238
  desc 'destroy_slave', 'remove a standby slave from its pool'
289
239
  method_option :node, :desc => 'IP of standby slave to remove'
290
240
  def destroy_slave
291
- puts "This task removes a standby slave from its pool entirely. THIS IS PERMANENT, ie, it does a RESET SLAVE on the target."
292
- node = options[:node] || ask('Please enter node IP: ')
293
- error "node address (#{node}) does not appear to be an IP" unless is_ip? node
294
- node = node.to_db
295
- raise "Node is not a standby slave" unless node.is_standby?
296
- raise "Aborting" unless ask('Please type YES in all capital letters to confirm: ') == 'YES'
241
+ puts "This task removes a standby/backup slave from its pool entirely. THIS IS PERMANENT, ie, it does a RESET SLAVE on the target."
242
+ node = ask_node('Please enter node IP: ', options[:node])
243
+ describe node
244
+ raise "Node is not a standby or backup slave" unless (node.is_standby? || node.for_backups?)
245
+ raise "Aborting" unless ask('Please type YES in all capital letters to confirm removing node from its pool: ') == 'YES'
297
246
  node.pool.remove_slave!(node)
298
247
  end
299
248
 
@@ -302,10 +251,9 @@ module Jetpants
302
251
  method_option :node, :desc => 'IP of standby slave to rebuild'
303
252
  def rebuild_slave
304
253
  puts "This task exports all data on a standby/backup slave and then re-imports it."
305
- node = options[:node] || ask('Please enter node IP: ')
306
- error "node address (#{node}) does not appear to be an ip" unless is_ip? node
307
- node = node.to_db
308
- raise "Node is not a standby or backup slave" unless node.is_standby? || node.for_backups?
254
+ node = ask_node('Please enter node IP: ', options[:node])
255
+ describe node
256
+ raise "Node is not a standby or backup slave" unless (node.is_standby? || node.for_backups?)
309
257
  raise "Cannot rebuild non-shard slaves from command suite; use jetpants console instead" unless node.pool.is_a?(Shard)
310
258
  node.rebuild!
311
259
  end
@@ -417,11 +365,7 @@ module Jetpants
417
365
  method_option :min_id, :desc => 'Minimum ID of parent shard being split'
418
366
  method_option :max_id, :desc => 'Maximum ID of parent shard being split'
419
367
  def shard_split_child_writes
420
- shard_min = options[:min_id] || ask('Please enter min ID of the parent shard: ')
421
- shard_max = options[:max_id] || ask('Please enter max ID of the parent shard: ')
422
- s = Jetpants.topology.shard shard_min, shard_max
423
- raise "Shard not found" unless s
424
- raise "Shard isn't in expected state" unless s.state == :deprecated && s.children.count > 1
368
+ s = ask_shard_being_split
425
369
  s.move_writes_to_children
426
370
  Jetpants.topology.write_config
427
371
  end
@@ -439,11 +383,7 @@ module Jetpants
439
383
  method_option :min_id, :desc => 'Minimum ID of parent shard being split'
440
384
  method_option :max_id, :desc => 'Maximum ID of parent shard being split'
441
385
  def shard_split_cleanup
442
- shard_min = options[:min_id] || ask('Please enter min ID of the parent shard: ')
443
- shard_max = options[:max_id] || ask('Please enter max ID of the parent shard: ')
444
- s = Jetpants.topology.shard shard_min, shard_max
445
- raise "Shard not found" unless s
446
- raise "Shard isn't in expected state" unless s.state == :deprecated && s.children.count > 1
386
+ s = ask_shard_being_split
447
387
  s.cleanup!
448
388
  end
449
389
  def self.after_shard_split_cleanup
@@ -456,6 +396,11 @@ module Jetpants
456
396
  desc 'shard_cutover', 'truncate the current last shard range, and add a new shard after it'
457
397
  method_option :cutover_id, :desc => 'Minimum ID of new last shard being created'
458
398
  def shard_cutover
399
+ # ensure the spares are available before beginning
400
+ raise "Not enough total spare machines!" unless Jetpants.topology.count_spares >= Jetpants.standby_slaves_per_pool + 1
401
+ raise "Not enough standby_slave role spare machines!" unless Jetpants.topology.count_spares(role: 'standby_slave') >= Jetpants.standby_slaves_per_pool
402
+ raise "Cannot find a spare master-role machine!" unless Jetpants.topology.count_spares(role: 'master') >= 1
403
+
459
404
  cutover_id = options[:cutover_id] || ask('Please enter min ID of the new shard to be created: ')
460
405
  cutover_id = cutover_id.to_i
461
406
  last_shard = Jetpants.topology.shards.select {|s| s.max_id == 'INFINITY' && s.in_config?}.first
@@ -511,11 +456,36 @@ module Jetpants
511
456
  puts message.blue
512
457
  end
513
458
 
514
- def ask_node(prompt)
515
- node = ask prompt
459
+ def describe node
460
+ puts "Node #{node} (#{node.hostname}:#{node.port}) has role #{node.role} in pool #{node.pool(true)}.".green
461
+ end
462
+
463
+ def ask_node(prompt, supplied_node=false)
464
+ node = supplied_node || ask(prompt)
516
465
  error "Node (#{node}) does not appear to be an IP address." unless is_ip? node
517
466
  node.to_db
518
467
  end
468
+
469
+ def ask_shard_being_split
470
+ shards_being_split = Jetpants.shards.select {|s| s.children.count > 0}
471
+ if shards_being_split.count == 0
472
+ raise 'No shards are currently being split. You can only use this task after running "jetpants shard_split".'
473
+ elsif shards_being_split.count == 1
474
+ s = shards_being_split[0]
475
+ puts "Detected #{s} as the only shard currently involved in a split operation."
476
+ error "Aborting." unless agree "Is this the right shard that you want to perform this action on? [yes/no]: "
477
+ else
478
+ puts "The following shards are currently involved in a split operation:"
479
+ shards_being_split.each {|sbs| puts "* #{sbs}"}
480
+ puts "Which shard would you like to perform this action on?"
481
+ shard_min = options[:min_id] || ask('Please enter min ID of the parent shard: ')
482
+ shard_max = options[:max_id] || ask('Please enter max ID of the parent shard: ')
483
+ s = Jetpants.topology.shard shard_min, shard_max
484
+ raise "Shard not found" unless s
485
+ end
486
+ raise "Shard isn't in expected state" unless s.state == :deprecated
487
+ s
488
+ end
519
489
  end
520
490
 
521
491
  def self.reminders(*strings)
data/doc/commands.rdoc CHANGED
@@ -103,13 +103,13 @@ With an asset tracker, \Jetpants allows you to mark a shard as read-only or comp
103
103
 
104
104
  == Informational commands
105
105
 
106
- These commands do not require an asset tracker. They may be fleshed out further in a future release of \Jetpants.
106
+ These commands display status information about a particular node, pool, or the entire topology.
107
107
 
108
- <b><tt>jetpants node_info</tt></b> displays information on a MySQL instance.
108
+ <b><tt>jetpants summary</tt></b> displays information about a node, along with its pool. Does not require an asset tracker.
109
109
 
110
- <b><tt>jetpants show_master</tt></b> tells you what node is the master of a given MySQL instance.
110
+ <b><tt>jetpants pools</tt></b> displays full information about all pools (name, size, full node list including roles), and then displays counts of all nodes by role. Requires an asset tracker to obtain the list of all pools. This command may take a minute or two to run (depending on topology size) since it has to probe every node to determine roles.
111
111
 
112
- <b><tt>jetpants show_slaves</tt></b> displays a list of slaves (along with current slave lag) of a MySQL instance.
112
+ <b><tt>jetpants pools_compact</tt></b> displays condensed information about all pools (name, master IP, size). Requires an asset tracker to obtain the list of all pools.
113
113
 
114
114
 
115
115
  == Miscellaneous
data/doc/faq.rdoc CHANGED
@@ -7,6 +7,26 @@
7
7
  The benefit of a toolkit is that you can still leverage standard MySQL replication, still use InnoDB/XtraDB as a robust storage engine choice, etc. \Jetpants largely doesn't interfere with any of that, and instead just provides tools to help you manage a large MySQL topology and support a range-based sharding scheme.
8
8
 
9
9
 
10
+ == If it's not a server, how does my application know which pool/shard to send queries to?
11
+
12
+ \Jetpants doesn't perform query routing; it leaves that to your application. The purpose of Jetpants is to *operationally* manage your database topology. It provides an object hierarchy that allows you to manipulate and automate your DB topology, and offers efficient implementations of common operational tasks in a sharded environment (bulk data importing/exporting, replica cloning, shard splitting, master promotion).
13
+
14
+ That said, you could integrate it into an application and implement query routing using its methods like Jetpants::Topology#shard_db_for_id. This method just compares a sharding key value to the list of shard ranges, to figure out which DB the query should go to. It's simple to port this logic to non-Ruby apps as well, since the list of shard ranges can be expressed in a portable format like YAML or JSON.
15
+
16
+ Doing this more seamlessly would require some amount of parsing of SQL in your app or service. This might be a small amount of code (if you're just scanning queries for "WHERE sharding_key_column = ?") or might be a lot (if you're trying to support joins and complex expressions).
17
+
18
+
19
+ == Can I use \Jetpants to query some or all shards at once?
20
+
21
+ Absolutely! You can even use it to implement some simple map/reduce-style distributed computation in MySQL:
22
+
23
+ Jetpants.topology.shards.concurrent_map {|s| s.query_return_first_value 'SELECT MAX(id) from foo'}.reduce(0) {|overall_max, val| [val, overall_max].max}
24
+
25
+ This example concurrently queries all shards to determine the maximum ID in existence for table foo. This might be useful if, say, you're using Memcached as an ID generation service, and need to determine the previous highest-assigned ID after a restart or failover event.
26
+
27
+ Another simple example would be to obtain counts of rows across all shards. In theory you can apply this same technique to other map/reduce style requirements with arbitrarily complex queries.
28
+
29
+
10
30
  == Is \Jetpants still useful if my architecture isn't sharded?
11
31
 
12
32
  Potentially, since \Jetpants fully supports "global" pools, also known as "functional partitions". You can even use \Jetpants to help manage a standard single-pool MySQL topology (1 master and some number of slaves) for handling common operations like slave cloning and master promotions. That said, there are other tools that may be easier to use if your MySQL footprint is smaller than, say, a dozen machines.
data/lib/jetpants.rb CHANGED
@@ -60,7 +60,9 @@ module Jetpants
60
60
  # Returns a hash containing :user => username string, :pass => password string
61
61
  # for the MySQL replication user, as found in Jetpants' configuration. Plugins
62
62
  # may freely override this if there's a better way to obtain this password --
63
- # for example, by parsing master.info on a slave in your topology.
63
+ # for example, by parsing master.info on a specific slave in your topology.
64
+ # SEE ALSO: DB#replication_credentials, which only falls back to the global
65
+ # version when needed.
64
66
  def replication_credentials
65
67
  {user: @config['mysql_repl_user'], pass: @config['mysql_repl_password']}
66
68
  end
@@ -97,4 +99,5 @@ module Jetpants
97
99
 
98
100
  # Finally, initialize topology object
99
101
  @topology = Topology.new
102
+ @topology.load_pools
100
103
  end
data/lib/jetpants/db.rb CHANGED
@@ -31,6 +31,10 @@ module Jetpants
31
31
  @@all_dbs = {}
32
32
  @@all_dbs_mutex = Mutex.new
33
33
 
34
+ def self.clear
35
+ @@all_dbs_mutex.synchronize {@@all_dbs = {}}
36
+ end
37
+
34
38
  # Because this class is rather large, methods have been grouped together
35
39
  # and moved to separate files in lib/jetpants/db. We load these all now.
36
40
  # They each just re-open the DB class and add some methods.
@@ -51,15 +55,19 @@ module Jetpants
51
55
 
52
56
  def initialize(ip, port=3306)
53
57
  @ip, @port = ip, port.to_i
54
- @user = false # connections will default to app user
58
+ @host = Host.new(ip)
59
+
60
+ # These get set upon DB#probe being run
55
61
  @master = nil
56
62
  @slaves = nil
57
63
  @repl_paused = nil
58
64
  @running = nil
59
- @host = Host.new(ip)
65
+
66
+ # These get set upon DB#connect being run
67
+ @user = nil
68
+ @schema = nil
60
69
  end
61
70
 
62
-
63
71
  ###### Host methods ########################################################
64
72
 
65
73
  # Jetpants::DB delegates missing methods to its Jetpants::Host.
@@ -5,21 +5,24 @@ module Jetpants
5
5
  #++
6
6
 
7
7
  class DB
8
- # Runs the provided SQL statement as root, and returns the response as a single string.
8
+ # Runs the provided SQL statement as root, locally via an SSH command line, and
9
+ # returns the response as a single string.
9
10
  # Available options:
10
11
  # :terminator:: how to terminate the query, such as '\G' or ';'. (default: '\G')
11
12
  # :parse:: parse a single-row, vertical-format result (:terminator must be '\G') and return it as a hash
13
+ # :schema:: name of schema to use, or true to use this DB's default. This may have implications when used with filtered replication! (default: nil, meaning no schema)
12
14
  # :attempts:: by default, queries will be attempted up to 3 times. set this to 0 or false for non-idempotent queries.
13
15
  def mysql_root_cmd(cmd, options={})
14
16
  terminator = options[:terminator] || '\G'
15
17
  attempts = (options[:attempts].nil? ? 3 : (options[:attempts].to_i || 1))
18
+ schema = (options[:schema] == true ? app_schema : options[:schema])
16
19
  failures = 0
17
20
 
18
21
  begin
19
22
  raise "MySQL is not running" unless running?
20
23
  supply_root_pw = (Jetpants.mysql_root_password ? "-p#{Jetpants.mysql_root_password}" : '')
21
24
  supply_port = (@port == 3306 ? '' : "-h 127.0.0.1 -P #{@port}")
22
- real_cmd = %Q{mysql #{supply_root_pw} #{supply_port} -ss -e "#{cmd}#{terminator}" #{Jetpants.mysql_schema}}
25
+ real_cmd = %Q{mysql #{supply_root_pw} #{supply_port} -ss -e "#{cmd}#{terminator}" #{schema}}
23
26
  real_cmd.untaint
24
27
  result = ssh_cmd!(real_cmd)
25
28
  raise result if result && result.downcase.start_with?('error ')
@@ -34,53 +37,91 @@ module Jetpants
34
37
  end
35
38
  end
36
39
 
37
- # Returns a Sequel database object
38
- def mysql
39
- return @db if @db
40
+ # Returns a Sequel database object for use in sending queries to the DB remotely.
41
+ # Initializes (or re-initializes) the connection pool upon first use or upon
42
+ # requesting a different user or schema. Note that we only maintain one connection
43
+ # pool per DB.
44
+ # Valid options include :user, :pass, :schema, :max_conns or omit these to use
45
+ # defaults.
46
+ def connect(options={})
47
+ options[:user] ||= app_credentials[:user]
48
+ options[:schema] ||= app_schema
49
+
50
+ return @db if @db && @user == options[:user] && @schema == options[:schema]
51
+
52
+ disconnect if @db
53
+
40
54
  @db = Sequel.connect(
41
55
  :adapter => 'mysql2',
42
56
  :host => @ip,
43
57
  :port => @port,
44
- :user => @user || Jetpants.app_credentials[:user],
45
- :password => Jetpants.app_credentials[:pass],
46
- :database => Jetpants.mysql_schema,
47
- :max_connections => Jetpants.max_concurrency)
58
+ :user => options[:user],
59
+ :password => options[:pass] || app_credentials[:pass],
60
+ :database => options[:schema],
61
+ :max_connections => options[:max_conns] || Jetpants.max_concurrency)
62
+ @user = options[:user]
63
+ @schema = options[:schema]
64
+ @db
48
65
  end
49
- alias init_db_connection_pool mysql
50
66
 
51
- # Closes existing mysql connection pool and opens a new one. Useful when changing users.
52
- # Supply a new user name as the param, or nothing/false to keep old user name, or
53
- # a literal true value to switch to the default app user in Jetpants configuration
54
- def reconnect(new_user=false)
55
- @user = (new_user == true ? Jetpants.app_credentials[:user] : new_user) if new_user
67
+ # Closes the database connection(s) in the connection pool.
68
+ def disconnect
56
69
  if @db
57
70
  @db.disconnect rescue nil
58
71
  @db = nil
59
72
  end
60
- init_db_connection_pool
73
+ @user = nil
74
+ @schema = nil
75
+ end
76
+
77
+ # Disconnects and reconnects to the database.
78
+ def reconnect(options={})
79
+ disconnect # force disconnection even if we're not changing user or schema
80
+ connect(options)
81
+ end
82
+
83
+ # Returns a Sequel database object representing the current connection. If no
84
+ # current connection, this will automatically connect with default options.
85
+ def connection
86
+ @db || connect
87
+ end
88
+ alias mysql connection
89
+
90
+ # Returns a hash containing :user and :pass indicating how the application connects to
91
+ # this database instance. By default this just delegates to Jetpants.application_credentials,
92
+ # which obtains credentials from the Jetpants config file. Plugins may override this
93
+ # to use different credentials for particular hosts or in certain situations.
94
+ def app_credentials
95
+ Jetpants.app_credentials
96
+ end
97
+
98
+ # Returns the schema name ("database name" in MySQL parlance) to use for connections.
99
+ # Defaults to just calling Jetpants.mysql_schema, but plugins may override.
100
+ def app_schema
101
+ Jetpants.mysql_schema
61
102
  end
62
103
 
63
104
  # Execute a write (INSERT, UPDATE, DELETE, REPLACE, etc) query.
64
105
  # If the query is an INSERT, returns the last insert ID (if an auto_increment
65
106
  # column is involved). Otherwise returns the number of affected rows.
66
107
  def query(sql, *binds)
67
- ds = mysql.fetch(sql, *binds)
68
- mysql.execute_dui(ds.update_sql) {|c| return c.last_id > 0 ? c.last_id : c.affected_rows}
108
+ ds = connection.fetch(sql, *binds)
109
+ connection.execute_dui(ds.update_sql) {|c| return c.last_id > 0 ? c.last_id : c.affected_rows}
69
110
  end
70
111
 
71
112
  # Execute a read (SELECT) query. Returns an array of hashes.
72
113
  def query_return_array(sql, *binds)
73
- mysql.fetch(sql, *binds).all
114
+ connection.fetch(sql, *binds).all
74
115
  end
75
116
 
76
117
  # Execute a read (SELECT) query. Returns a hash of the first row only.
77
118
  def query_return_first(sql, *binds)
78
- mysql.fetch(sql, *binds).first
119
+ connection.fetch(sql, *binds).first
79
120
  end
80
121
 
81
122
  # Execute a read (SELECT) query. Returns the value of the first column of the first row only.
82
123
  def query_return_first_value(sql, *binds)
83
- mysql.fetch(sql, *binds).single_value
124
+ connection.fetch(sql, *binds).single_value
84
125
  end
85
126
 
86
127
  # Parses the result of a MySQL query run with a \G terminator. Useful when
@@ -10,7 +10,7 @@ module Jetpants
10
10
  output 'Exporting table definitions'
11
11
  supply_root_pw = (Jetpants.mysql_root_password ? "-p#{Jetpants.mysql_root_password}" : '')
12
12
  supply_port = (@port == 3306 ? '' : "-h 127.0.0.1 -P #{@port}")
13
- cmd = "mysqldump #{supply_root_pw} #{supply_port} -d #{Jetpants.mysql_schema} " + tables.join(' ') + " >#{Jetpants.export_location}/create_tables_#{@port}.sql"
13
+ cmd = "mysqldump #{supply_root_pw} #{supply_port} -d #{app_schema} " + tables.join(' ') + " >#{Jetpants.export_location}/create_tables_#{@port}.sql"
14
14
  cmd.untaint
15
15
  result = ssh_cmd(cmd)
16
16
  output result
@@ -23,7 +23,7 @@ module Jetpants
23
23
  # CAUTION IF RUNNING THIS MANUALLY!
24
24
  def import_schemata!
25
25
  output 'Dropping and re-creating table definitions'
26
- result = mysql_root_cmd "source #{Jetpants.export_location}/create_tables_#{@port}.sql", :terminator => ''
26
+ result = mysql_root_cmd "source #{Jetpants.export_location}/create_tables_#{@port}.sql", terminator: '', schema: true
27
27
  output result
28
28
  end
29
29
 
@@ -44,12 +44,12 @@ module Jetpants
44
44
  create_user(import_export_user)
45
45
  grant_privileges(import_export_user) # standard privs
46
46
  grant_privileges(import_export_user, '*', 'FILE') # FILE global privs
47
- reconnect(import_export_user)
47
+ reconnect(user: import_export_user)
48
48
  @counts ||= {}
49
49
  tables.each {|t| @counts[t.name] = export_table_data t, min_id, max_id}
50
50
  ensure
51
- reconnect(true) # switches back to default app user
52
- drop_user(import_export_user)
51
+ reconnect(user: app_credentials[:user])
52
+ drop_user import_export_user
53
53
  end
54
54
 
55
55
  # Exports data for a table. Only includes the data subset that falls
@@ -108,7 +108,7 @@ module Jetpants
108
108
  create_user(import_export_user)
109
109
  grant_privileges(import_export_user) # standard privs
110
110
  grant_privileges(import_export_user, '*', 'FILE') # FILE global privs
111
- reconnect(import_export_user)
111
+ reconnect(user: import_export_user)
112
112
 
113
113
  import_counts = {}
114
114
  tables.each {|t| import_counts[t.name] = import_table_data t, min_id, max_id}
@@ -124,7 +124,7 @@ module Jetpants
124
124
  end
125
125
 
126
126
  ensure
127
- reconnect(true) # switches back to default app user
127
+ reconnect(user: app_credentials[:user])
128
128
  drop_user(import_export_user)
129
129
  end
130
130
 
@@ -194,7 +194,7 @@ module Jetpants
194
194
  # Supply the ID range (in terms of the table's sharding key)
195
195
  # of rows to KEEP.
196
196
  def prune_data_to_range(tables, keep_min_id, keep_max_id)
197
- reconnect(true)
197
+ reconnect(user: app_credentials[:user])
198
198
  tables.each do |t|
199
199
  output "Cleaning up data, pruning to only keep range #{keep_min_id}-#{keep_max_id}", t
200
200
  rows_deleted = 0
@@ -260,7 +260,6 @@ module Jetpants
260
260
  stop_query_killer
261
261
  disable_binary_logging
262
262
  restart_mysql
263
- reconnect
264
263
  pause_replication if is_slave?
265
264
 
266
265
  # Automatically detect missing min/max. Assumes that all tables' primary keys
@@ -293,15 +292,6 @@ module Jetpants
293
292
  enable_monitoring
294
293
  end
295
294
 
296
- # Returns the data set size in bytes (if in_gb is false or omitted) or in gigabytes
297
- # (if in_gb is true). Note that this is actually in gibibytes (2^30) rather than
298
- # a metric gigabyte. This puts it on the same scale as the output to tools like
299
- # "du -h" and "df -h".
300
- def data_set_size(in_gb=false)
301
- bytes = dir_size("#{mysql_directory}/#{Jetpants.mysql_schema}")
302
- in_gb ? (bytes / 1073741824.0).round : bytes
303
- end
304
-
305
295
  # Copies mysql db files from self to one or more additional DBs.
306
296
  # WARNING: temporarily shuts down mysql on self, and WILL OVERWRITE CONTENTS
307
297
  # OF MYSQL DIRECTORY ON TARGETS. Confirms first that none of the targets
@@ -321,7 +311,7 @@ module Jetpants
321
311
  fast_copy_chain(mysql_directory,
322
312
  destinations,
323
313
  port: 3306,
324
- files: ['ibdata1', 'mysql', 'test', Jetpants.mysql_schema],
314
+ files: ['ibdata1', 'mysql', 'test', app_schema],
325
315
  overwrite: true)
326
316
  [self, targets].flatten.concurrent_each {|t| t.start_mysql; t.start_query_killer}
327
317
  end
@@ -9,28 +9,30 @@ module Jetpants
9
9
  # configuration will be used instead. Does not automatically grant any
10
10
  # privileges; use DB#grant_privileges for that.
11
11
  def create_user(username=false, database=false, password=false)
12
- username ||= Jetpants.app_credentials[:user]
13
- database ||= Jetpants.mysql_schema
14
- password ||= Jetpants.app_credentials[:pass]
12
+ username ||= app_credentials[:user]
13
+ database ||= app_schema
14
+ password ||= app_credentials[:pass]
15
15
  commands = []
16
16
  Jetpants.mysql_grant_ips.each do |ip|
17
17
  commands << "CREATE USER '#{username}'@'#{ip}' IDENTIFIED BY '#{password}'"
18
18
  end
19
19
  commands << "FLUSH PRIVILEGES"
20
- mysql_root_cmd(commands.join '; ')
20
+ commands = commands.join '; '
21
+ mysql_root_cmd commands, schema: true
21
22
  end
22
23
 
23
24
  # Drops a user. Can optionally make this statement skip replication, if you
24
25
  # want to drop a user on master and not on its slaves.
25
26
  def drop_user(username=false, skip_binlog=false)
26
- username ||= Jetpants.app_credentials[:user]
27
+ username ||= app_credentials[:user]
27
28
  commands = []
28
29
  commands << 'SET sql_log_bin = 0' if skip_binlog
29
30
  Jetpants.mysql_grant_ips.each do |ip|
30
31
  commands << "DROP USER '#{username}'@'#{ip}'"
31
32
  end
32
33
  commands << "FLUSH PRIVILEGES"
33
- mysql_root_cmd(commands.join '; ')
34
+ commands = commands.join '; '
35
+ mysql_root_cmd commands, schema: true
34
36
  end
35
37
 
36
38
  # Grants privileges to the given username for the specified database.
@@ -50,8 +52,8 @@ module Jetpants
50
52
  # Helper method that can do grants or revokes.
51
53
  def grant_or_revoke_privileges(statement, username, database, privileges)
52
54
  preposition = (statement.downcase == 'revoke' ? 'FROM' : 'TO')
53
- username ||= Jetpants.app_credentials[:user]
54
- database ||= Jetpants.mysql_schema
55
+ username ||= app_credentials[:user]
56
+ database ||= app_schema
55
57
  privileges = Jetpants.mysql_grant_privs if privileges.empty?
56
58
  privileges = privileges.join(',')
57
59
  commands = []
@@ -60,14 +62,15 @@ module Jetpants
60
62
  commands << "#{statement} #{privileges} ON #{database}.* #{preposition} '#{username}'@'#{ip}'"
61
63
  end
62
64
  commands << "FLUSH PRIVILEGES"
63
- mysql_root_cmd(commands.join '; ')
65
+ commands = commands.join '; '
66
+ mysql_root_cmd commands, schema: true
64
67
  end
65
68
 
66
69
  # Disables access to a DB by the application user, and sets the DB to
67
70
  # read-only. Useful when decommissioning instances from a shard that's
68
71
  # been split.
69
72
  def revoke_all_access!
70
- user_name = Jetpants.app_credentials[:user]
73
+ user_name = app_credentials[:user]
71
74
  enable_read_only!
72
75
  output "Revoking access for user #{user_name}."
73
76
  output(drop_user(user_name, true)) # drop the user without replicating the drop statement to slaves
@@ -12,8 +12,9 @@ module Jetpants
12
12
  # If you omit :log_pos or :log_file, uses the current position/file from new_master,
13
13
  # though this is only safe if new_master is not receiving writes!
14
14
  #
15
- # If you omit :user or :password, tries obtaining replication credentials from global
16
- # settings, and failing that from the current node (assuming it is already a slave)
15
+ # If you omit :user and :password, tries obtaining replication credentials from the
16
+ # current node (assuming it is already a slave) or if that fails then from the global
17
+ # settings.
17
18
  def change_master_to(new_master, option_hash={})
18
19
  return disable_replication! unless new_master # change_master_to(nil) alias for disable_replication!
19
20
  return if new_master == master # no change
@@ -25,8 +26,8 @@ module Jetpants
25
26
  logfile, pos = new_master.binlog_coordinates
26
27
  end
27
28
 
28
- repl_user = option_hash[:user] || Jetpants.replication_credentials[:user] || replication_credentials[:user]
29
- repl_pass = option_hash[:password] || Jetpants.replication_credentials[:pass] || replication_credentials[:pass]
29
+ repl_user = option_hash[:user] || replication_credentials[:user]
30
+ repl_pass = option_hash[:password] || replication_credentials[:pass]
30
31
 
31
32
  pause_replication if @master && !@repl_paused
32
33
  result = mysql_root_cmd "CHANGE MASTER TO " +
@@ -78,12 +79,12 @@ module Jetpants
78
79
  # Resumes replication on self afterwards, but does NOT automatically start
79
80
  # replication on the targets.
80
81
  # You can omit passing in the replication user/pass if this machine is itself
81
- # a slave OR already has at least one slave.
82
+ # a slave OR already has at least one slave OR the global setting is fine to use here.
82
83
  # Warning: takes self offline during the process, so don't use on a master that
83
84
  # is actively in use by your application!
84
85
  def enslave!(targets, repl_user=false, repl_pass=false)
85
- repl_user ||= (Jetpants.replication_credentials[:user] || replication_credentials[:user])
86
- repl_pass ||= (Jetpants.replication_credentials[:pass] || replication_credentials[:pass])
86
+ repl_user ||= replication_credentials[:user]
87
+ repl_pass ||= replication_credentials[:pass]
87
88
  disable_monitoring
88
89
  pause_replication if master && ! @repl_paused
89
90
  file, pos = binlog_coordinates
@@ -113,8 +114,8 @@ module Jetpants
113
114
  t.change_master_to( master,
114
115
  log_file: file,
115
116
  log_pos: pos,
116
- user: (Jetpants.replication_credentials[:user] || replication_credentials[:user]),
117
- password: (Jetpants.replication_credentials[:pass] || replication_credentials[:pass]) )
117
+ user: replication_credentials[:user],
118
+ password: replication_credentials[:pass] )
118
119
  end
119
120
  resume_replication # should already have happened from the clone_to! restart anyway, but just to be explicit
120
121
  catch_up_to_master
@@ -205,14 +206,21 @@ module Jetpants
205
206
  end
206
207
 
207
208
  # Reads an existing master.info file on this db or one of its slaves,
208
- # propagates the info back to the Jetpants singleton, and returns it
209
+ # propagates the info back to the Jetpants singleton, and returns it as
210
+ # a hash containing :user and :pass.
211
+ # If the node is not a slave and has no slaves, will use the global Jetpants
212
+ # config instead.
209
213
  def replication_credentials
210
- unless @master || @slaves.count > 0
211
- raise "Cannot obtain replication credentials from orphaned instance -- this instance is not a slave, and has no slaves"
214
+ user = false
215
+ pass = false
216
+ if master || slaves.count > 0
217
+ target = (@master ? self : @slaves[0])
218
+ results = target.ssh_cmd("cat #{mysql_directory}/master.info | head -6 | tail -2").split
219
+ if results.count == 2 && results[0] != 'test'
220
+ user, pass = results
221
+ end
212
222
  end
213
- target = (@master ? self : @slaves[0])
214
- user, pass = target.ssh_cmd("cat #{mysql_directory}/master.info | head -6 | tail -2").split
215
- {user: user, pass: pass}
223
+ user && pass ? {user: user, pass: pass} : Jetpants.replication_credentials
216
224
  end
217
225
 
218
226
  # Disables binary logging in my.cnf. Does not take effect until you restart
@@ -31,10 +31,21 @@ module Jetpants
31
31
  # Restarts MySQL.
32
32
  def restart_mysql
33
33
  @repl_paused = false if @master
34
+
35
+ # Disconnect if we were previously connected
36
+ user, schema = false, false
37
+ if @db
38
+ user, schema = @user, @schema
39
+ disconnect
40
+ end
41
+
34
42
  output "Attempting to restart MySQL"
35
43
  output service(:restart, 'mysql')
36
44
  confirm_listening
37
45
  @running = true
46
+
47
+ # Reconnect if we were previously connected
48
+ connect(user: user, schema: schema) if user || schema
38
49
  end
39
50
 
40
51
  # Has no built-in effect. Plugins can override it, and/or implement
@@ -73,7 +73,7 @@ module Jetpants
73
73
  # remembers the state from the previous probe and any actions since then.
74
74
  def replicating?
75
75
  status = slave_status
76
- [status[:slave_io_running], status[:slave_sql_running]].all? {|s| s.downcase == 'yes'}
76
+ [status[:slave_io_running], status[:slave_sql_running]].all? {|s| s && s.downcase == 'yes'}
77
77
  end
78
78
 
79
79
  # Returns true if this instance has a master, false otherwise.
@@ -174,6 +174,16 @@ module Jetpants
174
174
  end
175
175
  end
176
176
 
177
+ # Returns the data set size in bytes (if in_gb is false or omitted) or in gigabytes
178
+ # (if in_gb is true). Note that this is actually in gibibytes (2^30) rather than
179
+ # a metric gigabyte. This puts it on the same scale as the output to tools like
180
+ # "du -h" and "df -h".
181
+ def data_set_size(in_gb=false)
182
+ bytes = dir_size("#{mysql_directory}/#{app_schema}")
183
+ in_gb ? (bytes / 1073741824.0).round : bytes
184
+ end
185
+
186
+
177
187
  ###### Private methods #####################################################
178
188
 
179
189
  private
data/lib/jetpants/host.rb CHANGED
@@ -8,11 +8,15 @@ module Jetpants
8
8
  class Host
9
9
  include CallbackHandler
10
10
 
11
+ # IP address of the Host, as a string.
12
+ attr_reader :ip
13
+
11
14
  @@all_hosts = {}
12
15
  @@all_hosts_mutex = Mutex.new
13
16
 
14
- # IP address of the Host, as a string.
15
- attr_reader :ip
17
+ def self.clear
18
+ @@all_hosts_mutex.synchronize {@@all_hosts = {}}
19
+ end
16
20
 
17
21
  # We override Host.new so that attempting to create a duplicate Host object
18
22
  # (that is, one with the same IP as an existing Host object) returns the
@@ -369,6 +373,7 @@ module Jetpants
369
373
 
370
374
  # Returns the machine's hostname
371
375
  def hostname
376
+ return 'unknown' unless available?
372
377
  @hostname ||= ssh_cmd('hostname').chomp
373
378
  end
374
379
 
data/lib/jetpants/pool.rb CHANGED
@@ -128,14 +128,15 @@ module Jetpants
128
128
  # Note that a plugin may want to override this (or implement after_remove_slave!)
129
129
  # to actually sync the change to an asset tracker, depending on how the plugin
130
130
  # implements Pool#sync_configuration. (If the implementation makes sync_configuration
131
- # work by iterating over the pool's current slaves, it won't see any slaves that have
132
- # been removed.)
131
+ # work by iterating over the pool's current slaves to update their status/role/pool, it
132
+ # won't see any slaves that have been removed, and therefore won't update them.)
133
133
  def remove_slave!(slave_db)
134
134
  raise "Slave is not in this pool" unless slave_db.pool == self
135
135
  slave_db.disable_monitoring
136
136
  slave_db.stop_replication
137
137
  slave_db.repl_binlog_coordinates # displays how far we replicated, in case you need to roll back this change manually
138
138
  slave_db.disable_replication!
139
+ sync_configuration # may or may not be sufficient -- see note above.
139
140
  end
140
141
 
141
142
  # Informs this pool that it has an alias. A pool may have any number of aliases.
@@ -167,7 +168,9 @@ module Jetpants
167
168
  elsif s == @master
168
169
  details[s] = {coordinates: s.binlog_coordinates(false), lag: 'N/A'}
169
170
  else
170
- details[s] = {coordinates: s.repl_binlog_coordinates(false), lag: s.seconds_behind_master.to_s + 's'}
171
+ lag = s.seconds_behind_master
172
+ lag_str = lag.nil? ? 'NULL' : lag.to_s + 's'
173
+ details[s] = {coordinates: s.repl_binlog_coordinates(false), lag: lag_str}
171
174
  end
172
175
  end
173
176
  end
@@ -9,12 +9,16 @@ module Jetpants
9
9
 
10
10
  def initialize
11
11
  @pools = [] # array of Pool objects
12
- load_pools
12
+ # We intentionally don't call load_pools here. The caller must do that.
13
+ # This allows Jetpants module to create Jetpants.topology object, and THEN
14
+ # invoke load_pools, which might then refer back to Jetpants.topology.
13
15
  end
14
16
 
15
17
  ###### Class methods #######################################################
16
18
 
17
19
  # Metaprogramming hackery to create a "synchronized" method decorator
20
+ # Note that all synchronized methods share the same mutex, so don't make one
21
+ # synchronized method call another!
18
22
  @lock = Mutex.new
19
23
  @do_sync = false
20
24
  @synchronized_methods = {} # symbol => true
@@ -84,7 +88,7 @@ module Jetpants
84
88
  end
85
89
 
86
90
 
87
- ###### Accessors ###########################################################
91
+ ###### Instance Methods ####################################################
88
92
 
89
93
  # Returns array of this topology's Jetpants::Pool objects of type Jetpants::Shard
90
94
  def shards
@@ -140,5 +144,19 @@ module Jetpants
140
144
  claim_spares(1, options)[0]
141
145
  end
142
146
 
147
+ synchronized
148
+ # Clears the pool list and nukes cached DB and Host object lookup tables
149
+ def clear
150
+ @pools = []
151
+ DB.clear
152
+ Host.clear
153
+ end
154
+
155
+ # Empties and then reloads the pool list
156
+ def refresh
157
+ clear
158
+ load_pools
159
+ true
160
+ end
143
161
  end
144
162
  end
@@ -43,9 +43,8 @@ module Jetpants
43
43
  end
44
44
 
45
45
  def determine_pool_and_role(ip, port=3306)
46
- ip += ":#{port}" if port.to_i != 3306
47
-
48
- [@global_pools + @shards].each do |h|
46
+ ip += ":#{port}"
47
+ (@global_pools + @shards).each do |h|
49
48
  pool = (h['name'] ? Jetpants.topology.pool(h['name']) : Jetpants.topology.shard(h['min_id'], h['max_id']))
50
49
  return [pool, 'MASTER'] if h['master'] == ip
51
50
  h['slaves'].each do |s|
@@ -57,9 +56,9 @@ module Jetpants
57
56
  end
58
57
 
59
58
  def determine_slaves(ip, port=3306)
60
- ip += ":#{port}" if port.to_i != 3306
59
+ ip += ":#{port}"
61
60
 
62
- [@global_pools + @shards].each do |h|
61
+ (@global_pools + @shards).each do |h|
63
62
  next unless h['master'] == ip
64
63
  return h['slaves'].map {|s| s['host'].to_db}
65
64
  end
@@ -71,4 +70,4 @@ module Jetpants
71
70
  end
72
71
 
73
72
  # load all the monkeypatches for other Jetpants classes
74
- %w(pool shard topology).each {|mod| require "simple_tracker/#{mod}"}
73
+ %w(pool shard topology db).each {|mod| require "simple_tracker/#{mod}"}
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: jetpants
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.7.2
5
+ version: 0.7.4
6
6
  platform: ruby
7
7
  authors:
8
8
  - Evan Elias
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2012-06-18 00:00:00 Z
14
+ date: 2012-09-05 00:00:00 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: mysql2
@@ -80,7 +80,7 @@ dependencies:
80
80
  type: :runtime
81
81
  version_requirements: *id006
82
82
  - !ruby/object:Gem::Dependency
83
- name: terminal-table
83
+ name: colored
84
84
  prerelease: false
85
85
  requirement: &id007 !ruby/object:Gem::Requirement
86
86
  none: false
@@ -90,17 +90,6 @@ dependencies:
90
90
  version: "0"
91
91
  type: :runtime
92
92
  version_requirements: *id007
93
- - !ruby/object:Gem::Dependency
94
- name: colored
95
- prerelease: false
96
- requirement: &id008 !ruby/object:Gem::Requirement
97
- none: false
98
- requirements:
99
- - - ">="
100
- - !ruby/object:Gem::Version
101
- version: "0"
102
- type: :runtime
103
- version_requirements: *id008
104
93
  description: Jetpants is an automation toolkit for handling monstrously large MySQL database topologies. It is geared towards common operational tasks like cloning slaves, rebalancing shards, and performing master promotions. It features a command suite for easy use by operations staff, though it's also a full Ruby library for use in developing custom migration scripts and database automation.
105
94
  email:
106
95
  - me@evanelias.com
@@ -111,40 +100,40 @@ extensions: []
111
100
 
112
101
  extra_rdoc_files:
113
102
  - README.rdoc
103
+ - doc/plugins.rdoc
114
104
  - doc/configuration.rdoc
115
105
  - doc/faq.rdoc
116
- - doc/requirements.rdoc
117
106
  - doc/commands.rdoc
118
- - doc/plugins.rdoc
107
+ - doc/requirements.rdoc
119
108
  files:
120
109
  - Gemfile
121
110
  - README.rdoc
111
+ - doc/plugins.rdoc
122
112
  - doc/configuration.rdoc
123
113
  - doc/faq.rdoc
124
- - doc/requirements.rdoc
125
114
  - doc/commands.rdoc
126
- - doc/plugins.rdoc
127
- - lib/jetpants/monkeypatch.rb
115
+ - doc/requirements.rdoc
116
+ - lib/jetpants/callback.rb
117
+ - lib/jetpants/topology.rb
118
+ - lib/jetpants/db/server.rb
119
+ - lib/jetpants/db/state.rb
128
120
  - lib/jetpants/db/import_export.rb
129
121
  - lib/jetpants/db/privileges.rb
130
122
  - lib/jetpants/db/client.rb
131
123
  - lib/jetpants/db/replication.rb
132
- - lib/jetpants/db/server.rb
133
- - lib/jetpants/db/state.rb
134
- - lib/jetpants/db.rb
135
124
  - lib/jetpants/shard.rb
125
+ - lib/jetpants/db.rb
126
+ - lib/jetpants/host.rb
136
127
  - lib/jetpants/pool.rb
128
+ - lib/jetpants/monkeypatch.rb
137
129
  - lib/jetpants/table.rb
138
- - lib/jetpants/topology.rb
139
- - lib/jetpants/callback.rb
140
- - lib/jetpants/host.rb
141
130
  - lib/jetpants.rb
142
131
  - bin/jetpants
143
- - plugins/simple_tracker/db.rb
132
+ - plugins/simple_tracker/topology.rb
144
133
  - plugins/simple_tracker/shard.rb
145
- - plugins/simple_tracker/pool.rb
146
134
  - plugins/simple_tracker/simple_tracker.rb
147
- - plugins/simple_tracker/topology.rb
135
+ - plugins/simple_tracker/db.rb
136
+ - plugins/simple_tracker/pool.rb
148
137
  - etc/jetpants.yaml.sample
149
138
  homepage: https://github.com/tumblr/jetpants/
150
139
  licenses: []