ar_mysql_flexmaster 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,5 @@
1
1
  rvm:
2
2
  - 1.9.3
3
- - 2.0
4
3
  gemfile:
5
4
  - gemfiles/rails2.gemfile
6
5
  - gemfiles/rails3.gemfile
data/Gemfile CHANGED
@@ -2,6 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in ar_mysql_flexmaster.gemspec
4
4
  gemspec
5
- gem "debugger", :platform => :ruby_19
5
+ gem "debugger", "~>1.5.0"
6
6
  gem "appraisal"
7
-
@@ -12,12 +12,12 @@ Gem::Specification.new do |gem|
12
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
13
  gem.name = "ar_mysql_flexmaster"
14
14
  gem.require_paths = ["lib"]
15
- gem.version = "0.3.1"
15
+ gem.version = "0.4.0"
16
16
 
17
17
  gem.add_runtime_dependency("mysql2")
18
18
  gem.add_runtime_dependency("activerecord")
19
19
  gem.add_runtime_dependency("activesupport")
20
20
  gem.add_development_dependency("appraisal")
21
21
  gem.add_development_dependency("yaggy")
22
- gem.add_development_dependency("mysql_isolated_server")
22
+ gem.add_development_dependency("mysql_isolated_server", "~> 0.1.1")
23
23
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "debugger", :platform=>:ruby_19
5
+ gem "debugger", "~>1.5.0"
6
6
  gem "appraisal"
7
7
  gem "rails", "~> 2.3.15"
8
8
  gem "mysql2", "~> 0.2.0"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/ben/src/ar_mysql_flexmaster
3
3
  specs:
4
- ar_mysql_flexmaster (0.3.0)
4
+ ar_mysql_flexmaster (0.3.1)
5
5
  activerecord
6
6
  activesupport
7
7
  mysql2
@@ -9,38 +9,37 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actionmailer (2.3.15)
13
- actionpack (= 2.3.15)
14
- actionpack (2.3.15)
15
- activesupport (= 2.3.15)
16
- rack (~> 1.1.3)
17
- activerecord (2.3.15)
18
- activesupport (= 2.3.15)
19
- activeresource (2.3.15)
20
- activesupport (= 2.3.15)
21
- activesupport (2.3.15)
22
- appraisal (0.5.1)
12
+ actionmailer (2.3.18)
13
+ actionpack (= 2.3.18)
14
+ actionpack (2.3.18)
15
+ activesupport (= 2.3.18)
16
+ rack (~> 1.1.0)
17
+ activerecord (2.3.18)
18
+ activesupport (= 2.3.18)
19
+ activeresource (2.3.18)
20
+ activesupport (= 2.3.18)
21
+ activesupport (2.3.18)
22
+ appraisal (0.5.2)
23
23
  bundler
24
24
  rake
25
25
  columnize (0.3.6)
26
- debugger (1.2.3)
26
+ debugger (1.5.0)
27
27
  columnize (>= 0.3.1)
28
- debugger-linecache (~> 1.1.1)
29
- debugger-ruby_core_source (~> 1.1.5)
30
- debugger-linecache (1.1.2)
31
- debugger-ruby_core_source (>= 1.1.1)
32
- debugger-ruby_core_source (1.1.6)
28
+ debugger-linecache (~> 1.2.0)
29
+ debugger-ruby_core_source (~> 1.2.0)
30
+ debugger-linecache (1.2.0)
31
+ debugger-ruby_core_source (1.2.0)
33
32
  mysql2 (0.2.18)
34
- mysql_isolated_server (0.0.2)
35
- rack (1.1.5)
36
- rails (2.3.15)
37
- actionmailer (= 2.3.15)
38
- actionpack (= 2.3.15)
39
- activerecord (= 2.3.15)
40
- activeresource (= 2.3.15)
41
- activesupport (= 2.3.15)
33
+ mysql_isolated_server (0.1.1)
34
+ rack (1.1.6)
35
+ rails (2.3.18)
36
+ actionmailer (= 2.3.18)
37
+ actionpack (= 2.3.18)
38
+ activerecord (= 2.3.18)
39
+ activeresource (= 2.3.18)
40
+ activesupport (= 2.3.18)
42
41
  rake (>= 0.8.3)
43
- rake (10.0.3)
42
+ rake (10.0.4)
44
43
  yaggy (0.1.7)
45
44
 
46
45
  PLATFORMS
@@ -49,8 +48,8 @@ PLATFORMS
49
48
  DEPENDENCIES
50
49
  appraisal
51
50
  ar_mysql_flexmaster!
52
- debugger
51
+ debugger (~> 1.5.0)
53
52
  mysql2 (~> 0.2.0)
