mysql_manager 1.0.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/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # MySQL Manager
2
+
3
+ MySQL Manager is a utility to perform routine tasks on a MySQL database.
4
+
5
+ * Continuously execute `SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1` and `START SLAVE` statements until replication is caught up (leaves slave in inconsistent state with master)
6
+ * Reload `my.cnf` without restarting MySQL (limited to dynamic variables)
7
+ * Kill queries that match a set of criteria (execution time, user, db, state, command, host, query) using PCRE regexes or literal strings.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'mysql_manager'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install mysql_manager
22
+
23
+ ## Usage
24
+
25
+ Usage: mysql-manager options
26
+ --kill Kill queries based on specified criteria
27
+ --kill:max-query-time TIME Kill queries that have been running for more than TIME
28
+ --kill:user USER Kill queries matching USER (repeatable)
29
+ --kill:host HOST Kill queries matching HOST (repeatable)
30
+ --kill:query SQL Kill queries matching SQL (repeatable)
31
+ --kill:command COMMAND Kill queries matching COMMAND (repeatable)
32
+ --kill:state STATE Kill queries matching STATE (repeatable)
33
+ --kill:db DB Kill queries matching DB (repeatable)
34
+ --skip-replication-errors Skip replication errors based on specified criteria
35
+ --skip-replication-errors:max-error-duration SECONDS
36
+ Abort after attempting to recover after SECONDS elapsed (default: )
37
+ --skip-replication-errors:min-healthy-duration SECONDS
38
+ Abort after replication healthy for SECONDS elapsed (default: -1)
39
+ --skip-replication-errors:max-errors SECONDS
40
+ Output replication status events every SECONDS elapsed (default: 10)
41
+ --reload-my-cnf Reload my.cnf based on specified criteria
42
+ --reload-my-cnf:config FILE Issue set 'SET GLOBAL' for each variable in FILE (default: /etc/my.cnf)
43
+ --reload-my-cnf:groups GROUP Issue set 'SET GLOBAL' for each variable in FILE (default: mysqld, mysqld_safe, mysql.server, mysql_server, server, mysql)
44
+ --db:dsn DSN DSN to connect to MySQL database (default: DBI:Mysql:mysql:localhost)
45
+ --db:username USERNAME Username corresponding to DSN (default: root)
46
+ --db:password PASSWORD Password corresponding to DSN (default: )
47
+ --log:level LEVEL Logging level
48
+ --log:file FILE Write logs to FILE (default: STDERR)
49
+ --log:age DAYS Rotate logs after DAYS pass (default: 7)
50
+ --log:size SIZE Rotate logs after the grow past SIZE bytes
51
+ --dry-run Do not run statements which affect the state of the database when executed
52
+ -V, --version Display version information
53
+ -h, --help Display this screen
54
+
55
+ ## Examples
56
+
57
+ Kill all queries by user "api" that have been running longer than 30 seconds:
58
+
59
+ mysql-manager --kill --kill:user api --kill:max-query-time 30 --log:level DEBUG --dry-run
60
+
61
+ Recover a MySQL Slave that has failed replication and wait for it to remain healthy (fully caught up to master) for 60 seconds.
62
+
63
+ mysql-manager --skip-replication-errors --skip-replication-errors:min-healthy-duration 60 --log:level DEBUG
64
+
65
+ Reload `/etc/my.cnf` without restarting MySQL:
66
+
67
+ mysql-manager --reload-my-cnf --reload-my-cnf:config /etc/my.cnf --log:level DEBUG
68
+
69
+ ## Contributing
70
+
71
+ 1. Fork it
72
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
73
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
74
+ 4. Push to the branch (`git push origin my-new-feature`)
75
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/mysql-manager ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # MySQL Manager - a utility to perform routine tasks on a MySQL database
4
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
5
+ #
6
+ # This file is part of MySQL Manager.
7
+ #
8
+ # MySQL Manager is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # MySQL Manager is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with MySQL Manager. If not, see <http://www.gnu.org/licenses/>.
20
+ #
21
+ $:.unshift(File.expand_path('.')) # Ruby 1.9 doesn't have . in the load path...
22
+ $:.push(File.expand_path('lib/'))
23
+
24
+ require 'rubygems'
25
+ require 'mysql_manager'
26
+
27
+ command_line = MysqlManager::CommandLine.new
28
+ command_line.execute
@@ -0,0 +1,25 @@
1
+ #
2
+ # MySQL Manager - a utility to perform routine tasks on a MySQL database
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of MySQL Manager.
6
+ #
7
+ # MySQL Manager is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # MySQL Manager is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with MySQL Manager. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'mysql_manager/version'
21
+ require 'mysql_manager/utilities'
22
+ require 'mysql_manager/command_line'
23
+
24
+ module MysqlManager
25
+ end
@@ -0,0 +1,245 @@
1
+ #
2
+ # MySQL Manager - a utility to perform routine tasks on a MySQL database
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of MySQL Manager.
6
+ #
7
+ # MySQL Manager is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # MySQL Manager is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with MySQL Manager. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'optparse'
21
+
22
+ module MysqlManager
23
+ class MissingArgumentException < Exception; end
24
+
25
+ class CommandLine
26
+ attr_accessor :utilities
27
+
28
+ def to_pattern(str)
29
+ if str =~ /^\/(.*?)\/$/
30
+ Regexp.new($1.to_s)
31
+ else
32
+ Regexp.new("^#{Regexp.escape(str)}$")
33
+ end
34
+ end
35
+
36
+ def initialize
37
+ @options = {}
38
+ @options[:db] = {}
39
+ @options[:log] = {}
40
+ @options[:kill] = {}
41
+ @options[:reload_my_cnf] = {}
42
+ @options[:skip_replication_errors] = {}
43
+
44
+ begin
45
+ @optparse = OptionParser.new do |opts|
46
+ opts.banner = "Usage: #{$0} options"
47
+
48
+
49
+ #
50
+ # Killing options
51
+ #
52
+ @options[:kill][:execute] = false
53
+ opts.on( '--kill', 'Kill queries based on specified criteria') do
54
+ @options[:kill][:execute] = true
55
+ end
56
+
57
+ @options[:kill][:max_query_time] = -1
58
+ opts.on( '--kill:max-query-time TIME', 'Kill queries that have been running for more than TIME') do |time|
59
+ @options[:kill][:max_query_time] = time.to_i
60
+ end
61
+
62
+ @options[:kill][:user] = []
63
+ opts.on( '--kill:user USER', 'Kill queries matching USER (repeatable)') do |user|
64
+ @options[:kill][:user] << to_pattern(user)
65
+ end
66
+
67
+ @options[:kill][:host] = []
68
+ opts.on( '--kill:host HOST', 'Kill queries matching HOST (repeatable)') do |host|
69
+ @options[:kill][:host] << to_pattern(host)
70
+ end
71
+
72
+ @options[:kill][:query] = []
73
+ opts.on( '--kill:query SQL', 'Kill queries matching SQL (repeatable)') do |sql|
74
+ @options[:kill][:query] << to_pattern(sql)
75
+ end
76
+
77
+ @options[:kill][:command] = []
78
+ opts.on( '--kill:command COMMAND', 'Kill queries matching COMMAND (repeatable)') do |command|
79
+ @options[:kill][:command] << to_pattern(command)
80
+ end
81
+
82
+ @options[:kill][:state] = []
83
+ opts.on( '--kill:state STATE', 'Kill queries matching STATE (repeatable)') do |state|
84
+ @options[:kill][:state] << to_patterns(state)
85
+ end
86
+
87
+ @options[:kill][:db] = []
88
+ opts.on( '--kill:db DB', 'Kill queries matching DB (repeatable)') do |db|
89
+ @options[:kill][:db] << to_pattern(db)
90
+ end
91
+
92
+ #
93
+ # Skip Replication Error options
94
+ #
95
+ @options[:skip_replication_errors][:execute] = false
96
+ opts.on( '--skip-replication-errors', 'Skip replication errors based on specified criteria') do
97
+ @options[:skip_replication_errors][:execute] = true
98
+ end
99
+
100
+ @options[:skip_replication_errors][:max_errors] = -1
101
+ opts.on( '--skip-replication-errors:max-errors NUMBER', "Abort after encountering NUMBER of errors (default: #{@options[:skip_replication_errors][:max_errors]})") do |number|
102
+ @options[:skip_replication_errors][:max_errors] = number.to_i
103
+ end
104
+
105
+ @options[:skip_replication_errors][:max_error_duration] = -1
106
+ opts.on( '--skip-replication-errors:max-error-duration SECONDS', "Abort after attempting to recover after SECONDS elapsed (default: #{@options[:skip_replication_errors][:max_error_durration]})") do |seconds|
107
+ @options[:skip_replication_errors][:max_error_duration] = seconds.to_i
108
+ end
109
+
110
+ @options[:skip_replication_errors][:min_healthy_duration] = -1
111
+ opts.on( '--skip-replication-errors:min-healthy-duration SECONDS', "Abort after replication healthy for SECONDS elapsed (default: #{@options[:skip_replication_errors][:min_healthy_duration]})") do |seconds|
112
+ @options[:skip_replication_errors][:min_healthy_duration] = seconds.to_i
113
+ end
114
+
115
+ @options[:skip_replication_errors][:log_frequency] = 10
116
+ opts.on( '--skip-replication-errors:max-errors SECONDS', "Output replication status events every SECONDS elapsed (default: #{@options[:skip_replication_errors][:log_frequency]})") do |seconds|
117
+ @options[:skip_replication_errors][:log_frequency] = seconds.to_i
118
+ end
119
+
120
+ #
121
+ # Reload my.cnf options
122
+ #
123
+ @options[:reload_my_cnf][:execute] = false
124
+ opts.on( '--reload-my-cnf', 'Reload my.cnf based on specified criteria') do
125
+ @options[:reload_my_cnf][:execute] = true
126
+ end
127
+
128
+
129
+ @options[:reload_my_cnf][:config] = '/etc/my.cnf'
130
+ opts.on( '--reload-my-cnf:config FILE', "Issue set 'SET GLOBAL' for each variable in FILE (default: #{@options[:reload_my_cnf][:config]})") do |file|
131
+ @options[:reload_my_cnf][:config] = file
132
+ end
133
+
134
+ @options[:reload_my_cnf][:groups] = ['mysqld', 'mysqld_safe', 'mysql.server', 'mysql_server', 'server', 'mysql']
135
+ opts.on( '--reload-my-cnf:groups GROUP', "Issue set 'SET GLOBAL' for each variable in FILE (default: #{@options[:reload_my_cnf][:groups].join(', ')})") do |group|
136
+ @options[:reload_my_cnf][:groups] << group
137
+ end
138
+
139
+
140
+ #
141
+ # Database connection options
142
+ #
143
+ @options[:db][:dsn] = 'DBI:Mysql:mysql:localhost'
144
+ opts.on( '--db:dsn DSN', "DSN to connect to MySQL database (default: #{@options[:db][:dsn]})" ) do|dsn|
145
+ @options[:db][:dsn] = dsn
146
+ end
147
+
148
+ @options[:db][:username] = 'root'
149
+ opts.on( '--db:username USERNAME', "Username corresponding to DSN (default: #{@options[:db][:username]})" ) do|username|
150
+ @options[:username] = username
151
+ end
152
+
153
+ @options[:db][:password] = ""
154
+ opts.on( '--db:password PASSWORD', "Password corresponding to DSN (default: #{@options[:db][:password]})" ) do|password|
155
+ @options[:db][:password] = password
156
+ end
157
+
158
+ #
159
+ # Logging options
160
+ #
161
+ @options[:log][:level] = Logger::INFO
162
+ opts.on( '--log:level LEVEL', 'Logging level' ) do|level|
163
+ @options[:log][:level] = Logger.const_get level.upcase
164
+ end
165
+
166
+ @options[:log][:file] = STDERR
167
+ opts.on( '--log:file FILE', 'Write logs to FILE (default: STDERR)' ) do|file|
168
+ @options[:log][:file] = File.open(file, File::WRONLY | File::APPEND | File::CREAT)
169
+ end
170
+
171
+ @options[:log][:age] = 7
172
+ opts.on( '--log:age DAYS', "Rotate logs after DAYS pass (default: #{@options[:log][:age]})" ) do|days|
173
+ @options[:log][:age] = days.to_i
174
+ end
175
+
176
+ @options[:log][:size] = 1024*1024*10
177
+ opts.on( '--log:size SIZE', 'Rotate logs after the grow past SIZE bytes' ) do |size|
178
+ @options[:log][:size] = size.to_i
179
+ end
180
+
181
+ #
182
+ # General options
183
+ #
184
+ @options[:dry_run] = false
185
+ opts.on( '--dry-run', 'Do not run statements which affect the state of the database when executed' ) do
186
+ @options[:dry_run] = true
187
+ end
188
+
189
+
190
+ opts.on( '-V', '--version', 'Display version information' ) do
191
+ puts "MySQL Manager #{MysqlManager::VERSION}"
192
+ puts "Copyright (C) 2012 Erik Osterman <e@osterman.com>"
193
+ puts "License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>"
194
+ puts "This is free software: you are free to change and redistribute it."
195
+ puts "There is NO WARRANTY, to the extent permitted by law."
196
+ exit
197
+ end
198
+
199
+ opts.on( '-h', '--help', 'Display this screen' ) do
200
+ puts opts
201
+ exit
202
+ end
203
+ end
204
+
205
+ @optparse.parse!
206
+
207
+ raise MissingArgumentException.new("No action specified") if @options.select { |k,v| v.instance_of?(Hash) && v.has_key?(:execute) && v[:execute] == true }.size == 0
208
+ @log = Logger.new(@options[:log][:file], @options[:log][:age], @options[:log][:size])
209
+ @log.level = @options[:log][:level]
210
+ @utilities = Utilities.new(@options[:db])
211
+ @utilities.log = @log
212
+ @utilities.dry_run = @options[:dry_run]
213
+ rescue MissingArgumentException => e
214
+ puts e.message
215
+ puts @optparse
216
+ exit (1)
217
+ end
218
+ end
219
+
220
+ def execute
221
+ @options.each do |type,options|
222
+ next if options.instance_of?(Hash) && options.has_key?(:execute) && options[:execute] == false
223
+ begin
224
+ case type
225
+ when :kill
226
+ @log.debug("about to call kill_queries")
227
+ @utilities.kill_queries(options)
228
+ when :skip_replication_errors
229
+ @log.debug("about to call skip_replication_errors")
230
+ @utilities.skip_replication_errors(options)
231
+ when :reload_my_cnf
232
+ @log.debug("about to call reload_my_cnf")
233
+ @utilities.reload_my_cnf(options)
234
+ end
235
+ rescue FileNotFoundException => e
236
+ @log.fatal(e.message)
237
+ rescue Interupt
238
+ @log.info("Exiting")
239
+ rescue Exception => e
240
+ @log.fatal(e.message + e.backtrace.join("\n"))
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,215 @@
1
+ #
2
+ # MySQL Manager - a utility to perform routine tasks on a MySQL database
3
+ # Copyright (C) 2012 Erik Osterman <e@osterman.com>
4
+ #
5
+ # This file is part of MySQL Manager.
6
+ #
7
+ # MySQL Manager is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # MySQL Manager is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with MySQL Manager. If not, see <http://www.gnu.org/licenses/>.
19
+ #
20
+ require 'dbi'
21
+ require 'logger'
22
+ require 'parseconfig'
23
+
24
+ module MysqlManager
25
+ class FileNotFoundException < Exception; end
26
+
27
+ class Utilities
28
+ attr_accessor :dbh, :log, :dry_run
29
+
30
+ def initialize(options = {})
31
+ options[:dsn] ||= "DBI:Mysql:mysql:localhost"
32
+ options[:username] ||= "root"
33
+ options[:password] ||= ""
34
+
35
+ @log = Logger.new(STDERR)
36
+
37
+ # connect to the MySQL server
38
+ @dbh = DBI.connect(options[:dsn], options[:username], options[:password])
39
+ end
40
+
41
+ def kill_queries(options = {})
42
+ options[:max_query_time] ||= -1
43
+ options[:user] ||= []
44
+ options[:host] ||= []
45
+ options[:query] ||= []
46
+ options[:command] ||= []
47
+ options[:state] ||= []
48
+ options[:db] ||= []
49
+
50
+ @dbh.execute("SHOW FULL PROCESSLIST") do |sth|
51
+ sth.fetch_hash() do |row|
52
+ next if row['Command'] == 'Binlog Dump'
53
+ next if row['User'] == 'system user'
54
+
55
+ results = []
56
+ options.each_pair do |field,criteria|
57
+ case field
58
+ when :max_query_time
59
+ if criteria >= 0
60
+ if row['Time'].to_i > criteria
61
+ results << true
62
+ else
63
+ results << false
64
+ end
65
+ end
66
+ when :user, :host, :query, :command, :state, :db
67
+ if criteria.length > 0
68
+ matched = false
69
+ criteria.each do |pattern|
70
+ if pattern.match(row[field.to_s.capitalize])
71
+ matched = true
72
+ break
73
+ end
74
+ end
75
+ #puts "#{row[field.to_s.capitalize]} #{criteria.inspect} == #{matched}"
76
+ results << matched
77
+ end
78
+ end
79
+ end
80
+ # Some conditions need to apply
81
+ #puts results.inspect
82
+ if results.length > 0
83
+ # None of them may be false
84
+ unless results.include?(false)
85
+ begin
86
+ @log.info("Killing id:#{row['Id']} db:#{row['db']} user:#{row['User']} command:#{row['Command']} state:#{row['State']} time:#{row['Time']} host:#{row['Host']} query:#{row['Info']} rows_sent:#{row['Rows_sent']} rows_examined:#{row['Rows_examined']} rows_read:#{row['Rows_read']}")
87
+ @dbh.do("KILL #{row['Id']}") unless @dry_run
88
+ rescue DBI::DatabaseError => e
89
+ @log.warn(e.message)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def reload_my_cnf(options = {})
98
+ options[:config] ||= '/etc/my.cnf'
99
+ options[:groups] ||= ['mysqld', 'mysqld_safe', 'mysql.server', 'mysql_server', 'server', 'mysql']
100
+
101
+ variables = {}
102
+ @dbh.execute("SHOW VARIABLES") do |sth|
103
+ sth.fetch_hash() do |row|
104
+ variables[row['Variable_name']] = row['Value']
105
+ end
106
+ end
107
+ unless File.exists?(options[:config])
108
+ raise FileNotFoundException.new("Unable to open file #{options[:config]}")
109
+ end
110
+
111
+ my_cnf = ParseConfig.new(options[:config])
112
+ my_cnf.groups.each do |group|
113
+ if options[:groups].include?(group)
114
+ @log.debug("loading values from [#{group}]")
115
+ my_cnf[group].each_pair do |k,v|
116
+ next if v.nil? || v.empty?
117
+ if variables.has_key?(k)
118
+ begin
119
+ v = v.to_s
120
+ v = v.to_i if v =~ /^(\d+)$/
121
+ v = $1.to_i * (1024) if v =~ /^(\d+)K$/
122
+ v = $1.to_i * (1024*1024) if v =~ /^(\d+)M$/
123
+ v = $1.to_i * (1024*1024*1024) if v =~ /^(\d+)G$/
124
+ if v.instance_of?(Integer) || v.instance_of?(Fixnum)
125
+ sql = "SET GLOBAL #{k} = #{v}"
126
+ else
127
+ sql = "SET GLOBAL #{k} = '#{v}'"
128
+ end
129
+ @dbh.do(sql) unless @dry_run
130
+ @log.info("set #{k}=#{v}")
131
+ rescue DBI::DatabaseError => e
132
+ @log.debug(@dbh.last_statement)
133
+ @log.warn(e.message)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def skip_replication_errors(options = {})
142
+ options[:max_errors] ||= -1
143
+ options[:max_error_duration] ||= -1
144
+ options[:min_healthy_duration] ||= -1
145
+ options[:log_frequency] ||= 10
146
+
147
+ begin
148
+ # get server version string and display it
149
+ max_seconds_behind = 0
150
+ t_start = Time.now.to_f
151
+ t_last_error = nil
152
+ t_last_error_elapsed = 0
153
+ t_last_log = 0
154
+ t_elapsed = 0
155
+ t_recovered = 0
156
+ errors = 0
157
+ while (options[:max_errors] < 0 || errors < options[:max_errors]) && (options[:max_error_duration] < 0 || t_last_error_elapsed < options[:max_error_duration])
158
+ @dbh.execute("SHOW SLAVE STATUS") do |sth|
159
+ t_now = Time.now.to_f
160
+
161
+ sth.fetch_hash() do |row|
162
+ seconds_behind = row['Seconds_Behind_Master']
163
+ if seconds_behind.nil?
164
+ @log.info("replication broken")
165
+ @log.info("last error: #{row['Last_Error'].gsub(/\r?\n/, '')}")
166
+
167
+ @dbh.do("SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1") unless @dry_run
168
+ @dbh.do("START SLAVE")
169
+ errors += 1
170
+ max_seconds_behind = 0
171
+ t_last_error = t_now
172
+ elsif seconds_behind == 0
173
+ t_recovered = t_now if t_recovered == 0
174
+ if t_last_log == 0 || t_now - t_last_log > options[:log_frequency]
175
+ @log.info("fully caught up with master")
176
+ t_last_log = t_now
177
+ end
178
+ if (t_min_healthy_duration >=0) && (t_now - t_recovered > t_min_healthy_duration)
179
+ @log.info("satisfied health duration window")
180
+ break
181
+ end
182
+ else
183
+ seconds_behind = seconds_behind.to_f
184
+ t_recovered = 0
185
+ t_last_error_elapsed = t_last_error.nil? ? 0 : t_now - t_last_error
186
+ t_elapsed = t_now - t_start
187
+ max_seconds_behind = [max_seconds_behind, seconds_behind].max
188
+ t_catchup = max_seconds_behind - seconds_behind
189
+ t_rate = t_elapsed == 0 ? 1 : t_catchup/t_elapsed
190
+ t_left = t_rate > 0 ? seconds_behind/t_rate : 0
191
+ if t_left > 60*60
192
+ t_left_str = sprintf('%.2f hours', t_left/(60*60))
193
+ elsif t_left > 60
194
+ t_left_str = sprintf('%.2f mins', t_left/60)
195
+ else
196
+ t_left_str = sprintf('%.2f secs', t_left)
197
+ end
198
+ if t_last_log == 0 || t_now.to_f - t_last_log.to_f > options[:log_frequency]
199
+ @log.info("#{seconds_behind} seconds behind master; #{t_catchup} seconds caught up; #{sprintf('%.2f', t_rate)} seconds/second; #{t_left_str} left; #{errors} errors; last error #{sprintf('%.2f', t_last_error_elapsed)} seconds ago")
200
+ t_last_log = t_now
201
+ end
202
+
203
+ sleep 0.100
204
+ end
205
+ end
206
+ end
207
+ end
208
+ rescue Interrupt
209
+ @log.info("Aborted by user")
210
+ rescue DBI::DatabaseError => e
211
+ @log.error("Error code: #{e.err}: #{e.errstr}")
212
+ end
213
+ end
214
+ end
215
+ end