makara 0.3.8 → 0.5.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-publish-public.yml +36 -0
  3. data/.travis.yml +71 -9
  4. data/CHANGELOG.md +84 -25
  5. data/Gemfile +4 -3
  6. data/README.md +37 -34
  7. data/gemfiles/ar-head.gemfile +9 -0
  8. data/gemfiles/ar30.gemfile +7 -1
  9. data/gemfiles/ar31.gemfile +8 -1
  10. data/gemfiles/ar32.gemfile +8 -1
  11. data/gemfiles/ar40.gemfile +10 -1
  12. data/gemfiles/ar41.gemfile +10 -1
  13. data/gemfiles/ar42.gemfile +10 -1
  14. data/gemfiles/ar50.gemfile +11 -2
  15. data/gemfiles/ar51.gemfile +11 -2
  16. data/gemfiles/ar52.gemfile +24 -0
  17. data/gemfiles/ar60.gemfile +24 -0
  18. data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +109 -3
  19. data/lib/active_record/connection_adapters/makara_postgis_adapter.rb +41 -0
  20. data/lib/makara.rb +15 -4
  21. data/lib/makara/cache.rb +4 -40
  22. data/lib/makara/config_parser.rb +14 -3
  23. data/lib/makara/connection_wrapper.rb +26 -2
  24. data/lib/makara/context.rb +108 -38
  25. data/lib/makara/cookie.rb +52 -0
  26. data/lib/makara/error_handler.rb +2 -2
  27. data/lib/makara/errors/blacklisted_while_in_transaction.rb +14 -0
  28. data/lib/makara/errors/invalid_shard.rb +16 -0
  29. data/lib/makara/logging/logger.rb +1 -1
  30. data/lib/makara/middleware.rb +12 -75
  31. data/lib/makara/pool.rb +53 -40
  32. data/lib/makara/proxy.rb +52 -30
  33. data/lib/makara/railtie.rb +0 -6
  34. data/lib/makara/strategies/round_robin.rb +6 -0
  35. data/lib/makara/strategies/shard_aware.rb +47 -0
  36. data/lib/makara/version.rb +2 -2
  37. data/makara.gemspec +5 -1
  38. data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +10 -5
  39. data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +17 -2
  40. data/spec/active_record/connection_adapters/makara_postgis_adapter_spec.rb +155 -0
  41. data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +76 -3
  42. data/spec/cache_spec.rb +2 -52
  43. data/spec/config_parser_spec.rb +27 -13
  44. data/spec/connection_wrapper_spec.rb +5 -2
  45. data/spec/context_spec.rb +163 -100
  46. data/spec/cookie_spec.rb +72 -0
  47. data/spec/middleware_spec.rb +26 -55
  48. data/spec/pool_spec.rb +24 -0
  49. data/spec/proxy_spec.rb +51 -36
  50. data/spec/spec_helper.rb +5 -9
  51. data/spec/strategies/shard_aware_spec.rb +219 -0
  52. data/spec/support/helpers.rb +6 -2
  53. data/spec/support/mock_objects.rb +5 -1
  54. data/spec/support/mysql2_database.yml +1 -0
  55. data/spec/support/mysql2_database_with_custom_errors.yml +5 -0
  56. data/spec/support/postgis_database.yml +15 -0
  57. data/spec/support/postgis_schema.rb +11 -0
  58. data/spec/support/postgresql_database.yml +2 -0
  59. data/spec/support/proxy_extensions.rb +1 -1
  60. data/spec/support/schema.rb +5 -5
  61. data/spec/support/user.rb +5 -0
  62. metadata +28 -9
  63. data/lib/makara/cache/memory_store.rb +0 -28
  64. data/lib/makara/cache/noop_store.rb +0 -15
@@ -14,8 +14,11 @@ describe Makara::ConnectionWrapper do
14
14
  end
15
15
 
16
16
  it 'should invoke hijacked methods on the proxy when invoked directly' do