54
- mysql_isolated_server
53
+ mysql_isolated_server (~> 0.1.1)
55
54
  rails (~> 2.3.15)
56
55
  yaggy
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "debugger", :platform=>:ruby_19
5
+ gem "debugger", "~>1.5.0"
6
6
  gem "appraisal"
7
7
  gem "rails", "~> 3.2.0"
8
8
  gem "mysql2", "~> 0.3.0"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/ben/src/ar_mysql_flexmaster
3
3
  specs:
4
- ar_mysql_flexmaster (0.3.0)
4
+ ar_mysql_flexmaster (0.3.1)
5
5
  activerecord
6
6
  activesupport
7
7
  mysql2
@@ -9,96 +9,95 @@ PATH
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actionmailer (3.2.11)
13
- actionpack (= 3.2.11)
14
- mail (~> 2.4.4)
15
- actionpack (3.2.11)
16
- activemodel (= 3.2.11)
17
- activesupport (= 3.2.11)
12
+ actionmailer (3.2.13)
13
+ actionpack (= 3.2.13)
14
+ mail (~> 2.5.3)
15
+ actionpack (3.2.13)
16
+ activemodel (= 3.2.13)
17
+ activesupport (= 3.2.13)
18
18
  builder (~> 3.0.0)
19
19
  erubis (~> 2.7.0)
20
20
  journey (~> 1.0.4)
21
- rack (~> 1.4.0)
21
+ rack (~> 1.4.5)
22
22
  rack-cache (~> 1.2)
23
23
  rack-test (~> 0.6.1)
24
24
  sprockets (~> 2.2.1)
25
- activemodel (3.2.11)
26
- activesupport (= 3.2.11)
25
+ activemodel (3.2.13)
26
+ activesupport (= 3.2.13)
27
27
  builder (~> 3.0.0)
28
- activerecord (3.2.11)
29
- activemodel (= 3.2.11)
30
- activesupport (= 3.2.11)
28
+ activerecord (3.2.13)
29
+ activemodel (= 3.2.13)
30
+ activesupport (= 3.2.13)
31
31
  arel (~> 3.0.2)
32
32
  tzinfo (~> 0.3.29)
33
- activeresource (3.2.11)
34
- activemodel (= 3.2.11)
35
- activesupport (= 3.2.11)
36
- activesupport (3.2.11)
37
- i18n (~> 0.6)
33
+ activeresource (3.2.13)
34
+ activemodel (= 3.2.13)
35
+ activesupport (= 3.2.13)
36
+ activesupport (3.2.13)
37
+ i18n (= 0.6.1)
38
38
  multi_json (~> 1.0)
39
- appraisal (0.5.1)
39
+ appraisal (0.5.2)
40
40
  bundler
41
41
  rake
42
42
  arel (3.0.2)
43
43
  builder (3.0.4)
44
44
  columnize (0.3.6)
45
- debugger (1.2.3)
45
+ debugger (1.5.0)
46
46
  columnize (>= 0.3.1)
47
- debugger-linecache (~> 1.1.1)
48
- debugger-ruby_core_source (~> 1.1.5)
49
- debugger-linecache (1.1.2)
50
- debugger-ruby_core_source (>= 1.1.1)
51
- debugger-ruby_core_source (1.1.6)
47
+ debugger-linecache (~> 1.2.0)
48
+ debugger-ruby_core_source (~> 1.2.0)
49
+ debugger-linecache (1.2.0)
50
+ debugger-ruby_core_source (1.2.0)
52
51
  erubis (2.7.0)
53
- hike (1.2.1)
52
+ hike (1.2.2)
54
53
  i18n (0.6.1)
55
54
  journey (1.0.4)
56
- json (1.7.6)
57
- mail (2.4.4)
55
+ json (1.7.7)
56
+ mail (2.5.3)
58
57
  i18n (>= 0.4.0)
59
58
  mime-types (~> 1.16)
60
59
  treetop (~> 1.4.8)
61
- mime-types (1.19)
62
- multi_json (1.5.0)
60
+ mime-types (1.23)
61
+ multi_json (1.7.2)
63
62
  mysql2 (0.3.11)
64
- mysql_isolated_server (0.0.2)
63
+ mysql_isolated_server (0.1.1)
65
64
  polyglot (0.3.3)
66
- rack (1.4.4)
65
+ rack (1.4.5)
67
66
  rack-cache (1.2)
68
67
  rack (>= 0.4)
69
- rack-ssl (1.3.2)
68
+ rack-ssl (1.3.3)
70
69
  rack
71
70
  rack-test (0.6.2)
72
71
  rack (>= 1.0)
