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
@@ -5,6 +5,7 @@ describe Makara::Pool do
5
5
  let(:proxy){ FakeProxy.new({:makara => pool_config.merge(:connections => [])}) }
6
6
  let(:pool){ Makara::Pool.new('test', proxy) }
7
7
  let(:pool_config){ {:blacklist_duration => 5} }
8
+ let(:master_pool){ Makara::Pool.new('master', proxy) }
8
9
 
9
10
  it 'should wrap connections with a ConnectionWrapper as theyre added to the pool' do
10
11
  expect(pool.connections).to be_empty
@@ -157,4 +158,27 @@ describe Makara::Pool do
157
158
 
158
159
  end
159
160
 
161
+ it 'should error out while blacklisted in transaction' do
162
+ wrapper_a = master_pool.add(pool_config){ FakeConnection.new(open_transactions: 1) }
163
+ master_pool.add(pool_config){ FakeConnection.new }
164
+ expect {
165
+ master_pool.provide do |connection|
166
+ if connection == wrapper_a
167
+ raise Makara::Errors::BlacklistConnection.new(wrapper_a, StandardError.new('failure'))
168
+ end
169
+ end
170
+ }.to raise_error(Makara::Errors::BlacklistedWhileInTransaction)
171
+ end
172
+
173
+ it 'skips blacklisted connections in master pool when not in transaction' do
174
+ wrapper_a = master_pool.add(pool_config){ FakeConnection.new(open_transactions: 0) }
175
+ master_pool.add(pool_config){ FakeConnection.new }
176
+ master_pool.provide do |connection|
177
+ if connection == wrapper_a
178
+ raise Makara::Errors::BlacklistConnection.new(wrapper_a, StandardError.new('failure'))
179
+ end
180
+ end
181
+ 10.times{ master_pool.provide{|connection| expect(connection).not_to eq(wrapper_a) } }
182
+ end
183
+
160
184
  end
@@ -41,20 +41,57 @@ describe Makara::Proxy do
41
41
  expect(proxy.irespondtothis).to eq('hello!')
42
42
  end
43
43
 
44
- it 'should use master if manually forced' do
45
- proxy = klass.new(config(1, 2))
44
+ describe '#stick_to_master' do
45
+ let(:proxy) { klass.new(config(1, 2)) }
46
46
 
47
- expect(proxy.master_for?('select * from users')).to eq(false)
47
+ it 'should use master if manually forced' do
48
+ expect(proxy.master_for?('select * from users')).to eq(false)
48
49
 
49
- proxy.stick_to_master!
50
+ proxy.stick_to_master!
50
51
 
51
- expect(proxy.master_for?('select * from users')).to eq(true)
52
- end
52
+ expect(proxy.master_for?('select * from users')).to eq(true)
53
+ end
54
+
55
+ it 'should persist stickiness by default' do
56
+ now = Time.now
57
+ proxy.stick_to_master!
58
+
59
+ next_context = Makara::Context.next
60
+ expect(next_context[proxy.id]).to be >= (now + 5).to_f
61
+
62
+ proxy = klass.new(config(1, 2))
63
+ expect(proxy.master_for?('select * from users')).to eq(true)
64
+ end
53
65
 
66
+ it 'optionally skips stickiness persistence, so it applies only to the current request' do
67
+ now = Time.now
68
+ proxy.stick_to_master!(false)
69
+
70
+ expect(proxy.master_for?('select * from users')).to eq(true)
71
+ next_context = Makara::Context.next
72
+ expect(next_context).to be_nil # Nothing to persist, so context is empty
73
+
74
+ proxy = klass.new(config(1, 2))
75
+ expect(proxy.master_for?('select * from users')).to eq(false)
76
+ end
77
+
78
+ it 'supports a float master_ttl for stickiness duration' do
79
+ now = Time.now
80
+ config = config(1, 2).dup
81
+ config[:makara][:master_ttl] = 0.5
82
+ proxy = klass.new(config)
83
+
84
+ proxy.stick_to_master!
85
+
86
+ next_context = Makara::Context.next
87
+ expect(next_context[proxy.id]).to be >= (now + 0.5).to_f
88
+ expect(next_context[proxy.id]).to be < (now + 1).to_f
89
+ end
90
+ end
54
91
 
55
- context '#appropriate_pool' do
56
92
 
57
- let(:proxy){ klass.new(config(1,1)) }
93
+ describe '#appropriate_pool' do
94
+ let(:proxy) { klass.new(config(1,1)) }
58
95
 
59
96
  it 'should be sticky by default' do
60
97
  expect(proxy.sticky).to eq(true)
@@ -104,41 +141,23 @@ describe Makara::Proxy do
104
141
  expect(proxy.master_for?('select * from users')).to eq(false)