17
- expect(proxy).to receive(:execute).with('test').once
18
- connection.execute("test")
17
+ expect(proxy).to receive(:execute).with('test').once do |&block|
18
+ expect(block.call).to eq('Hello')
19
+ end
20
+
21
+ connection.execute('test') { 'Hello' }
19
22
  end
20
23
 
21
24
  it 'should have a default weight of 1' do
@@ -1,146 +1,209 @@
1
1
  require 'spec_helper'
2
+ require 'rack'
3
+ require 'time'
2
4
 
3
5
  describe Makara::Context do
6
+ let(:now) { Time.parse('2018-02-11 11:10:40 +0000') }
7
+ let(:context_data) { { "mysql" => now.to_f + 5, "redis" => now.to_f + 5 } }
4
8
 
5
- describe 'get_current' do
6
- it 'does not share context acorss threads' do
7
- uniq_curret_contexts = []
8
- threads = []
9
-
10
- 1.upto(3).map do |i|
11
- threads << Thread.new do
12
- current = Makara::Context.get_current
13
- expect(current).to_not be_nil
14
- expect(uniq_curret_contexts).to_not include(current)
15
- uniq_curret_contexts << current
16
-
17
- sleep(0.2)
18
- end
19
- sleep(0.1)
20
- end
9
+ before do
10
+ Timecop.freeze(now)
11
+ end
12
+
13
+ after do
14
+ Timecop.return
15
+ end
16
+
17
+ it 'does not share stickiness state across threads' do
18
+ contexts = {}
19
+ threads = []
20
+
21
+ [1, -1].each_with_index do |f, i|
22
+ threads << Thread.new do
23
+ context_data = { "mysql" => now.to_f + f*5 }
24
+ Makara::Context.set_current(context_data)
21
25
 
22
- threads.map(&:join)
23
- expect(uniq_curret_contexts.uniq.count).to eq(3)
26
+ contexts["context_#{i}"] = Makara::Context.stuck?('mysql')
27
+
28
+ sleep(0.2)
29
+ end
30
+ sleep(0.1)
24
31
  end
32
+
33
+ threads.map(&:join)
34
+ expect(contexts).to eq({ 'context_0' => true, 'context_1' => false })
25
35
  end
26
36
 
27
37
  describe 'set_current' do
38
+ it 'sets stickiness information from given hash' do
39
+ Makara::Context.set_current(context_data)
40
+
41
+ expect(Makara::Context.stuck?('mysql')).to be_truthy
42
+ expect(Makara::Context.stuck?('redis')).to be_truthy
43
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
44
+ end
45
+ end
28
46
 
29
- it 'does not share context acorss threads' do
30
- uniq_curret_contexts = []
31
- threads = []
47
+ describe 'stick' do
48
+ before do
49
+ Makara::Context.set_current(context_data)
50
+ end
51
+
52
+ it 'sticks a proxy to master for the current request' do
53
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
32
54
 
33
- 1.upto(3).map do |i|
34
- threads << Thread.new do
55
+ Makara::Context.stick('mariadb', 10)
35
56
 
36
- current = Makara::Context.set_current("val#{i}")
37
- expect(current).to_not be_nil
38
- expect(current).to eq("val#{i}")
57
+ expect(Makara::Context.stuck?('mariadb')).to be_truthy
58
+ Timecop.travel(Time.now + 20)
59
+ # The ttl kicks off when the context is committed
60
+ next_context = Makara::Context.next
61
+ expect(next_context['mariadb']).to be >= now.to_f + 30 # 10 ttl + 20 seconds that have passed
39
62
 
40
- sleep(0.2)
63
+ # It expires after going to the next request
64
+ Timecop.travel(Time.now + 20)
65
+ Makara::Context.next
66
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
67
+ end
41
68
 
42
- current = Makara::Context.get_current
43
- expect(current).to_not be_nil
44
- expect(current).to eq("val#{i}")
69
+ it "doesn't overwrite previously stuck proxies with current-request-only stickiness" do
70
+ expect(Makara::Context.stuck?('mysql')).to be_truthy
45
71
 
46
- uniq_curret_contexts << current
47
- end
48
- sleep(0.1)
49
- end
72
+ # ttl=0 to avoid persisting mysql for the next request
73
+ Makara::Context.stick('mysql', 0)
50
74
 
