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.
@@ -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