105
142
  end
106
143
 
107
- # if the context changes we should still use master until the previous context is no longer relevant
108
- it 'should release master if the context changes and enough time passes' do
144
+ it "should not release master if it was stuck in the same request (no context changes yet)" do
109
145
  expect(proxy.master_for?('insert into users values (a,b,c)')).to eq(true)
110
146
  expect(proxy.master_for?('select * from users')).to eq(true)
111
147
 
112
- change_context
113
-
114
148
  Timecop.travel Time.now + 10 do
115
- expect(proxy.master_for?('select * from users')).to eq(false)
149
+ # master_ttl has passed but we are still in the same request, so current context
150
+ # is still relevant
151
+ expect(proxy.master_for?('select * from users')).to eq(true)
116
152
  end
117
153
  end
118
154
 
119
- it 'should not release master if the previous context is still relevant' do
155
+ it 'should release master if all stuck connections are released' do
120
156
  expect(proxy.master_for?('insert into users values (a,b,c)')).to eq(true)
121
157
  expect(proxy.master_for?('select * from users')).to eq(true)
122
158
 
123
- roll_context
124
-
125
- proxy.master_for?('select * from users')
126
- expect(proxy.master_for?('select * from users')).to eq(true)
127
-
128
- Timecop.travel Time.now + 10 do
129
- # cache is expired but context has not changed
130
- expect(proxy.master_for?('select * from users')).to eq(true)
131
-
132
- roll_context
159
+ Makara::Context.release_all
133
160
 
134
- expect(proxy.master_for?('select * from users')).to eq(false)
135
- end
136
- end
137
-
138
- it 'should release master if context changes enough' do
139
- expect(proxy.master_for?('insert into users values (a,b,c)')).to eq(true)
140
- roll_context
141
- roll_context
142
161
  expect(proxy.master_for?('select * from users')).to eq(false)
143
162
  end
144
163
 
@@ -188,9 +207,5 @@ describe Makara::Proxy do
188
207
  proxy.slave_pool.connections.each{|con| expect(con._makara_blacklisted?).to eq(false) }
189
208
  proxy.master_pool.connections.each{|con| expect(con._makara_blacklisted?).to eq(false) }
190
209
  end
191
-
192
210
  end
193
-
194
-
195
-
196
211
  end
@@ -1,7 +1,9 @@
1
+ require 'uri'
1
2
  require 'active_record'
2
3
  require 'makara'
3
4
  require 'timecop'
4
5
  require 'yaml'
6
+ require 'rack'
5
7
 
6
8
  begin
7
9
  require 'byebug'
@@ -24,23 +26,17 @@ RSpec.configure do |config|
24
26
  require "#{File.dirname(__FILE__)}/support/pool_extensions"
25
27
  require "#{File.dirname(__FILE__)}/support/mock_objects"
26
28
  require "#{File.dirname(__FILE__)}/support/deep_dup"
29
+ require "#{File.dirname(__FILE__)}/support/user"
27
30
 
28
31
  config.include SpecHelpers
29
32
 
30
33
  config.before :each do
31
- Makara::Cache.store = :memory
32
34
  change_context
33
35
  allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:should_shuffle?){ false }
36
+ RSpec::Mocks.space.proxy_for(ActiveRecord::Base).reset # make sure not stubbed in some way
34
37
  end
35
38
 
36
39
  def change_context
37
- Makara::Context.set_previous nil
38
- Makara::Context.set_current nil
40
+ Makara::Context.set_current({})
39
41
  end
40
-
41
- def roll_context
42
- Makara::Context.set_previous Makara::Context.get_current
43
- Makara::Context.set_current Makara::Context.generate
44
- end
45
-
46
42
  end