51
- threads.map(&:join)
52
- expect(uniq_curret_contexts.uniq.count).to eq(3)
75
+ Makara::Context.next
76
+ # mysql proxy is still stuck in the next context
77
+ expect(Makara::Context.stuck?('mysql')).to be_truthy
53
78
  end
54
- end
55
79
 
56
- describe 'get_previous' do
57
- it 'does not share context acorss threads' do
58
- uniq_curret_contexts = []
59
- threads = []
60
-
61
- 1.upto(3).map do |i|
62
- threads << Thread.new do
63
- current = Makara::Context.get_previous
64
- expect(current).to_not be_nil
65
- expect(uniq_curret_contexts).to_not include(current)
66
- uniq_curret_contexts << current
67
-
68
- sleep(0.2)
69
- end
70
- sleep(0.1)
71
- end
80
+ it 'uses always the max ttl given' do
81
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
82
+
83
+ Makara::Context.stick('mariadb', 10)
84
+ expect(Makara::Context.stuck?('mariadb')).to be_truthy
85
+
86
+ Makara::Context.stick('mariadb', 5)
87
+
88
+ next_context = Makara::Context.next
89
+ expect(next_context['mariadb']).to eq((now + 10).to_f)
90
+ end
91
+
92
+ it 'supports floats as ttl' do
93
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
72
94
 
73
- threads.map(&:join)
74
- expect(uniq_curret_contexts.uniq.count).to eq(3)
95
+ Makara::Context.stick('mariadb', 0.5)
96
+
97
+ next_context = Makara::Context.next
98
+ expect(next_context['mariadb']).to eq((now + 0.5).to_f)
75
99
  end
76
100
  end
77
101
 
78
- describe 'set_previous' do
79
- it 'does not share context acorss threads' do
80
- uniq_curret_contexts = []
81
- threads = []
102
+ describe 'next' do
103
+ before do
104
+ Makara::Context.set_current(context_data)
105
+ end
82
106
 
83
- 1.upto(3).map do |i|
84
- threads << Thread.new do
107
+ it 'returns nil if there is nothing new to stick' do
108
+ expect(Makara::Context.next).to be_nil
109
+ end
85
110
 
86
- current = Makara::Context.set_previous("val#{i}")
87
- expect(current).to_not be_nil
88
- expect(current).to eq("val#{i}")
111
+ it "doesn't store staged proxies with 0 stickiness duration" do
112
+ Makara::Context.stick('mariadb', 0)
89
113
 
90
- sleep(0.2)
114
+ expect(Makara::Context.next).to be_nil
115
+ end
91
116
 
92
- current = Makara::Context.get_previous
93
- expect(current).to_not be_nil
94
- expect(current).to eq("val#{i}")
117
+ it 'returns hash with updated stickiness' do
118
+ Makara::Context.stick('mariadb', 10)
95
119
 
96
- uniq_curret_contexts << current
97
- end
98
- sleep(0.1)
99
- end
120
+ next_context = Makara::Context.next
121
+ expect(next_context['mysql']).to eq((now + 5).to_f)
122
+ expect(next_context['redis']).to eq((now + 5).to_f)
123
+ expect(next_context['mariadb']).to eq((now + 10).to_f)
124
+ end
125
+
126
+ it "doesn't update previously stored proxies if the update will cause a sooner expiration" do
127
+ Makara::Context.stick('mariadb', 10)
128
+ Makara::Context.stick('mysql', 2.5)
129
+
130
+ next_context = Makara::Context.next
131
+ expect(next_context['mysql']).to eq((now + 5).to_f)
132
+ expect(next_context['mariadb']).to eq((now + 10).to_f)
133
+
134
+ Makara::Context.set_current(context_data)
135
+ Makara::Context.stick('mysql', 2.5)
136
+
137
+ expect(Makara::Context.next).to be_nil
138
+ end
139
+
140
+ it 'clears expired entries for proxies that are no longer stuck' do
141
+ Timecop.travel(now + 10)
100
142
 
