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 +107 -137
- data/doc/commands.rdoc +4 -4
- data/doc/faq.rdoc +20 -0
- data/lib/jetpants.rb +4 -1
- data/lib/jetpants/db.rb +11 -3
- data/lib/jetpants/db/client.rb +62 -21
- data/lib/jetpants/db/import_export.rb +9 -19
- data/lib/jetpants/db/privileges.rb +13 -10
- data/lib/jetpants/db/replication.rb +23 -15
- data/lib/jetpants/db/server.rb +11 -0
- data/lib/jetpants/db/state.rb +11 -1
- data/lib/jetpants/host.rb +7 -2
- data/lib/jetpants/pool.rb +6 -3
- data/lib/jetpants/topology.rb +20 -2
- data/plugins/simple_tracker/simple_tracker.rb +5 -6
- metadata +17 -28
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
|
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
|
-
|
67
|
-
|
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
|
-
|
70
|
-
error
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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 '
|
121
|
-
method_option :node, :desc => 'node to query
|
122
|
-
def
|
123
|
-
node =
|
124
|
-
|
125
|
-
|
126
|
-
|
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 '
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
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 =
|
232
|
-
|
233
|
-
|
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 =
|
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
|
-
|
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 =
|
281
|
-
|
282
|
-
|
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 =
|
293
|
-
|
294
|
-
node
|
295
|
-
raise "
|
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 =
|
306
|
-
|
307
|
-
node
|
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
|
-
|
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
|
-
|
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
|
515
|
-
node
|
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
|
106
|
+
These commands display status information about a particular node, pool, or the entire topology.
|
107
107
|
|
108
|
-
<b><tt>jetpants
|
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
|
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
|
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
|
-
@
|
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
|
-
|
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.
|
data/lib/jetpants/db/client.rb
CHANGED
@@ -5,21 +5,24 @@ module Jetpants
|
|
5
5
|
#++
|
6
6
|
|
7
7
|
class DB
|
8
|
-
# Runs the provided SQL statement as root,
|
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}" #{
|
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
|
-
|
39
|
-
|
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 =>
|
45
|
-
:password =>
|
46
|
-
:database =>
|
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
|
52
|
-
|
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
|
-
|
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 =
|
68
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 #{
|
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", :
|
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(
|
52
|
-
drop_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(
|
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(
|
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',
|
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 ||=
|
13
|
-
database ||=
|
14
|
-
password ||=
|
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
|
-
|
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 ||=
|
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
|
-
|
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 ||=
|
54
|
-
database ||=
|
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
|
-
|
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 =
|
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
|
16
|
-
#
|
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] ||
|
29
|
-
repl_pass = option_hash[:password] ||
|
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 ||=
|
86
|
-
repl_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:
|
117
|
-
password:
|
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
|
-
|
211
|
-
|
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
|
-
|
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
|
data/lib/jetpants/db/server.rb
CHANGED
@@ -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
|
data/lib/jetpants/db/state.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
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
|
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
|
-
|
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
|
data/lib/jetpants/topology.rb
CHANGED
@@ -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
|
-
######
|
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}"
|
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}"
|
59
|
+
ip += ":#{port}"
|
61
60
|
|
62
|
-
|
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.
|
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-
|
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:
|
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/
|
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/
|
127
|
-
- lib/jetpants/
|
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/
|
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/
|
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: []
|