jetpants 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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