makara 0.3.10 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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).gmtime.rfc2822}; 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.gmtime.rfc2822}; 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).gmtime.rfc2822}; 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).gmtime.rfc2822}; 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