mysql_manager 1.0.0

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