101
- threads.map(&:join)
102
- expect(uniq_curret_contexts.uniq.count).to eq(3)
143
+ expect(Makara::Context.next).to eq({})
103
144
  end
104
145
 
105
- it 'clears config sticky cache' do
106
- Makara::Cache.store = :memory
146
+ it 'sets expiration time with ttl based on the invokation time' do
147
+ Makara::Context.stick('mariadb', 10)
148
+ request_ends_at = Time.now + 20
149
+ Timecop.travel(request_ends_at)
107
150
 
108
- Makara::Context.set_previous('a')
109
- Makara::Context.stick('a', 1, 10)
110
- expect(Makara::Context.previously_stuck?(1)).to be_truthy
151
+ next_context = Makara::Context.next
111
152
 
112
- Makara::Context.set_previous('b')
113
- expect(Makara::Context.previously_stuck?(1)).to be_falsey
153
+ # The previous stuck proxies would have expired
154
+ expect(next_context['mysql']).to be_nil
155
+ expect(next_context['redis']).to be_nil
156
+ # But the proxy stuck in that request would expire in ttl seconds from now
157
+ expect(next_context['mariadb']).to be >= (request_ends_at + 10).to_f
114
158
  end
115
159
  end
116
160
 
117
- describe 'stick' do
118
- it 'sticks a config to master for subsequent requests' do
119
- Makara::Cache.store = :memory
161
+ describe 'release' do
162
+ before do
163
+ Makara::Context.set_current(context_data)
164
+ end
165
+
166
+ it 'clears stickiness for the given proxy' do
167
+ expect(Makara::Context.stuck?('mysql')).to be_truthy
168
+
169
+ Makara::Context.release('mysql')
170
+
171
+ expect(Makara::Context.stuck?('mysql')).to be_falsey
172
+
173
+ next_context = Makara::Context.next
174
+ expect(next_context.key?('mysql')).to be_falsey
175
+ expect(next_context['redis']).to eq((now + 5).to_f)
176
+ end
177
+
178
+ it 'does nothing if the proxy given was not stuck' do
179
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
120
180
 
121
- expect(Makara::Context.stuck?('context', 1)).to be_falsey
181
+ Makara::Context.release('mariadb')
122
182
 
123
- Makara::Context.stick('context', 1, 10)
124
- expect(Makara::Context.stuck?('context', 1)).to be_truthy
125
- expect(Makara::Context.stuck?('context', 2)).to be_falsey
183
+ expect(Makara::Context.stuck?('mariadb')).to be_falsey
184
+ expect(Makara::Context.next).to be_nil
126
185
  end
127
186
  end
128
187
 
129
- describe 'previously_stuck?' do
130
- it 'checks whether a config was stuck to master in the previous context' do
131
- Makara::Cache.store = :memory
132
- Makara::Context.set_previous 'previous'
188
+ describe 'release_all' do
189
+ it 'clears stickiness for all stuck proxies' do
190
+ Makara::Context.set_current(context_data)
191
+ expect(Makara::Context.stuck?('mysql')).to be_truthy
192
+ expect(Makara::Context.stuck?('redis')).to be_truthy
193
+
194
+ Makara::Context.release_all
133
195
 
134
- # Emulate sticking the previous web request to master.
135
- Makara::Context.stick 'previous', 1, 10
196
+ expect(Makara::Context.stuck?('mysql')).to be_falsey
197
+ expect(Makara::Context.stuck?('redis')).to be_falsey
198
+ expect(Makara::Context.next).to eq({})
199
+ end
200
+
201
+ it 'does nothing if there were no stuck proxies' do
202
+ Makara::Context.set_current({})
136
203
 
137
- # Emulate handling the subsequent web request with a previous context
138
- # cookie that is stuck to master.
139
- expect(Makara::Context.previously_stuck?(1)).to be_truthy
204
+ Makara::Context.release_all
140
205
 
141
- # Other configs should not be stuck to master, though.
142
- expect(Makara::Context.previously_stuck?(2)).to be_falsey
206
+ expect(Makara::Context.next).to be_nil
143
207
  end