73
- rails (3.2.11)
74
- actionmailer (= 3.2.11)
75
- actionpack (= 3.2.11)
76
- activerecord (= 3.2.11)
77
- activeresource (= 3.2.11)
78
- activesupport (= 3.2.11)
72
+ rails (3.2.13)
73
+ actionmailer (= 3.2.13)
74
+ actionpack (= 3.2.13)
75
+ activerecord (= 3.2.13)
76
+ activeresource (= 3.2.13)
77
+ activesupport (= 3.2.13)
79
78
  bundler (~> 1.0)
80
- railties (= 3.2.11)
81
- railties (3.2.11)
82
- actionpack (= 3.2.11)
83
- activesupport (= 3.2.11)
79
+ railties (= 3.2.13)
80
+ railties (3.2.13)
81
+ actionpack (= 3.2.13)
82
+ activesupport (= 3.2.13)
84
83
  rack-ssl (~> 1.3.2)
85
84
  rake (>= 0.8.7)
86
85
  rdoc (~> 3.4)
87
86
  thor (>= 0.14.6, < 2.0)
88
- rake (10.0.3)
89
- rdoc (3.12)
87
+ rake (10.0.4)
88
+ rdoc (3.12.2)
90
89
  json (~> 1.4)
91
90
  sprockets (2.2.2)
92
91
  hike (~> 1.2)
93
92
  multi_json (~> 1.0)
94
93
  rack (~> 1.0)
95
94
  tilt (~> 1.1, != 1.3.0)
96
- thor (0.16.0)
97
- tilt (1.3.3)
95
+ thor (0.18.1)
96
+ tilt (1.3.7)
98
97
  treetop (1.4.12)
99
98
  polyglot
100
99
  polyglot (>= 0.3.1)
101
- tzinfo (0.3.35)
100
+ tzinfo (0.3.37)
102
101
  yaggy (0.1.7)
103
102
 
104
103
  PLATFORMS
@@ -107,8 +106,8 @@ PLATFORMS
107
106
  DEPENDENCIES
108
107
  appraisal
109
108
  ar_mysql_flexmaster!
110
- debugger
109
+ debugger (~> 1.5.0)
111
110
  mysql2 (~> 0.3.0)
112
- mysql_isolated_server
111
+ mysql_isolated_server (~> 0.1.1)
113
112
  rails (~> 3.2.0)
114
113
  yaggy
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "debugger", :platform=>:ruby_19
5
+ gem "debugger", "~>1.5.0"
6
6
  gem "appraisal"
7
7
  gem "rails", "~> 3.0.0"
8
8
  gem "mysql2", "~> 0.2.0"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: /Users/ben/src/ar_mysql_flexmaster
3
3
  specs:
4
- ar_mysql_flexmaster (0.3.0)
4
+ ar_mysql_flexmaster (0.3.1)
5
5
  activerecord
6
6
  activesupport
7
7
  mysql2
@@ -10,12 +10,12 @@ GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
12
  abstract (1.0.0)
13
- actionmailer (3.0.19)
14
- actionpack (= 3.0.19)
13
+ actionmailer (3.0.20)
14
+ actionpack (= 3.0.20)
15
15
  mail (~> 2.2.19)
16
- actionpack (3.0.19)
17
- activemodel (= 3.0.19)
18
- activesupport (= 3.0.19)
16
+ actionpack (3.0.20)
17
+ activemodel (= 3.0.20)
18
+ activesupport (= 3.0.20)
19
19
  builder (~> 2.1.2)
20
20
  erubis (~> 2.6.6)
21
21
  i18n (~> 0.5.0)
@@ -23,72 +23,71 @@ GEM
23
23
  rack-mount (~> 0.6.14)
24
24
  rack-test (~> 0.5.7)
25
25
  tzinfo (~> 0.3.23)
26
- activemodel (3.0.19)
27
- activesupport (= 3.0.19)
26
+ activemodel (3.0.20)
27
+ activesupport (= 3.0.20)
28
28
  builder (~> 2.1.2)
29
29
  i18n (~> 0.5.0)
30
- activerecord (3.0.19)
31
- activemodel (= 3.0.19)
32
- activesupport (= 3.0.19)
30
+ activerecord (3.0.20)
31
+ activemodel (= 3.0.20)
32
+ activesupport (= 3.0.20)
33
33
  arel (~> 2.0.10)
34
34
  tzinfo (~> 0.3.23)
35
- activeresource (3.0.19)
36
- activemodel (= 3.0.19)
37
- activesupport (= 3.0.19)
38
- activesupport (3.0.19)
39
- appraisal (0.5.1)
35
+ activeresource (3.0.20)
36
+ activemodel (= 3.0.20)
37
+ activesupport (= 3.0.20)
38
+ activesupport (3.0.20)
39
+ appraisal (0.5.2)
40
40
  bundler
