mysql-replication-helper 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,27 @@
1
+ = mysql-replication-helper
2
+
3
+ This is a script that mitigates the errors generated by a Master/Slave MySQL
4
+ replication configuration.
5
+
6
+ A slave database engine needs to be updated in tandem with the master to
7
+ account for changes such as:
8
+
9
+ * Adding a new database
10
+ * Adding a new user
11
+ * Adding views which reference particular users
12
+
13
+ The replication helper facilitates this by identifying typical errors and
14
+ fixing them.
15
+
16
+ == Installing
17
+
18
+ % gem sources -a http://gems.github.com
19
+ % sudo gem install theworkinggroup-mysql-replication-helper
20
+
21
+ == Starting
22
+
23
+ % replication-helper --daemon
24
+
25
+ == Copyright
26
+
27
+ Copyright (c) 2009 The Working Group Inc. (http://twg.ca/)
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "mysql-replication-helper"
8
+ gem.summary = %Q{MySQL Replication Helper}
9
+ gem.email = "github@tadman.ca"
10
+ gem.homepage = "http://github.com/theworkinggroup/mysql-replication-helper"
11
+ gem.authors = [ 'Scott Tadman' ]
12
+ gem.executables = %w[ replication-helper ]
13
+ gem.add_dependency 'daemons'
14
+
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/*_test.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :default => :test
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ if File.exist?('VERSION')
46
+ version = File.read('VERSION').chomp
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "mysql-replication-helper #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # replication-helper MySQL replication helper agent
4
+ #
5
+ # chkconfig: - 65 34
6
+ # description: MySQL replication helper agent
7
+ # processname: replication-helper
8
+ # config: /etc/replication-helper.conf
9
+ # pidfile: /var/run/replication-helper.pid
10
+ #
11
+
12
+ require 'rubygems'
13
+ require 'daemons'
14
+
15
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
16
+
17
+ require 'mysql_replication_helper'
18
+
19
+ daemon_script_path = File.expand_path('../lib/mysql_replication_helper/daemon_launcher.rb', File.dirname(__FILE__))
20
+
21
+ if (!File.exist?(daemon_script_path))
22
+ daemon_script_path = Gem.required_location('mysql_replication_helper','mysql_replication_helper/daemon_launcher.rb')
23
+ end
24
+
25
+ Daemons.run(daemon_script_path)
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'mysql'
3
+
4
+ module MysqlReplicationHelper
5
+ # == Autoloads ============================================================
6
+
7
+ autoload(:Agent, 'mysql_replication_helper/agent')
8
+ autoload(:ErrorHandler, 'mysql_replication_helper/error_handler')
9
+ autoload(:Daemon, 'mysql_replication_helper/daemon')
10
+ end
@@ -0,0 +1,51 @@
1
+ module MysqlReplicationHelper
2
+ class Agent
3
+ autoload(:Master, 'mysql_replication_helper/agent/master')
4
+ autoload(:Slave, 'mysql_replication_helper/agent/slave')
5
+
6
+ DEFAULT_OPTIONS = {
7
+ :host => 'localhost',
8
+ :user => 'root',
9
+ :master_socket => '/local/db/mysql.sock',
10
+ :slave_socket => '/ebs/db/mysql.sock'
11
+ }
12
+
13
+ def initialize(options)
14
+ @options = with_default_options(options)
15
+ end
16
+
17
+ def connection
18
+ @connection ||=
19
+ Mysql.real_connect(
20
+ @options[:host],
21
+ user_name,
22
+ @options[:password],
23
+ @options[:db],
24
+ @options[:port],
25
+ socket_name
26
+ )
27
+ end
28
+
29
+ def user_name
30
+ @options[:user]
31
+ end
32
+
33
+ def socket_name
34
+ @options[:socket]
35
+ end
36
+
37
+ def with_default_options(options)
38
+ DEFAULT_OPTIONS.merge(options)
39
+ end
40
+
41
+ def execute(statement)
42
+ STDERR.puts(statement)
43
+ connection.real_query(statement)
44
+ end
45
+
46
+ def query(statement)
47
+ STDERR.puts(statement)
48
+ connection.query(statement)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,28 @@
1
+ module MysqlReplicationHelper
2
+ class Agent
3
+ class Master < Agent
4
+ def poll!
5
+ # Nothing yet
6
+ end
7
+
8
+ def master_status
9
+ row = query("SHOW MASTER STATUS").fetch_row
10
+
11
+ return unless (row)
12
+
13
+ {
14
+ :master_log_file => row[0],
15
+ :master_log_position => row[1].to_i
16
+ }
17
+ end
18
+
19
+ def user_name
20
+ @options[:master_user] or super
21
+ end
22
+
23
+ def socket_name
24
+ @options[:master_socket] or super
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,68 @@
1
+ module MysqlReplicationHelper
2
+ class Agent
3
+ class Slave < Agent
4
+ include MysqlReplicationHelper::ErrorHandler
5
+
6
+ def poll!
7
+ if (configured?)
8
+ if (error_message = slave_error)
9
+ if (statements = sql_to_recover_from(error_message))
10
+ statements.each do |sql|
11
+ connection.real_query(sql)
12
+ end
13
+ else
14
+ # Unrecoverable error?
15
+ end
16
+ end
17
+ else
18
+ assign_master(@options[:master])
19
+ start!
20
+ end
21
+ end
22
+
23
+ def configured?
24
+ !!query("SHOW SLAVE STATUS").fetch_row
25
+ end
26
+
27
+ def slave_error
28
+ row = query("SHOW SLAVE STATUS").fetch_row
29
+
30
+ row and row[19]
31
+ end
32
+
33
+ def assign_master(master)
34
+ master_status = master.master_status
35
+
36
+ master_options =
37
+ {
38
+ 'MASTER_HOST' => 'localhost',
39
+ 'MASTER_USER' => master.user_name,
40
+ 'MASTER_PORT' => 3306,
41
+ 'MASTER_LOG_FILE' => master_status[:master_log_file],
42
+ 'MASTER_LOG_POS' => master_status[:master_log_position]
43
+ }.collect do |k, v|
44
+ case (v)
45
+ when String:
46
+ "#{k}='#{Mysql.quote(v)}'"
47
+ else
48
+ "#{k}=#{v}"
49
+ end
50
+ end
51
+
52
+ execute("CHANGE MASTER TO #{master_options * ', '}")
53
+ end
54
+
55
+ def start!
56
+ execute("START SLAVE")
57
+ end
58
+
59
+ def user_name
60
+ @options[:slave_user] or super
61
+ end
62
+
63
+ def socket_name
64
+ @options[:slave_socket] or super
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,18 @@
1
+ module MysqlReplicationHelper
2
+ class Daemon
3
+ def initialize(options)
4
+ @options = options
5
+
6
+ @options[:master] = @master = Agent::Master.new(options)
7
+ @options[:slave] = @slave = Agent::Slave.new(options)
8
+ end
9
+
10
+ def run!
11
+ while (true)
12
+ @master.poll!
13
+ @slave.poll!
14
+ sleep(10)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Daemonize module
4
+ #
5
+
6
+ require 'rubygems'
7
+ require 'yaml'
8
+ require 'optparse'
9
+
10
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..'))
11
+
12
+ require 'mysql_replication_helper'
13
+
14
+ # == Constants ==============================================================
15
+
16
+ CONFIG_FILE_LOCATIONS = [
17
+ "/etc/replication-helper.conf",
18
+ "/etc/replication-helper/config",
19
+ "~/.replication-helper/config"
20
+ ].collect { |p| File.expand_path(p) }.freeze
21
+
22
+ DEFAULT_CONFIG = {
23
+ }
24
+
25
+ # == Options ================================================================
26
+
27
+ op = OptionParser.new
28
+ options = { }
29
+ config = { }
30
+ config_file = nil
31
+
32
+ op.on("--master-socket=s") { |socket| options[:master_socket] = socket }
33
+ op.on("--master-data=s") { |dir| options[:master_data] = dir }
34
+ op.on("--master-user=s") { |name| options[:master_user] = name }
35
+
36
+ op.on("--slave-socket=s") { |socket| options[:slave_socket] = socket }
37
+ op.on("--slave-data=s") { |dir| options[:slave_data] = dir }
38
+ op.on("--slave-user=s") { |name| options[:slave_user] = name }
39
+
40
+ op.on("-c", "--config=s") { |path| config_file = nil }
41
+ op.on("-v", "--verbose") { options[:verbose] = true }
42
+ op.on("-h", "--help") { show_help }
43
+
44
+ args = op.parse(*ARGV)
45
+
46
+ # == Configuration ==========================================================
47
+
48
+ [ CONFIG_FILE_LOCATIONS, config_file ].flatten.each do |config_file|
49
+ if (File.exist?(config_file))
50
+ config = YAML.load(open(config_file))
51
+ break
52
+ end
53
+ end
54
+
55
+ config = DEFAULT_CONFIG.merge(config.inject({ }) { |h,(k,v)| h[k.to_sym] = v; h }).merge(options)
56
+
57
+
58
+ # == Main ===================================================================
59
+
60
+ MysqlReplicationHelper::Daemon.new(config).run!
@@ -0,0 +1,24 @@
1
+ module MysqlReplicationHelper
2
+ module ErrorHandler
3
+ def sql_to_recover_from(error)
4
+ case (error)
5
+ when /^Error 'Unknown database '([^\']+)'' on query/
6
+ [
7
+ "CREATE DATABASE `#{$1}`",
8
+ "START SLAVE"
9
+ ]
10
+ when /^Error 'There is no '([^\']+)'@'([^\']+)' registered' on query. Default database: '([^\']+)'./
11
+ [
12
+ "CREATE USER `#{$1}`@`#{$2}`",
13
+ "GRANT ALL PRIVILEGES ON `#{$3}`.* TO `#{$1}`@`#{$2}`",
14
+ "START SLAVE"
15
+ ]
16
+ when /^Error 'View '[^\']+' references invalid table\(s\) or column\(s\) or function\(s\) or definer\/invoker of view lack rights to use them' on query. Default database: '([^\']+)'. Query: 'CREATE .*? DEFINER=`([^\`]+)`@`([^\`]+)`/
17
+ [
18
+ "GRANT ALL PRIVILEGES ON `#{$1}`.* TO `#{$2}`@`#{$3}`",
19
+ "START SLAVE"
20
+ ]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,52 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{mysql-replication-helper}
8
+ s.version = "0.2.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = [%q{Scott Tadman}]
12
+ s.date = %q{2011-08-18}
13
+ s.email = %q{github@tadman.ca}
14
+ s.executables = [%q{replication-helper}]
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "bin/replication-helper",
23
+ "lib/mysql_replication_helper.rb",
24
+ "lib/mysql_replication_helper/agent.rb",
25
+ "lib/mysql_replication_helper/agent/master.rb",
26
+ "lib/mysql_replication_helper/agent/slave.rb",
27
+ "lib/mysql_replication_helper/daemon.rb",
28
+ "lib/mysql_replication_helper/daemon_launcher.rb",
29
+ "lib/mysql_replication_helper/error_handler.rb",
30
+ "mysql-replication-helper.gemspec",
31
+ "test/mysql_replication_helper/error_handler_test.rb",
32
+ "test/mysql_replication_helper_test.rb",
33
+ "test/test_helper.rb"
34
+ ]
35
+ s.homepage = %q{http://github.com/theworkinggroup/mysql-replication-helper}
36
+ s.require_paths = [%q{lib}]
37
+ s.rubygems_version = %q{1.8.8}
38
+ s.summary = %q{MySQL Replication Helper}
39
+
40
+ if s.respond_to? :specification_version then
41
+ s.specification_version = 3
42
+
43
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
44
+ s.add_runtime_dependency(%q<daemons>, [">= 0"])
45
+ else
46
+ s.add_dependency(%q<daemons>, [">= 0"])
47
+ end
48
+ else
49
+ s.add_dependency(%q<daemons>, [">= 0"])
50
+ end
51
+ end
52
+
@@ -0,0 +1,28 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../test_helper")
2
+
3
+ class TestHandler
4
+ include MysqlReplicationHelper::ErrorHandler
5
+ end
6
+
7
+ class MysqlReplicationHelper::ErrorHandlerTest < Test::Unit::TestCase
8
+ def test_responses
9
+ handler = TestHandler.new
10
+
11
+ [
12
+ [
13
+ "Error 'Unknown database 'example_db'' on query. Default database: 'example_db'.",
14
+ [ "CREATE DATABASE `example_db`", "START SLAVE" ]
15
+ ],
16
+ [
17
+ "Error 'There is no 'example_user'@'example_host' registered' on query. Default database: 'example_db'.",
18
+ [ "CREATE USER `example_user`@`example_host`", "GRANT ALL PRIVILEGES ON `example_db`.* TO `example_user`@`example_host`", "START SLAVE" ]
19
+ ],
20
+ [
21
+ "Error 'View 'example_db.example_table' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them' on query. Default database: 'example_db'. Query: 'CREATE ALGORITHM=UNDEFINED DEFINER=`example_user`@`example_host` SQL SECURITY DEFINER VIEW",
22
+ [ "GRANT ALL PRIVILEGES ON `example_db`.* TO `example_user`@`example_host`", "START SLAVE" ]
23
+ ]
24
+ ].each do |test_case|
25
+ assert_equal test_case[1], handler.sql_to_recover_from(test_case[0])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,6 @@
1
+ require 'test_helper'
2
+
3
+ class MysqlReplicationHelperTest < Test::Unit::TestCase
4
+ def test_module_init
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ require 'mysql_replication_helper'
7
+
8
+ class Test::Unit::TestCase
9
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mysql-replication-helper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Scott Tadman
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-08-18 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: daemons
16
+ requirement: &70228311548780 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70228311548780
25
+ description:
26
+ email: github@tadman.ca
27
+ executables:
28
+ - replication-helper
29
+ extensions: []
30
+ extra_rdoc_files:
31
+ - README.rdoc
32
+ files:
33
+ - .document
34
+ - README.rdoc
35
+ - Rakefile
36
+ - bin/replication-helper
37
+ - lib/mysql_replication_helper.rb
38
+ - lib/mysql_replication_helper/agent.rb
39
+ - lib/mysql_replication_helper/agent/master.rb
40
+ - lib/mysql_replication_helper/agent/slave.rb
41
+ - lib/mysql_replication_helper/daemon.rb
42
+ - lib/mysql_replication_helper/daemon_launcher.rb
43
+ - lib/mysql_replication_helper/error_handler.rb
44
+ - mysql-replication-helper.gemspec
45
+ - test/mysql_replication_helper/error_handler_test.rb
46
+ - test/mysql_replication_helper_test.rb
47
+ - test/test_helper.rb
48
+ homepage: http://github.com/theworkinggroup/mysql-replication-helper
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.8
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: MySQL Replication Helper
72
+ test_files: []