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