mysql_manager 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +75 -0
- data/Rakefile +2 -0
- data/bin/mysql-manager +28 -0
- data/lib/mysql_manager.rb +25 -0
- data/lib/mysql_manager/command_line.rb +245 -0
- data/lib/mysql_manager/utilities.rb +215 -0
- data/lib/mysql_manager/version.rb +22 -0
- data/mysql_manager.gemspec +21 -0
- metadata +125 -0
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
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
|