ar_mysql_flexmaster 0.3.1 → 0.4.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.
@@ -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