41
41
  rake
42
42
  arel (2.0.10)
43
43
  builder (2.1.2)
44
44
  columnize (0.3.6)
45
- debugger (1.2.3)
45
+ debugger (1.5.0)
46
46
  columnize (>= 0.3.1)
47
- debugger-linecache (~> 1.1.1)
48
- debugger-ruby_core_source (~> 1.1.5)
49
- debugger-linecache (1.1.2)
50
- debugger-ruby_core_source (>= 1.1.1)
51
- debugger-ruby_core_source (1.1.6)
47
+ debugger-linecache (~> 1.2.0)
48
+ debugger-ruby_core_source (~> 1.2.0)
49
+ debugger-linecache (1.2.0)
50
+ debugger-ruby_core_source (1.2.0)
52
51
  erubis (2.6.6)
53
52
  abstract (>= 1.0.0)
54
53
  i18n (0.5.0)
55
- json (1.7.6)
54
+ json (1.7.7)
56
55
  mail (2.2.19)
57
56
  activesupport (>= 2.3.6)
58
57
  i18n (>= 0.4.0)
59
58
  mime-types (~> 1.16)
60
59
  treetop (~> 1.4.8)
61
- mime-types (1.19)
60
+ mime-types (1.23)
62
61
  mysql2 (0.2.18)
63
- mysql_isolated_server (0.0.2)
62
+ mysql_isolated_server (0.1.1)
64
63
  polyglot (0.3.3)
65
- rack (1.2.7)
64
+ rack (1.2.8)
66
65
  rack-mount (0.6.14)
67
66
  rack (>= 1.0.0)
68
67
  rack-test (0.5.7)
69
68
  rack (>= 1.0)
70
- rails (3.0.19)
71
- actionmailer (= 3.0.19)
72
- actionpack (= 3.0.19)
73
- activerecord (= 3.0.19)
74
- activeresource (= 3.0.19)
75
- activesupport (= 3.0.19)
69
+ rails (3.0.20)
70
+ actionmailer (= 3.0.20)
71
+ actionpack (= 3.0.20)
72
+ activerecord (= 3.0.20)
73
+ activeresource (= 3.0.20)
74
+ activesupport (= 3.0.20)
76
75
  bundler (~> 1.0)
77
- railties (= 3.0.19)
78
- railties (3.0.19)
79
- actionpack (= 3.0.19)
80
- activesupport (= 3.0.19)
76
+ railties (= 3.0.20)
77
+ railties (3.0.20)
78
+ actionpack (= 3.0.20)
79
+ activesupport (= 3.0.20)
81
80
  rake (>= 0.8.7)
82
81
  rdoc (~> 3.4)
83
82
  thor (~> 0.14.4)
84
- rake (10.0.3)
85
- rdoc (3.12)
83
+ rake (10.0.4)
84
+ rdoc (3.12.2)
86
85
  json (~> 1.4)
87
86
  thor (0.14.6)
88
87
  treetop (1.4.12)
89
88
  polyglot
90
89
  polyglot (>= 0.3.1)
91
- tzinfo (0.3.35)
90
+ tzinfo (0.3.37)
92
91
  yaggy (0.1.7)
93
92
 
94
93
  PLATFORMS
@@ -97,8 +96,8 @@ PLATFORMS
97
96
  DEPENDENCIES
98
97
  appraisal
99
98
  ar_mysql_flexmaster!
100
- debugger
99
+ debugger (~> 1.5.0)
101
100
  mysql2 (~> 0.2.0)
102
- mysql_isolated_server
101
+ mysql_isolated_server (~> 0.1.1)
103
102
  rails (~> 3.0.0)
104
103
  yaggy
@@ -35,36 +35,37 @@ module ActiveRecord
35
35
  def initialize(logger, config)
36
36
  @select_counter = 0
37
37
  @config = config
38
- @is_master = !config[:slave]
38
+ @rw = config[:slave] ? :read : :write
39
39
  @tx_hold_timeout = @config[:tx_hold_timeout] || DEFAULT_TX_HOLD_TIMEOUT
40
40
  @connection_timeout = @config[:connection_timeout] || DEFAULT_CONNECT_TIMEOUT
41
41
 
42
- connection = find_correct_host
42
+ connection = find_correct_host(@rw)
43
43
 
44
44
  raise_no_server_available! unless connection
45
45
  super(connection, logger, [], config)
46
46
  end
47
47
 
48
48
  def begin_db_transaction
49
- if !cx_correct? && open_transactions == 0
50
- refind_correct_host!
49
+ if !in_transaction?
50
+ with_lost_cx_guard { hard_verify }
51
51
  end
