mysql-replication-helper 0.2.1

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.
@@ -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: []