144
208
  end
145
209
  end
146
-
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+ require 'rack'
3
+ require 'time'
4
+
5
+ describe Makara::Cookie do
6
+ let(:now) { Time.parse('2018-02-11 11:10:40 +0000') }
7
+ let(:cookie_key) { Makara::Cookie::IDENTIFIER }
8
+
9
+ before do
10
+ Timecop.freeze(now)
11
+ end
12
+
13
+ after do
14
+ Timecop.return
15
+ end
16
+
17
+ describe 'fetch' do
18
+ let(:cookie_string) { "mysql:#{now.to_f + 5}|redis:#{now.to_f + 5}" }
19
+ let(:request) { Rack::Request.new('HTTP_COOKIE' => "#{cookie_key}=#{cookie_string}") }
20
+
21
+ it 'parses stickiness context from cookie string' do
22
+ context_data = Makara::Cookie.fetch(request)
23
+
24
+ expect(context_data['mysql']).to eq(now.to_f + 5)
25
+ expect(context_data['redis']).to eq(now.to_f + 5)
26
+ expect(context_data.key?('mariadb')).to be_falsey
27
+ end
28
+
29
+ it 'returns empty context data when there is no cookie' do
30
+ context_data = Makara::Cookie.fetch(Rack::Request.new('HTTP_COOKIE' => "another_cookie=1"))
31
+
32
+ expect(context_data).to eq({})
33
+ end
34
+
35
+ it 'returns empty context data when the cookie contents are invalid' do
36
+ context_data = Makara::Cookie.fetch(Rack::Request.new('HTTP_COOKIE' => "#{cookie_key}=1"))
37
+
38
+ expect(context_data).to eq({})
39
+ end
40
+ end
41
+
42
+ describe 'store' do
43
+ let(:headers) { {} }
44
+ let(:context_data) { { "mysql" => now.to_f + 5, "redis" => now.to_f + 5 } }
45
+
46
+ it 'does not set a cookie if there is no next context' do
47
+ Makara::Cookie.store(nil, headers)
48
+
49
+ expect(headers).to eq({})
50
+ end
51
+
52
+ it 'sets the context cookie with updated stickiness and enough expiration time' do
53
+ Makara::Cookie.store(context_data, headers)
54
+
55
+ expect(headers['Set-Cookie']).to include("#{cookie_key}=mysql%3A#{(now + 5).to_f}%7Credis%3A#{(now + 5).to_f};")
56
+ expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).httpdate}; HttpOnly")
57
+ end
58
+
59
+ it 'expires the cookie if the next context is empty' do
60
+ Makara::Cookie.store({}, headers)
61
+
62
+ expect(headers['Set-Cookie']).to eq("#{cookie_key}=; path=/; max-age=0; expires=#{Time.now.httpdate}; HttpOnly")
63
+ end
64
+
65
+ it 'allows custom cookie options to be provided' do
66
+ Makara::Cookie.store(context_data, headers, { :secure => true })
67
+
68
+ expect(headers['Set-Cookie']).to include("#{cookie_key}=mysql%3A#{(now + 5).to_f}%7Credis%3A#{(now + 5).to_f};")
69
+ expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).httpdate}; secure; HttpOnly")
70
+ end
71
+ end
72
+ end
@@ -1,11 +1,12 @@
1
1
  require 'spec_helper'
2
+ require 'time'
2
3
 
3
4
  describe Makara::Middleware do
4
-
5
+ let(:now) { Time.parse('2018-02-11 11:10:40 +0000') }
5
6
  let(:app){
6
7
  lambda{|env|
7
- proxy.query(env[:query] || 'select * from users')
8
- [200, {}, ["#{Makara::Context.get_current}-#{Makara::Context.get_previous}"]]
8
+ response = proxy.query(env[:query] || 'select * from users')
9
+ [200, {}, response]
9
10
  }
10
11
  }
11
12
 
@@ -13,72 +14,42 @@ describe Makara::Middleware do
13
14
  let(:proxy){ FakeProxy.new(config(1,2)) }