52
52
  super
53
53
  end
54
54
 
55
55
  def execute(sql, name = nil)
56
- if open_transactions == 0 && sql =~ /^(INSERT|UPDATE|DELETE|ALTER|CHANGE)/ && !cx_correct?
57
- refind_correct_host!
56
+ if in_transaction?
57
+ super # no way to rescue any lost cx or wrong-host errors at this point.
58
58
  else
59
- @select_counter += 1
60
- if (@select_counter % CHECK_EVERY_N_SELECTS == 0) && !cx_correct?
61
- # on select statements, check every 10 times to see if we need to switch masters,
62
- # but don't sleep, and if existing connection isn't correct, go ahead anyway.
63
- cx = find_correct_host
64
- @connection = cx if cx
59
+ with_lost_cx_guard do
60
+ if has_side_effects?(sql)
61
+ hard_verify
62
+ else
63
+ soft_verify
64
+ end
65
+
66
+ super
65
67
  end
66
68
  end
67
- super
68
69
  end
69
70
 
70
71
  def current_host
@@ -77,9 +78,77 @@ module ActiveRecord
77
78
 
78
79
  private
79
80
 
81
+ def in_transaction?
82
+ open_transactions > 0
83
+ end
84
+
85
+ # never try to carry on if inside a transaction
86
+ # otherwise try to detect when the master/slave has crashed and retry stuff.
87
+ def with_lost_cx_guard
88
+ retried = false
89
+
90
+ begin
91
+ yield
92
+ rescue Mysql2::Error, ActiveRecord::StatementInvalid => e
93
+ if retryable_error?(e) && !retried
94
+ retried = true
95
+ @connection = nil
96
+ retry
97
+ else
98
+ raise e
99
+ end
100
+ end
101
+ end
102
+
103
+ AR_MESSAGES = [ /^Mysql2::Error: MySQL server has gone away/,
104
+ /^Mysql2::Error: Can't connect to MySQL server/ ]
105
+ def retryable_error?(e)
106
+ case e
107
+ when Mysql2::Error
108
+ # 2006 is gone-away, 61 is can't-connect (for reconnection: true connections)
109
+ [2006, 61].include?(e.errno)
110
+ when ActiveRecord::StatementInvalid
111
+ AR_MESSAGES.any? { |m| e.message.match(m) }
112
+ end
113
+ end
114
+
115
+ # when either doing BEGIN or INSERT/UPDATE/DELETE etc, ensure a correct connection
116
+ # and crash if wrong
117
+ def hard_verify
118
+ if !@connection || !cx_correct?
119
+ refind_correct_host!
120
+ end
121
+ end
122
+
123
+ # on select statements, check every 10 statements to see if we need to switch hosts,
124
+ # but don't crash if the cx is wrong, and don't sleep trying to find a correct one.
125
+ def soft_verify
126
+ if !@connection
127
+ @connection = find_correct_host(@rw)
128
+ else
129
+ @select_counter += 1
130
+ return unless @select_counter % CHECK_EVERY_N_SELECTS == 0
131
+
132
+ if !cx_correct?
133
+ cx = find_correct_host(@rw)
134
+ @connection = cx if cx
135
+ end
136
+ end
137
+ if @rw == :write && !@connection
138
+ # desperation mode: we've been asked for the master, but it's just not available.
139
+ # we'll go ahead and return a connection to the slave, understanding that it'll never work
140
+ # for writes.
141
+ @connection = find_correct_host(:read)
142
+ end
143
+ end
144
+
145
+ def has_side_effects?(sql)
146
+ sql =~ /^\s*(INSERT|UPDATE|DELETE|ALTER|CHANGE|REPLACE)/i
147
+ end
148
+
80
149
  def connect
81
- @connection = find_correct_host
82
- raise NoActiveMasterException unless @connection
150
+ @connection = find_correct_host(@rw)
151
+ raise_no_server_available! unless @connection
83
152
  end
84
153
 
85
154
  def raise_no_server_available!
@@ -106,11 +175,9 @@ module ActiveRecord
106
175
  tries = @tx_hold_timeout.to_f / sleep_interval
107
176
 
108
177
  tries.to_i.times do
109
- cx = find_correct_host
110
- if cx
111
- @connection = cx
112
- return
113
- end
178
+ @connection = find_correct_host(@rw)
179
+ return if @connection
180
+
114
181
  sleep(sleep_interval)
115
182
  end
116
183
  raise_no_server_available!
@@ -124,7 +191,7 @@ module ActiveRecord
124
191
  end
125
192
  end
126
193
 
127
- def find_correct_host
194
+ def find_correct_host(rw)
128
195
  cxs = hosts_and_ports.map do |host, port|
129
196
  initialize_connection(host, port)
130
197
  end.compact
@@ -132,7 +199,8 @@ module ActiveRecord
132
199
  correct_cxs = cxs.select { |cx| cx_correct?(cx) }
133
200
 
134
201
  chosen_cx = nil
135
- if @is_master
202
+ case rw
203
+ when :write
136
204
  # for master connections, we make damn sure that we have just one master
137
205
  if correct_cxs.size == 1
138
206
  chosen_cx = correct_cxs.first
@@ -146,8 +214,8 @@ module ActiveRecord
146
214
 
147
215
  chosen_cx = nil
148
216
  end
149
- else
150
- # for slave connections, we just return a random RO candidate or the master if none are available
217
+ when :read
218
+ # for slave connections (or master-gone-away scenarios), we just return a random RO candidate or the master if none are available
151
219
  if correct_cxs.empty?
152
220
  chosen_cx = cxs.first
153
221
  else
@@ -178,7 +246,7 @@ module ActiveRecord
178
246
  def cx_correct?(cx = @connection)
179
247
  res = cx.query("SELECT @@read_only as ro").first
180
248
 
181
- if @is_master
249
+ if @rw == :write
182
250
  res.first == 0
183
251
  else
184
252
  res.first == 1
@@ -7,20 +7,27 @@ require 'debugger'
7
7
 
8
8
  File.open(File.dirname(File.expand_path(__FILE__)) + "/database.yml", "w+") do |f|
9
9
  f.write <<-EOL
10
- test:
10
+ common: &common
11
11
  adapter: mysql_flexmaster
12
12
  username: flex
13
- hosts: ["127.0.0.1:#{$mysql_master.port}", "127.0.0.1:#{$mysql_slave.port}"]
14
- password:
13
+ hosts: ["127.0.0.1:#{$mysql_master.port}", "127.0.0.1:#{$mysql_slave.port}", "127.0.0.1:#{$mysql_slave_2.port}"]
15
14
  database: flexmaster_test
16
15
 
16
+ test:
17
+ <<: *common
18
+
17
19
  test_slave:
18
- adapter: mysql_flexmaster
19
- username: flex
20
+ <<: *common
21
+ slave: true
22
+
23
+ reconnect:
24
+ <<: *common
25
+ reconnect: true
26
+
27
+ reconnect_slave:
28
+ <<: *common
29
+ reconnect: true
20
30
  slave: true
21
- hosts: ["127.0.0.1:#{$mysql_master.port}", "127.0.0.1:#{$mysql_slave.port}", "127.0.0.1:#{$mysql_slave_2.port}"]
22
- password:
23
- database: flexmaster_test
24
31
  EOL
25
32
  end
26
33
 
@@ -32,7 +39,17 @@ end
32
39
 
33
40
  class UserSlave < ActiveRecord::Base
34
41
  establish_connection(:test_slave)
35
- set_table_name "users"
42
+ self.table_name = "users"
43
+ end
44
+
45
+ class Reconnect < ActiveRecord::Base
46
+ establish_connection(:reconnect)
47
+ self.table_name = "users"
48
+ end
49
+
50
+ class ReconnectSlave < ActiveRecord::Base
51
+ establish_connection(:reconnect_slave)
52
+ self.table_name = "users"
36
53
  end
37
54
 
38
55
  # $mysql_master and $mysql_slave are separate references to the master and slave that we
@@ -62,7 +79,7 @@ class TestArFlexmaster < Test::Unit::TestCase
62
79
  end
63
80
 
64
81
  def test_should_select_the_master_on_boot
65
- assert main_connection_is_original_master?
82
+ assert_equal $mysql_master, master_connection
66
83
  end
67
84
 
68
85
  def test_should_hold_txs_until_timeout_then_abort
@@ -85,7 +102,7 @@ class TestArFlexmaster < Test::Unit::TestCase
85
102
  $mysql_slave.set_rw(true)
86
103
  end
87
104
  User.create(:name => "foo")
88
- assert !main_connection_is_original_master?
105
+ assert_equal $mysql_slave, master_connection
89
106
  assert User.first(:conditions => {:name => "foo"})
90
107
  end
91
108
 
@@ -97,7 +114,9 @@ class TestArFlexmaster < Test::Unit::TestCase
97
114
  $mysql_slave.set_rw(true)
98
115
  end
99
116
  User.update_all(:name => "bar")
100
- assert !main_connection_is_original_master?
117
+
118
+ assert_equal $mysql_slave, master_connection
119
+
101
120
  assert_equal "bar", User.first.name
102
121
  end
103
122
 
@@ -114,11 +133,11 @@ class TestArFlexmaster < Test::Unit::TestCase
114
133
  ActiveRecord::Base.connection
115
134
  $mysql_master.set_rw(false)
116
135
  $mysql_slave.set_rw(true)
117
- assert main_connection_is_original_master?
136
+ assert_equal $mysql_master, master_connection
118
137
  100.times do
119
138
  u = User.first
120
139
  end
121
- assert !main_connection_is_original_master?
140
+ assert_equal $mysql_slave, master_connection
122
141
  end
123
142
 
124
143
  # there's a small window in which the old master is read-only but the new slave hasn't come online yet.
@@ -127,7 +146,7 @@ class TestArFlexmaster < Test::Unit::TestCase
127
146
  ActiveRecord::Base.connection
128
147
  $mysql_master.set_rw(false)
129
148
  $mysql_slave.set_rw(false)
130
- assert main_connection_is_original_master?
149
+ assert_equal $mysql_master, master_connection
131
150
  100.times do
132
151
  u = User.first
133
152
  end
@@ -149,16 +168,17 @@ class TestArFlexmaster < Test::Unit::TestCase
149
168
  assert_equal $mysql_master.port, cx.current_port
150
169
  end
151
170
 
152
- def test_should_flip_the_slave_after_it_becomes_master
171
+ def test_should_move_off_the_slave_after_it_becomes_master
153
172
  UserSlave.first
154
173
  User.create!
155
174
  $mysql_master.set_rw(false)
156
175
  $mysql_slave.set_rw(true)
176
+
157
177
  20.times do
158
178
  UserSlave.connection.execute("select 1")
159
179
  end
160
- connected_port = port_for_class(UserSlave)
161
- assert [$mysql_slave_2.port, $mysql_master.port].include?(connected_port)
180
+
181
+ assert [$mysql_master, $mysql_slave_2].include?(slave_connection)
162
182
  end
163
183
 
164
184
  def test_xxx_non_responsive_master
@@ -167,33 +187,70 @@ class TestArFlexmaster < Test::Unit::TestCase
167
187
  start_time = Time.now.to_i
168
188
  User.connection.reconnect!
169
189
  assert Time.now.to_i - start_time >= 5, "only took #{Time.now.to_i - start_time} to timeout"
190
+ ensure
170
191
  ActiveRecord::Base.configurations["test"]["hosts"].pop
171
192
  end
172
193
 
173
- def test_yyy_shooting_the_master_in_the_head
194
+ def test_shooting_the_master_in_the_head
174
195
  User.create!
196
+ UserSlave.first
197
+
198
+ $mysql_master.down!
175
199
 
176
- $mysql_master.kill!
177
- $mysql_master = nil
200
+ # protected against 'gone away' errors?
201
+ assert User.first
178
202
 
179
- sleep 1
203
+ # this statement should
204
+ # put us into a bad state -- our @connection should be nil, as we'll fail to get a master connection
205
+ assert_raises(ActiveRecord::ConnectionAdapters::MysqlFlexmasterAdapter::NoServerAvailableException) do
206
+ User.create!
207
+ end
208
+
209
+ # now test that the next time through we ask for a read connection, we'll grudgingly give back the slave
210
+ User.first
211
+
212
+ assert [$mysql_slave, $mysql_slave_2].include?(master_connection)
213
+
214
+ # now a dba or someone comes along and flips the read-only bit on the slave
180
215
  $mysql_slave.set_rw(true)
181
- User.connection.reconnect!
182
216
  User.create!
183
217
  UserSlave.first
184
- assert !main_connection_is_original_master?
218
+
219
+ assert_equal $mysql_slave, master_connection
220
+ ensure
221
+ $mysql_master.up!
185
222
  end
186
223
 
187
- # test that when nothing else is available we can fall back to the master in a slave role
188
- # note that by the time this test runs, the 'yyy' test has already killed the master
189
- def test_zzz_shooting_the_other_slave_in_the_head
224
+ def test_losing_the_server_with_reconnect_on
225
+ Reconnect.create!
226
+ ReconnectSlave.first
227
+
228
+ $mysql_master.down!
229
+
230
+ assert Reconnect.first
231
+ assert ReconnectSlave.first
232
+
233
+ assert_raises(ActiveRecord::ConnectionAdapters::MysqlFlexmasterAdapter::NoServerAvailableException) do
234
+ Reconnect.create!
235
+ end
236
+
190
237
  $mysql_slave.set_rw(true)
238
+ Reconnect.create!
239
+ ReconnectSlave.first
240
+ ensure
241
+ $mysql_master.up!
242
+ end
191
243
 
192
- $mysql_slave_2.kill!
193
- $mysql_slave_2 = nil
244
+ # test that when nothing else is available we can fall back to the master in a slave role
245
+ def test_master_can_act_as_slave
246
+ $mysql_slave.down!
247
+ $mysql_slave_2.down!
194
248
 
195
- UserSlave.connection.reconnect!
196
- assert port_for_class(UserSlave) == $mysql_slave.port
249
+ UserSlave.first
250
+ assert_equal $mysql_master, slave_connection
251
+ ensure
252
+ $mysql_slave.up!
253
+ $mysql_slave_2.up!
197
254
  end
198
255
 
199
256
 
@@ -207,4 +264,17 @@ class TestArFlexmaster < Test::Unit::TestCase
207
264
  port = port_for_class(ActiveRecord::Base)
208
265
  port == $original_master_port
209
266
  end
267
+
268
+ def connection_for_class(klass)
269
+ port = port_for_class(klass)
270
+ [$mysql_master, $mysql_slave, $mysql_slave_2].find { |cx| cx.port == port }
271
+ end
272
+
273
+ def master_connection
274
+ connection_for_class(User)
275
+ end
276
+
277
+ def slave_connection
278
+ connection_for_class(UserSlave)
279
+ end
210
280
  end
@@ -0,0 +1,47 @@
1
+ Here's a logic flow I've been thinking about for dealing with master outages. In plain english, I'm generally
2
+ thinking:
3
+
4
+ - inside a transaction, we will never attempt to recover from a master failure or attempt to ensure proper connections
5
+ - outside a transaction, we'll try to be liberal about recovering from bad states and limp along in a read-only mode
6
+ as best we can.
7
+
8
+ guard:
9
+ begin
10
+ yield
11
+ rescue 'server gone away', "can't connect to server"
12
+ retry-once
13
+ end
14
+
15
+ hard_verify(INSERTs) == verify correct connection, try for 5 seconds, crash and unset connection if you can't
16
+
17
+ soft_verify(SELECTs) == every N requests, try to verify that your connection is the right one.
18
+
19
+ If you're running a SELECT targeted at the master, and no master is online, it's acceptable to use either
20
+ the old master (your existing connection) or a slave connection for the purposes of reads (until the next
21
+ verify)
22
+
23
+ This will allow us to limp along in read-only mode for a short time until a new master can be promoted.
24
+ The downside of this approach is that in a pathological case we could be making decisions based on stale
25
+ or incorrect data. I feel that the odds of this are long and are probably made up for by having a
26
+ halfway decent read-only mode.
27
+
28
+ ```
29
+ switch incoming_sql:
30
+ BEGIN:
31
+ - in transaction?
32
+ -> do not verify, do not guard.
33
+
34
+ - guard { hard-verify }
35
+ - execute BEGIN statement (without guard). hard to reconnect here because of side effects
36
+
37
+ INSERT/UPDATE/DELETE:
38
+ - in transaction?
39
+ -> do not verify, do not guard
40
+ -> guard { hard-verify, execute }
41
+
42
+ SELECT:
43
+ - in transaction?
44
+ -> no verify, no guard
45
+ -> guard { soft-verify / execute }
46
+ ```
47
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ar_mysql_flexmaster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-19 00:00:00.000000000 Z
12
+ date: 2013-04-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mysql2
@@ -96,17 +96,17 @@ dependencies:
96
96
  requirement: !ruby/object:Gem::Requirement
97
97
  none: false
98
98
  requirements:
99
- - - ! '>='
99
+ - - ~>
100
100
  - !ruby/object:Gem::Version
101
- version: '0'
101
+ version: 0.1.1
102
102
  type: :development
103
103
  prerelease: false
104
104
  version_requirements: !ruby/object:Gem::Requirement
105
105
  none: false
106
106
  requirements:
107
- - - ! '>='
107
+ - - ~>
108
108
  - !ruby/object:Gem::Version
109
- version: '0'
109
+ version: 0.1.1
110
110
  description: ar_mysql_flexmaster allows configuring N mysql servers in database.yml
111
111
  and auto-selects which is a master at runtime
112
112
  email:
@@ -141,6 +141,7 @@ files:
141
141
  - test/integration/there_and_back_again_test.rb
142
142
  - test/integration/with_queries_to_be_killed_test.rb
143
143
  - test/integration/wrong_setup_test.rb
144
+ - unplanned_failovers.md
144
145
  homepage: http://github.com/osheroff/ar_mysql_flexmaster
145
146
  licenses: []
146
147
  post_install_message:
@@ -161,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
161
162
  version: '0'
162
163
  requirements: []
163
164
  rubyforge_project:
164
- rubygems_version: 1.8.24
165
+ rubygems_version: 1.8.25
165
166
  signing_key:
166
167
  specification_version: 3
167
168
  summary: select a master at runtime from a list