jetpants 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +3 -0
- data/README.rdoc +88 -0
- data/bin/jetpants +442 -0
- data/doc/commands.rdoc +119 -0
- data/doc/configuration.rdoc +27 -0
- data/doc/plugins.rdoc +120 -0
- data/doc/requirements.rdoc +54 -0
- data/etc/jetpants.yaml.sample +58 -0
- data/lib/jetpants.rb +100 -0
- data/lib/jetpants/callback.rb +131 -0
- data/lib/jetpants/db.rb +122 -0
- data/lib/jetpants/db/client.rb +103 -0
- data/lib/jetpants/db/import_export.rb +330 -0
- data/lib/jetpants/db/privileges.rb +89 -0
- data/lib/jetpants/db/replication.rb +226 -0
- data/lib/jetpants/db/server.rb +79 -0
- data/lib/jetpants/db/state.rb +212 -0
- data/lib/jetpants/host.rb +396 -0
- data/lib/jetpants/monkeypatch.rb +74 -0
- data/lib/jetpants/pool.rb +272 -0
- data/lib/jetpants/shard.rb +311 -0
- data/lib/jetpants/table.rb +146 -0
- data/lib/jetpants/topology.rb +144 -0
- data/plugins/simple_tracker/db.rb +23 -0
- data/plugins/simple_tracker/pool.rb +70 -0
- data/plugins/simple_tracker/shard.rb +76 -0
- data/plugins/simple_tracker/simple_tracker.rb +74 -0
- data/plugins/simple_tracker/topology.rb +66 -0
- data/tasks/promotion.rb +260 -0
- metadata +191 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Jetpants
|
2
|
+
|
3
|
+
#--
|
4
|
+
# User / Grant manipulation methods ##########################################
|
5
|
+
#++
|
6
|
+
|
7
|
+
class DB
|
8
|
+
# Create a MySQL user. If you omit parameters, the defaults from Jetpants'
|
9
|
+
# configuration will be used instead. Does not automatically grant any
|
10
|
+
# privileges; use DB#grant_privileges for that.
|
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]
|
15
|
+
commands = []
|
16
|
+
Jetpants.mysql_grant_ips.each do |ip|
|
17
|
+
commands << "CREATE USER '#{username}'@'#{ip}' IDENTIFIED BY '#{password}'"
|
18
|
+
end
|
19
|
+
commands << "FLUSH PRIVILEGES"
|
20
|
+
mysql_root_cmd(commands.join '; ')
|
21
|
+
end
|
22
|
+
|
23
|
+
# Drops a user. Can optionally make this statement skip replication, if you
|
24
|
+
# want to drop a user on master and not on its slaves.
|
25
|
+
def drop_user(username=false, skip_binlog=false)
|
26
|
+
username ||= Jetpants.app_credentials[:user]
|
27
|
+
commands = []
|
28
|
+
commands << 'SET sql_log_bin = 0' if skip_binlog
|
29
|
+
Jetpants.mysql_grant_ips.each do |ip|
|
30
|
+
commands << "DROP USER '#{username}'@'#{ip}'"
|
31
|
+
end
|
32
|
+
commands << "FLUSH PRIVILEGES"
|
33
|
+
mysql_root_cmd(commands.join '; ')
|
34
|
+
end
|
35
|
+
|
36
|
+
# Grants privileges to the given username for the specified database.
|
37
|
+
# Pass in privileges as additional params, each as strings.
|
38
|
+
# You may omit parameters to use the defaults in the Jetpants config file.
|
39
|
+
def grant_privileges(username=false, database=false, *privileges)
|
40
|
+
grant_or_revoke_privileges('GRANT', username, database, privileges)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Revokes privileges from the given username for the specified database.
|
44
|
+
# Pass in privileges as additional params, each as strings.
|
45
|
+
# You may omit parameters to use the defaults in the Jetpants config file.
|
46
|
+
def revoke_privileges(username=false, database=false, *privileges)
|
47
|
+
grant_or_revoke_privileges('REVOKE', username, database, privileges)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Helper method that can do grants or revokes.
|
51
|
+
def grant_or_revoke_privileges(statement, username, database, privileges)
|
52
|
+
preposition = (statement.downcase == 'revoke' ? 'FROM' : 'TO')
|
53
|
+
username ||= Jetpants.app_credentials[:user]
|
54
|
+
database ||= Jetpants.mysql_schema
|
55
|
+
privileges = Jetpants.mysql_grant_privs if privileges.empty?
|
56
|
+
privileges = privileges.join(',')
|
57
|
+
commands = []
|
58
|
+
|
59
|
+
Jetpants.mysql_grant_ips.each do |ip|
|
60
|
+
commands << "#{statement} #{privileges} ON #{database}.* #{preposition} '#{username}'@'#{ip}'"
|
61
|
+
end
|
62
|
+
commands << "FLUSH PRIVILEGES"
|
63
|
+
mysql_root_cmd(commands.join '; ')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Disables access to a DB by the application user, and sets the DB to
|
67
|
+
# read-only. Useful when decommissioning instances from a shard that's
|
68
|
+
# been split.
|
69
|
+
def revoke_all_access!
|
70
|
+
user_name = Jetpants.app_credentials[:user]
|
71
|
+
output("Revoking access for user #{user_name} and setting global read-only.")
|
72
|
+
read_only!
|
73
|
+
output(drop_user(user_name, true)) # drop the user without replicating the drop statement to slaves
|
74
|
+
end
|
75
|
+
|
76
|
+
# Enables global read-only mode on the database.
|
77
|
+
def read_only!
|
78
|
+
mysql_root_cmd 'SET GLOBAL read_only = 1' unless read_only?
|
79
|
+
read_only?
|
80
|
+
end
|
81
|
+
|
82
|
+
# Disables global read-only mode on the database.
|
83
|
+
def disable_read_only!
|
84
|
+
mysql_root_cmd 'SET GLOBAL read_only = 0' if read_only?
|
85
|
+
not read_only?
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
module Jetpants
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Replication and binlog-related methods #####################################
|
5
|
+
#++
|
6
|
+
|
7
|
+
class DB
|
8
|
+
# Changes the master for this instance. Supply a Jetpants::DB indicating the new
|
9
|
+
# master, along with options :log_pos, :log_file, :user, :password.
|
10
|
+
# Does NOT automatically start replication afterwards on self!
|
11
|
+
#
|
12
|
+
# If you omit :log_pos or :log_file, uses the current position/file from new_master,
|
13
|
+
# though this is only safe if new_master is not receiving writes!
|
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)
|
17
|
+
def change_master_to(new_master, option_hash={})
|
18
|
+
return disable_replication! unless new_master # change_master_to(nil) alias for disable_replication!
|
19
|
+
return if new_master == master # no change
|
20
|
+
|
21
|
+
logfile = option_hash[:log_file]
|
22
|
+
pos = option_hash[:log_pos]
|
23
|
+
if !(logfile && pos)
|
24
|
+
raise "Cannot use coordinates of a new master that is receiving updates" if new_master.master && ! new_master.repl_paused
|
25
|
+
logfile, pos = new_master.binlog_coordinates
|
26
|
+
end
|
27
|
+
|
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]
|
30
|
+
|
31
|
+
pause_replication if @master && !@repl_paused
|
32
|
+
result = mysql_root_cmd "CHANGE MASTER TO " +
|
33
|
+
"MASTER_HOST='#{new_master.ip}', " +
|
34
|
+
"MASTER_PORT=#{new_master.port}, " +
|
35
|
+
"MASTER_LOG_FILE='#{logfile}', " +
|
36
|
+
"MASTER_LOG_POS=#{pos}, " +
|
37
|
+
"MASTER_USER='#{repl_user}', " +
|
38
|
+
"MASTER_PASSWORD='#{repl_pass}'"
|
39
|
+
|
40
|
+
output "Changing master to #{new_master} with coordinates (#{logfile}, #{pos}): #{result}"
|
41
|
+
@master.slaves.delete(self) if @master rescue nil
|
42
|
+
@master = new_master
|
43
|
+
@repl_paused = true
|
44
|
+
new_master.slaves << self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Pauses replication
|
48
|
+
def pause_replication
|
49
|
+
raise "This DB object has no master" unless master
|
50
|
+
return if @repl_paused
|
51
|
+
output "Pausing replication from #{@master}."
|
52
|
+
output mysql_root_cmd "STOP SLAVE"
|
53
|
+
@repl_paused = true
|
54
|
+
end
|
55
|
+
alias stop_replication pause_replication
|
56
|
+
|
57
|
+
# Starts replication, or restarts replication after a pause
|
58
|
+
def resume_replication
|
59
|
+
raise "This DB object has no master" unless master
|
60
|
+
output "Resuming replication from #{@master}."
|
61
|
+
output mysql_root_cmd "START SLAVE"
|
62
|
+
@repl_paused = false
|
63
|
+
end
|
64
|
+
alias start_replication resume_replication
|
65
|
+
|
66
|
+
# Permanently disables replication
|
67
|
+
def disable_replication!
|
68
|
+
raise "This DB object has no master" unless master
|
69
|
+
output "Disabling replication; this db is no longer a slave."
|
70
|
+
output mysql_root_cmd "STOP SLAVE; RESET SLAVE"
|
71
|
+
@master.slaves.delete(self) rescue nil
|
72
|
+
@master = nil
|
73
|
+
@repl_paused = nil
|
74
|
+
end
|
75
|
+
alias reset_replication! disable_replication!
|
76
|
+
|
77
|
+
# Wipes out the target instances and turns them into slaves of self.
|
78
|
+
# Resumes replication on self afterwards, but does NOT automatically start
|
79
|
+
# replication on the targets.
|
80
|
+
# 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
|
+
# Warning: takes self offline during the process, so don't use on a master that
|
83
|
+
# is actively in use by your application!
|
84
|
+
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])
|
87
|
+
pause_replication if master && ! @repl_paused
|
88
|
+
file, pos = binlog_coordinates
|
89
|
+
clone_to!(targets)
|
90
|
+
targets.each do |t|
|
91
|
+
t.change_master_to( self,
|
92
|
+
log_file: file,
|
93
|
+
log_pos: pos,
|
94
|
+
user: repl_user,
|
95
|
+
password: repl_pass )
|
96
|
+
end
|
97
|
+
resume_replication if @master # should already have happened from the clone_to! restart anyway, but just to be explicit
|
98
|
+
end
|
99
|
+
|
100
|
+
# Wipes out the target instances and turns them into slaves of self's master.
|
101
|
+
# Resumes replication on self afterwards, but does NOT automatically start
|
102
|
+
# replication on the targets.
|
103
|
+
# Warning: takes self offline during the process, so don't use on an active slave!
|
104
|
+
def enslave_siblings!(targets)
|
105
|
+
raise "Can only call enslave_siblings! on a slave instance" unless master
|
106
|
+
disable_monitoring
|
107
|
+
pause_replication unless @repl_paused
|
108
|
+
file, pos = repl_binlog_coordinates
|
109
|
+
clone_to!(targets)
|
110
|
+
targets.each do |t|
|
111
|
+
t.change_master_to( master,
|
112
|
+
log_file: file,
|
113
|
+
log_pos: pos,
|
114
|
+
user: (Jetpants.replication_credentials[:user] || replication_credentials[:user]),
|
115
|
+
password: (Jetpants.replication_credentials[:pass] || replication_credentials[:pass]) )
|
116
|
+
end
|
117
|
+
resume_replication # should already have happened from the clone_to! restart anyway, but just to be explicit
|
118
|
+
catch_up_to_master
|
119
|
+
enable_monitoring
|
120
|
+
end
|
121
|
+
|
122
|
+
# Shortcut to call DB#enslave_siblings! on a single target
|
123
|
+
def enslave_sibling!(target)
|
124
|
+
enslave_siblings!([target])
|
125
|
+
end
|
126
|
+
|
127
|
+
# Use this on a slave to return [master log file name, position] for how far
|
128
|
+
# this slave has executed (in terms of its master's binlogs) in its SQL replication thread.
|
129
|
+
def repl_binlog_coordinates(display_info=true)
|
130
|
+
raise "This instance is not a slave" unless master
|
131
|
+
status = slave_status
|
132
|
+
file, pos = status[:relay_master_log_file], status[:exec_master_log_pos].to_i
|
133
|
+
output "Has executed through master's binlog coordinates of (#{file}, #{pos})." if display_info
|
134
|
+
[file, pos]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a two-element array containing [log file name, position] for this
|
138
|
+
# database. Only useful when called on a master. This is the current
|
139
|
+
# instance's own binlog coordinates, NOT the coordinates of replication
|
140
|
+
# progress on a slave!
|
141
|
+
def binlog_coordinates
|
142
|
+
hash = mysql_root_cmd('SHOW MASTER STATUS', :parse=>true)
|
143
|
+
raise "Cannot obtain binlog coordinates of this master becaues binary logging is not enabled" unless hash[:file]
|
144
|
+
output "Own binlog coordinates are (#{hash[:file]}, #{hash[:position].to_i})."
|
145
|
+
[hash[:file], hash[:position].to_i]
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns the number of seconds beind the master the replication execution is,
|
149
|
+
# as reported by SHOW SLAVE STATUS.
|
150
|
+
def seconds_behind_master
|
151
|
+
raise "This instance is not a slave" unless master
|
152
|
+
slave_status[:seconds_behind_master].to_i
|
153
|
+
end
|
154
|
+
|
155
|
+
# Waits for this instance's SECONDS_BEHIND_MASTER to reach 0 and stay at
|
156
|
+
# 0 after repeated polls (based on threshold and poll_frequency). Will raise
|
157
|
+
# an exception if this has not happened within the timeout period, in seconds.
|
158
|
+
# In other words, with default settings: checks slave lag every 5+ sec, and
|
159
|
+
# returns true if slave lag is zero 3 times in a row. Gives up if this does
|
160
|
+
# not occur within a one-hour period. If a large amount of slave lag is
|
161
|
+
# reported, this method will automatically reduce its polling frequency.
|
162
|
+
def catch_up_to_master(timeout=3600, threshold=3, poll_frequency=5)
|
163
|
+
raise "This instance is not a slave" unless master
|
164
|
+
resume_replication if @repl_paused
|
165
|
+
|
166
|
+
times_at_zero = 0
|
167
|
+
start = Time.now.to_i
|
168
|
+
output "Waiting to catch up to master"
|
169
|
+
while (Time.now.to_i - start) < timeout
|
170
|
+
lag = seconds_behind_master
|
171
|
+
if lag == 0
|
172
|
+
times_at_zero += 1
|
173
|
+
if times_at_zero >= threshold
|
174
|
+
output "Caught up to master."
|
175
|
+
return true
|
176
|
+
end
|
177
|
+
sleep poll_frequency
|
178
|
+
else
|
179
|
+
output "Currently #{lag} seconds behind master."
|
180
|
+
times_at_zero = 0
|
181
|
+
extra_sleep_time = (lag > 30000 ? 300 : (seconds_behind_master / 100).ceil)
|
182
|
+
sleep poll_frequency + extra_sleep_time
|
183
|
+
end
|
184
|
+
end
|
185
|
+
raise "This instance did not catch up to its master within #{timeout} seconds"
|
186
|
+
end
|
187
|
+
|
188
|
+
# Returns a hash containing the information from SHOW SLAVE STATUS
|
189
|
+
def slave_status
|
190
|
+
hash = mysql_root_cmd('SHOW SLAVE STATUS', :parse=>true)
|
191
|
+
hash = {} if hash[:master_user] == 'test'
|
192
|
+
if @master && hash.count < 1
|
193
|
+
message = "should be a slave of #{@master}, but SHOW SLAVE STATUS indicates otherwise"
|
194
|
+
raise "#{self}: #{message}" if Jetpants.verify_replication
|
195
|
+
output message
|
196
|
+
end
|
197
|
+
hash
|
198
|
+
end
|
199
|
+
|
200
|
+
# Reads an existing master.info file on this db or one of its slaves,
|
201
|
+
# propagates the info back to the Jetpants singleton, and returns it
|
202
|
+
def replication_credentials
|
203
|
+
unless @master || @slaves.count > 0
|
204
|
+
raise "Cannot obtain replication credentials from orphaned instance -- this instance is not a slave, and has no slaves"
|
205
|
+
end
|
206
|
+
target = (@master ? self : @slaves[0])
|
207
|
+
user, pass = target.ssh_cmd("cat #{mysql_directory}/master.info | head -6 | tail -2").split
|
208
|
+
{user: user, pass: pass}
|
209
|
+
end
|
210
|
+
|
211
|
+
# Disables binary logging in my.cnf. Does not take effect until you restart
|
212
|
+
# mysql.
|
213
|
+
def disable_binary_logging
|
214
|
+
output "Disabling binary logging in MySQL configuration; will take effect at next restart"
|
215
|
+
comment_out_ini(mysql_config_file, 'log-bin', 'log-slave-updates')
|
216
|
+
end
|
217
|
+
|
218
|
+
# Re-enables binary logging in my.cnf after a prior call to disable_bin_log.
|
219
|
+
# Does not take effect until you restart mysql.
|
220
|
+
def enable_binary_logging
|
221
|
+
output "Re-enabling binary logging in MySQL configuration; will take effect at next restart"
|
222
|
+
uncomment_out_ini(mysql_config_file, 'log-bin', 'log-slave-updates')
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Jetpants
|
2
|
+
|
3
|
+
#--
|
4
|
+
# MySQL server manipulation methods ##########################################
|
5
|
+
#++
|
6
|
+
|
7
|
+
class DB
|
8
|
+
# Shuts down MySQL, and confirms that it is no longer listening.
|
9
|
+
# OK to use this if MySQL is already stopped; it's a no-op then.
|
10
|
+
def stop_mysql
|
11
|
+
output "Attempting to shutdown MySQL"
|
12
|
+
output service(:stop, 'mysql')
|
13
|
+
running = ssh_cmd "netstat -ln | grep #{@port} | wc -l"
|
14
|
+
raise "[#{@ip}] Failed to shut down MySQL: Something is still listening on port #{@port}" unless running.chomp == '0'
|
15
|
+
@running = false
|
16
|
+
end
|
17
|
+
|
18
|
+
# Starts MySQL, and confirms that something is now listening on the port.
|
19
|
+
# Raises an exception if MySQL is already running or if something else is
|
20
|
+
# already running on its port.
|
21
|
+
def start_mysql
|
22
|
+
@repl_paused = false if @master
|
23
|
+
running = ssh_cmd "netstat -ln | grep #{@port} | wc -l"
|
24
|
+
raise "[#{@ip}] Failed to start MySQL: Something is already listening on port #{@port}" unless running.chomp == '0'
|
25
|
+
output "Attempting to start MySQL"
|
26
|
+
output service(:start, 'mysql')
|
27
|
+
confirm_listening
|
28
|
+
@running = true
|
29
|
+
end
|
30
|
+
|
31
|
+
# Restarts MySQL.
|
32
|
+
def restart_mysql
|
33
|
+
@repl_paused = false if @master
|
34
|
+
output "Attempting to restart MySQL"
|
35
|
+
output service(:restart, 'mysql')
|
36
|
+
confirm_listening
|
37
|
+
@running = true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Has no built-in effect. Plugins can override it, and/or implement
|
41
|
+
# before_stop_query_killer and after_stop_query_killer callbacks.
|
42
|
+
def stop_query_killer
|
43
|
+
end
|
44
|
+
|
45
|
+
# Has no built-in effect. Plugins can override it, and/or implement
|
46
|
+
# before_start_query_killer and after_start_query_killer callbacks.
|
47
|
+
def start_query_killer
|
48
|
+
end
|
49
|
+
|
50
|
+
# Confirms that a process is listening on the DB's port
|
51
|
+
def confirm_listening(timeout=10)
|
52
|
+
confirm_listening_on_port(@port, timeout)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the MySQL data directory for this instance. A plugin can override this
|
56
|
+
# if needed, especially if running multiple MySQL instances on the same host.
|
57
|
+
def mysql_directory
|
58
|
+
'/var/lib/mysql'
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the MySQL server configuration file for this instance. A plugin can
|
62
|
+
# override this if needed, especially if running multiple MySQL instances on
|
63
|
+
# the same host.
|
64
|
+
def mysql_config_file
|
65
|
+
'/etc/my.cnf'
|
66
|
+
end
|
67
|
+
|
68
|
+
# Has no built-in effect. Plugins can override it, and/or implement
|
69
|
+
# before_enable_monitoring and after_enable_monitoring callbacks.
|
70
|
+
def enable_monitoring(*services)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Has no built-in effect. Plugins can override it, and/or implement
|
74
|
+
# before_disable_monitoring and after_disable_monitoring callbacks.
|
75
|
+
def disable_monitoring(*services)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
module Jetpants
|
2
|
+
|
3
|
+
#--
|
4
|
+
# State accessors ############################################################
|
5
|
+
#++
|
6
|
+
|
7
|
+
class DB
|
8
|
+
# Returns the Jetpants::DB instance that is the master of this instance, or false if
|
9
|
+
# there isn't one, or nil if we can't tell because this instance isn't running.
|
10
|
+
def master
|
11
|
+
return nil unless running? || @master
|
12
|
+
probe if @master.nil?
|
13
|
+
@master
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns an Array of Jetpants::DB instances that are slaving from this instance,
|
17
|
+
# or nil if we can't tell because this instance isn't running.
|
18
|
+
def slaves
|
19
|
+
return nil unless running? || @slaves
|
20
|
+
probe if @slaves.nil?
|
21
|
+
@slaves
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns true if replication is paused on this instance, false if it isn't, or
|
25
|
+
# nil if this instance isn't a slave (or if we can't tell because the instance
|
26
|
+
# isn't running)
|
27
|
+
def repl_paused?
|
28
|
+
return nil unless master
|
29
|
+
probe if @repl_paused.nil?
|
30
|
+
@repl_paused
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns true if MySQL is running for this instance, false otherwise.
|
34
|
+
# Note that if the host isn't available/online/reachable, we consider
|
35
|
+
# MySQL to not be running.
|
36
|
+
def running?
|
37
|
+
probe if @running.nil?
|
38
|
+
@running
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns true if we've probed this MySQL instance already. Several
|
42
|
+
# methods trigger a probe, including master, slaves, repl_paused?, and
|
43
|
+
# running?.
|
44
|
+
def probed?
|
45
|
+
[@master, @slaves, @running].compact.count >= 3
|
46
|
+
end
|
47
|
+
|
48
|
+
# Probes this instance to discover its status, master, and slaves. Several
|
49
|
+
# other methods trigger a probe automatically, including master, slaves,
|
50
|
+
# repl_paused?, and running?.
|
51
|
+
# Ordinarily this method won't re-probe an instance that has already been
|
52
|
+
# probed, unless you pass force=true. This can be useful if something
|
53
|
+
# external to Jetpants has changed a DB's state while Jetpants is running.
|
54
|
+
# For example, if you're using jetpants console and, for whatever reason,
|
55
|
+
# you stop replication on a slave manually outside of Jetpants. In this
|
56
|
+
# case you will need to force a probe so that Jetpants learns about the
|
57
|
+
# change.
|
58
|
+
def probe(force=false)
|
59
|
+
return if probed? && !force
|
60
|
+
output "Probing MySQL installation"
|
61
|
+
probe_running
|
62
|
+
probe_master
|
63
|
+
probe_slaves
|
64
|
+
end
|
65
|
+
|
66
|
+
# Alias for probe(true)
|
67
|
+
def probe!() probe(true) end
|
68
|
+
|
69
|
+
# Returns true if the MySQL slave I/O thread and slave SQL thread are
|
70
|
+
# both running, false otherwise. Note that this always checks the current
|
71
|
+
# actual state of the instance, as opposed to DB#repl_paused? which just
|
72
|
+
# remembers the state from the previous probe and any actions since then.
|
73
|
+
def replicating?
|
74
|
+
status = slave_status
|
75
|
+
[status[:slave_io_running], status[:slave_sql_running]].all? {|s| s.downcase == 'yes'}
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns true if this instance has a master, false otherwise.
|
79
|
+
def is_slave?
|
80
|
+
!!master
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns true if this instance had at least one slave when it was last
|
84
|
+
# probed, false otherwise. (This method will indirectly force a probe if
|
85
|
+
# the instance hasn't been probed before.)
|
86
|
+
def has_slaves?
|
87
|
+
slaves.count > 0
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns true if the global READ_ONLY variable is set, false otherwise.
|
91
|
+
def read_only?
|
92
|
+
global_variables[:read_only].downcase == 'on'
|
93
|
+
end
|
94
|
+
|
95
|
+
# Confirms instance has no more than [max] connections currently
|
96
|
+
# (AS VISIBLE TO THE APP USER), and in [interval] seconds hasn't
|
97
|
+
# received more than [threshold] additional connections.
|
98
|
+
# You may need to adjust max if running multiple query killers,
|
99
|
+
# monitoring agents, etc.
|
100
|
+
def taking_connections?(max=4, interval=2.0, threshold=1)
|
101
|
+
current_conns = query_return_array('show processlist').count
|
102
|
+
return true if current_conns > max
|
103
|
+
conn_counter = global_status[:Connections].to_i
|
104
|
+
sleep(interval)
|
105
|
+
global_status[:Connections].to_i - conn_counter > threshold
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns true if this instance appears to be a standby slave,
|
109
|
+
# false otherwise. Note that "standby" in this case is based
|
110
|
+
# on whether the slave is actively receiving connections, not
|
111
|
+
# based on any Pool's understanding of the slave's state. An asset-
|
112
|
+
# tracker plugin may want to override this to determine standby
|
113
|
+
# status differently.
|
114
|
+
def is_standby?
|
115
|
+
!(running?) || (is_slave? && !taking_connections?)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Jetpants supports a notion of dedicated backup machines, containing one
|
119
|
+
# or more MySQL instances that are considered "backup slaves", which will
|
120
|
+
# never be promoted to serve production queries. The default
|
121
|
+
# implementation identifies these by a hostname beginning with "backup".
|
122
|
+
# You may want to override this with a plugin to use a different scheme
|
123
|
+
# if your architecture contains a similar type of node.
|
124
|
+
def for_backups?
|
125
|
+
@host.hostname.start_with? 'backup'
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a hash mapping global MySQL variables (as symbols)
|
129
|
+
# to their values (as strings).
|
130
|
+
def global_variables
|
131
|
+
query_return_array('show global variables').reduce do |variables, variable|
|
132
|
+
variables[variable[:Variable_name].to_sym] = variable[:Value]
|
133
|
+
variables
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a hash mapping global MySQL status fields (as symbols)
|
138
|
+
# to their values (as strings).
|
139
|
+
def global_status
|
140
|
+
query_return_array('show global status').reduce do |variables, variable|
|
141
|
+
variables[variable[:Variable_name].to_sym] = variable[:Value]
|
142
|
+
variables
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the Jetpants::Pool that this instance belongs to, if any.
|
147
|
+
def pool
|
148
|
+
Jetpants.topology.pool(self) || Jetpants.topology.pool(master)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
###### Private methods #####################################################
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
# Check if mysqld is running
|
157
|
+
def probe_running
|
158
|
+
if @host.available?
|
159
|
+
status = service(:status, 'mysql')
|
160
|
+
@running = !(status.downcase.include?('not running'))
|
161
|
+
else
|
162
|
+
@running = false
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Checks slave status to determine master and whether replication is paused
|
167
|
+
# An asset-tracker plugin may want to implement DB#after_probe_master to
|
168
|
+
# populate @master even if @running is false.
|
169
|
+
def probe_master
|
170
|
+
return unless @running # leaves @master as nil to indicate unknown state
|
171
|
+
status = slave_status
|
172
|
+
if !status || status.count < 1
|
173
|
+
@master = false
|
174
|
+
else
|
175
|
+
@master = self.class.new(status[:master_host], status[:master_port])
|
176
|
+
if status[:slave_io_running] != status[:slave_sql_running]
|
177
|
+
message = "One replication thread is stopped and the other is not"
|
178
|
+
raise "#{self}: #{message}" if Jetpants.verify_replication
|
179
|
+
output message
|
180
|
+
pause_replication
|
181
|
+
end
|
182
|
+
@repl_paused = (status[:slave_io_running].downcase == 'no')
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Check processlist as root to determine replication clients. This assumes
|
187
|
+
# you're running only one MySQL instance per machine, and all MySQL instances
|
188
|
+
# use the standard port 3306. This is a limitation of SHOW PROCESSLIST not
|
189
|
+
# containing the slave's listening port.
|
190
|
+
#
|
191
|
+
# An asset-tracker plugin may want to implement DB#after_probe_slaves to
|
192
|
+
# populate @slaves even if @running is false.
|
193
|
+
#
|
194
|
+
# Plugins may want to override DB#probe_slaves itself too, if running multiple
|
195
|
+
# MySQL instances per physical machine. In this case you'll want to use
|
196
|
+
# SHOW SLAVE HOSTS, and all slaves must be using the --report-host option.
|
197
|
+
def probe_slaves
|
198
|
+
return unless @running # leaves @slaves as nil to indicate unknown state
|
199
|
+
@slaves = []
|
200
|
+
slaves_mutex = Mutex.new
|
201
|
+
processes = mysql_root_cmd("SHOW PROCESSLIST", :terminator => ';').split("\n")
|
202
|
+
processes.grep(/Binlog Dump/).concurrent_each do |p|
|
203
|
+
tokens = p.split
|
204
|
+
ip, dummy = tokens[2].split ':'
|
205
|
+
db = self.class.new(ip)
|
206
|
+
db.probe
|
207
|
+
slaves_mutex.synchronize {@slaves << db if db.master == self}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|