@@ -0,0 +1,219 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::Strategies::ShardAware do
4
+
5
+ def with_shard(shard_id)
6
+ begin
7
+ Thread.current['makara_shard_id'] = shard_id
8
+ yield
9
+ ensure
10
+ Thread.current['makara_shard_id'] = nil
11
+ end
12
+ end
13
+
14
+ describe "failover strategy with shard awareness," do
15
+ let(:proxy){ FakeProxy.new({:makara => pool_config.merge(makara_config).merge(:connections => [])}) }
16
+ let(:pool){ Makara::Pool.new('master', proxy) }
17
+ let(:pool_config){ { blacklist_duration: 5} }
18
+ let(:makara_config) { {
19
+ master_strategy: 'failover',
20
+ master_shard_aware: true,
21
+ master_default_shard: 'shard2'
22
+ } }
23
+ let(:strategy) { pool.strategy }
24
+
25
+ it 'should use the strategy' do
26
+ expect(pool.strategy).to be_instance_of(Makara::Strategies::ShardAware)
27
+ end
28
+
29
+ it 'should take the top weight for a given shard' do
30
+ wrapper_a = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'a') }
31
+ wrapper_b = pool.add(pool_config.merge(shard_id: 'shard1', weight: 2)){ FakeConnection.new(something: 'b') }
32
+ wrapper_c = pool.add(pool_config.merge(weight: 2, shard_id: 'shard2')){ FakeConnection.new(something: 'c') }
33
+
34
+ # default shard
35
+ expect(strategy.current.something).to eql('c')
36
+ expect(strategy.next.something).to eql('c')
37
+ expect(strategy.next.something).to eql('c')
38
+
39
+ # shard1
40
+ with_shard('shard1') do
41
+ expect(strategy.current.something).to eql('b')
42
+ expect(strategy.next.something).to eql('b')
43
+ expect(strategy.next.something).to eql('b')
44
+ end
45
+
46
+ # shard2
47
+ with_shard('shard2') do
48
+ expect(strategy.current.something).to eql('c')
49
+ expect(strategy.next.something).to eql('c')
50
+ expect(strategy.next.something).to eql('c')
51
+ end
52
+ end
53
+
54
+ it 'should take given order within shard if no weights' do
55
+ wrapper_a = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'a') }
56
+ wrapper_b = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'b') }
57
+ wrapper_c = pool.add(pool_config.merge(shard_id: 'shard2')){ FakeConnection.new(something: 'c') }
58
+
59
+ # default shard
60
+ expect(strategy.current.something).to eql('c')
61
+ expect(strategy.next.something).to eql('c')
62
+ expect(strategy.next.something).to eql('c')
63
+
64
+ # shard1
65
+ with_shard('shard1') do
66
+ expect(strategy.current.something).to eql('a')
67
+ expect(strategy.next.something).to eql('a')
68
+ end
69
+
70
+ # shard2
71
+ with_shard('shard2') do
72
+ expect(strategy.current.something).to eql('c')
73
+ expect(strategy.next.something).to eql('c')
74
+ end
75
+ end
76
+
77
+ it 'should handle failover to next one within shard' do
78
+ wrapper_a = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'a') }
79
+ wrapper_b = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'b') }
80
+ wrapper_c = pool.add(pool_config.merge(shard_id: 'shard2')){ FakeConnection.new(something: 'c') }
81
+
82
+ # default shard
83
+ expect(strategy.current.something).to eql('c')
84
+ expect(strategy.next.something).to eql('c')
85
+ expect(strategy.next.something).to eql('c')
86
+
87
+ # skips a for shard1
88
+ with_shard('shard1') do
89
+ pool.provide do |connection|
90
+ if connection == wrapper_a
91
+ raise Makara::Errors::BlacklistConnection.new(wrapper_a, StandardError.new('failure'))
92
+ end
93
+ end
94
+ expect(strategy.current.something).to eql('b')
95
+ expect(strategy.next.something).to eql('b')
96
+ end
97
+
98
+ # shard2
99
+ with_shard('shard2') do
100
+ expect(strategy.current.something).to eql('c')
101
+ expect(strategy.next.something).to eql('c')
102
+ end
103
+ end
104
+ it 'raises error for invalid shard' do
105
+ with_shard('shard3') do
106
+ expect{strategy.current.something }.to raise_error(Makara::Errors::InvalidShard)
107
+ end
108
+ end
109
+ end
110
+
111
+ describe "round_robin strategy with shard awareness," do
112
+ let(:proxy){ FakeProxy.new({:makara => pool_config.merge(makara_config).merge(:connections => [])}) }
113
+ let(:pool){ Makara::Pool.new('master', proxy) }
114
+ let(:pool_config){ { blacklist_duration: 5} }
115
+ let(:makara_config) { {
116
+ master_strategy: 'round_robin',
117
+ master_shard_aware: true,
118
+ master_default_shard: 'shard2'
119
+ } }
120
+ let(:strategy) { pool.strategy }
121
+
122
+ it 'should use the strategy' do
123
+ expect(pool.strategy).to be_instance_of(Makara::Strategies::ShardAware)
124
+ end
125
+
126
+ it 'should loop through with weights within shard' do
127
+ wrapper_a = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'a') }
128
+ wrapper_b = pool.add(pool_config.merge(shard_id: 'shard1', weight: 2)){ FakeConnection.new(something: 'b') }
129
+ wrapper_c = pool.add(pool_config.merge(weight: 2, shard_id: 'shard2')){ FakeConnection.new(something: 'c') }
130
+
131
+ # default shard
132
+ expect(strategy.current.something).to eql('c')
133
+ expect(strategy.next.something).to eql('c')
134
+ expect(strategy.next.something).to eql('c')
135
+
136
+ # shard1
137
+ with_shard('shard1') do
138
+ expect(strategy.current.something).to eql('a')
139
+ expect(strategy.next.something).to eql('b')
140
+ expect(strategy.next.something).to eql('b')
141
+ expect(strategy.next.something).to eql('a')
142
+ end
143
+
144
+ # shard2
145
+ with_shard('shard2') do
146
+ expect(strategy.current.something).to eql('c')
147
+ expect(strategy.next.something).to eql('c')
148
+ expect(strategy.next.something).to eql('c')
149
+ end
150
+ end
151
+
152
+ it 'should handle failover to next one within shard' do
153
+ wrapper_a = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'a') }
154
+ wrapper_b = pool.add(pool_config.merge(shard_id: 'shard1')){ FakeConnection.new(something: 'b') }
155
+ wrapper_c = pool.add(pool_config.merge(shard_id: 'shard2')){ FakeConnection.new(something: 'c') }
156
+
157
+ # default shard
158
+ expect(strategy.current.something).to eql('c')
159
+ expect(strategy.next.something).to eql('c')
160
+ expect(strategy.next.something).to eql('c')
161
+
162
+ # skips a for shard1
163
+ with_shard('shard1') do
164
+ pool.provide do |connection|
165
+ if connection == wrapper_a
166
+ raise Makara::Errors::BlacklistConnection.new(wrapper_a, StandardError.new('failure'))
167
+ end
168
+ end
169
+ expect(strategy.current.something).to eql('b')
170
+ expect(strategy.next.something).to eql('b')
171
+ expect(strategy.next.something).to eql('b')
172
+ end
173
+
174
+ # shard2
175
+ with_shard('shard2') do
176
+ expect(strategy.current.something).to eql('c')
177
+ expect(strategy.next.something).to eql('c')
178
+ expect(strategy.next.something).to eql('c')
179
+ end
180
+ end
181
+ it 'raises error for invalid shard' do
182
+ with_shard('shard3') do
183
+ expect{strategy.current.something }.to raise_error(Makara::Errors::InvalidShard)
184
+ end
185
+ end
186
+ end
187
+
188
+ describe "uses the configured failover strategy when shard_aware set to false," do
189
+ let(:proxy){ FakeProxy.new({:makara => pool_config.merge(makara_config).merge(:connections => [])}) }
190
+ let(:pool){ Makara::Pool.new('master', proxy) }
191
+ let(:pool_config){ { blacklist_duration: 5} }
192
+ let(:makara_config) { {
193
+ master_strategy: 'failover',
194
+ master_shard_aware: false,
195
+ master_default_shard: 'shard2'
196
+ } }
197
+ let(:strategy) { pool.strategy }
198
+
199
+ it 'should use the failover strategy' do
200
+ expect(pool.strategy).to be_instance_of(Makara::Strategies::PriorityFailover)
201
+ end
202
+ end
203
+
204
+ describe "uses the configured roundrobin strategy when shard_aware set to false," do
205
+ let(:proxy){ FakeProxy.new({:makara => pool_config.merge(makara_config).merge(:connections => [])}) }
206
+ let(:pool){ Makara::Pool.new('master', proxy) }
207
+ let(:pool_config){ { blacklist_duration: 5} }
208
+ let(:makara_config) { {
209
+ master_strategy: 'round_robin',
210
+ master_shard_aware: false,
211
+ master_default_shard: 'shard2'
212
+ } }
213
+ let(:strategy) { pool.strategy }
214
+
215
+ it 'should use the failover strategy' do
216
+ expect(pool.strategy).to be_instance_of(Makara::Strategies::RoundRobin)
217
+ end
218
+ end
219
+ end
@@ -15,9 +15,13 @@ module SpecHelpers
15
15
  slaves.times{ connections << {:role => 'slave'} }
16
16
  {
17
17
  :makara => {
18
- :blacklist_duration => 30,
18
+ # Defaults:
19
+ # :master_ttl => 5,
20
+ # :blacklist_duration => 30,
21
+ # :sticky => true
22
+ :id => 'mock_mysql',
19
23
  :connections => connections
20
24
  }
21
25
  }
22
26
  end
23
- end
27
+ end
@@ -11,13 +11,17 @@ class FakeConnection < Struct.new(:config)
11
11
  end
12
12
 
13
13
  def query(content)
14
- []
14
+ config[:name]
15
15
  end
16
16
 
17
17
  def active?
18
18
  true
19
19
  end
20
20
 
21
+ def open_transactions
22
+ (config || {}).fetch(:open_transactions, 0)
23
+ end
24
+
21
25
  def disconnect!
22
26
  true
23
27
  end