14
15
  let(:middleware){ described_class.new(app, :secure => true) }
15
16
 
16
- let(:key){ Makara::Middleware::IDENTIFIER }
17
-
18
- it 'should set the context before the request' do
19
- Makara::Context.set_previous 'old'
20
- Makara::Context.set_current 'old'
21
-
22
- response = middleware.call(env)
23
- current, prev = context_from(response)
17
+ let(:key){ Makara::Cookie::IDENTIFIER }
24
18
 
25
- expect(current).not_to eq('old')
26
- expect(prev).not_to eq('old')
27
-
28
- expect(current).to eq(Makara::Context.get_current)
29
- expect(prev).to eq(Makara::Context.get_previous)
19
+ before do
20
+ @hijacked_methods = FakeProxy.hijack_methods
21
+ FakeProxy.hijack_method :query
22
+ Timecop.freeze(now)
30
23
  end
31
24
 
32
- it 'should use the cookie-provided context if present' do
33
- env['HTTP_COOKIE'] = "#{key}=abcdefg--200; path=/; max-age=5"
25
+ after do
26
+ Timecop.return
27
+ FakeProxy.hijack_methods = []
28
+ FakeProxy.hijack_method(*@hijacked_methods)
29
+ end
34
30
 
35
- response = middleware.call(env)
36
- current, prev = context_from(response)
31
+ it 'should init the context and not be stuck by default' do
32
+ _, headers, body = middleware.call(env)
37
33
 
38
- expect(prev).to eq('abcdefg')
39
- expect(current).to eq(Makara::Context.get_current)
40
- expect(current).not_to eq('abcdefg')
34
+ expect(headers).to eq({})
35
+ expect(body).to eq('slave/1')
41
36
  end
42
37
 
43
- it 'should use the param-provided context if present' do
44
- env['QUERY_STRING'] = "dog=true&#{key}=abcdefg&cat=false"
38
+ it 'should use the cookie-provided context if present' do
39
+ env['HTTP_COOKIE'] = "#{key}=mock_mysql%3A#{(now + 3).to_f}; path=/; max-age=5"
45
40
 
46
- response = middleware.call(env)
47
- current, prev = context_from(response)
41
+ _, headers, body = middleware.call(env)
48
42
 
49
- expect(prev).to eq('abcdefg')
50
- expect(current).to eq(Makara::Context.get_current)
51
- expect(current).not_to eq('abcdefg')
43
+ expect(headers).to eq({})
44
+ expect(body).to eq('master/1')
52
45
  end
53
46
 
54
47
  it 'should set the cookie if master is used' do
55
48
  env[:query] = 'update users set name = "phil"'
56
49
 
57
- status, headers, body = middleware.call(env)
50
+ _, headers, body = middleware.call(env)
58
51
 
59
- expect(headers['Set-Cookie']).to eq("#{key}=#{Makara::Context.get_current}--200; path=/; max-age=5; secure; HttpOnly")
52
+ expect(headers['Set-Cookie']).to eq("#{key}=mock_mysql%3A#{(now + 5).to_f}; path=/; max-age=10; expires=#{(Time.now + 10).httpdate}; secure; HttpOnly")
53
+ expect(body).to eq('master/1')
60
54
  end
61
-
62
- it 'should preserve the same context if the previous request was a redirect' do
63
- env['HTTP_COOKIE'] = "#{key}=abcdefg--301; path=/; max-age=5"
64
-
65
- response = middleware.call(env)
66
- curr, prev = context_from(response)
67
-
68
- expect(curr).to eq('abcdefg')
69
- expect(prev).to eq('abcdefg')
70
-
71
- env['HTTP_COOKIE'] = response[1]['Set-Cookie']
72
-
73
- response = middleware.call(env)
74
- curr2, prev2 = context_from(response)
75
-
76
- expect(prev2).to eq('abcdefg')
77
- expect(curr2).to eq(Makara::Context.get_current)
78
- end
79
-
80
- def context_from(response)
81
- response[2][0].split('-')
82
- end
83
-
84
55
  end