makara 0.3.5

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 (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +28 -0
  7. data/CHANGELOG.md +27 -0
  8. data/Gemfile +18 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +278 -0
  11. data/Rakefile +9 -0
  12. data/gemfiles/ar30.gemfile +30 -0
  13. data/gemfiles/ar31.gemfile +29 -0
  14. data/gemfiles/ar32.gemfile +29 -0
  15. data/gemfiles/ar40.gemfile +15 -0
  16. data/gemfiles/ar41.gemfile +15 -0
  17. data/gemfiles/ar42.gemfile +15 -0
  18. data/lib/active_record/connection_adapters/jdbcmysql_makara_adapter.rb +25 -0
  19. data/lib/active_record/connection_adapters/jdbcpostgresql_makara_adapter.rb +25 -0
  20. data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +209 -0
  21. data/lib/active_record/connection_adapters/makara_jdbcmysql_adapter.rb +25 -0
  22. data/lib/active_record/connection_adapters/makara_jdbcpostgresql_adapter.rb +25 -0
  23. data/lib/active_record/connection_adapters/makara_mysql2_adapter.rb +44 -0
  24. data/lib/active_record/connection_adapters/makara_postgresql_adapter.rb +44 -0
  25. data/lib/active_record/connection_adapters/mysql2_makara_adapter.rb +44 -0
  26. data/lib/active_record/connection_adapters/postgresql_makara_adapter.rb +44 -0
  27. data/lib/makara.rb +25 -0
  28. data/lib/makara/cache.rb +53 -0
  29. data/lib/makara/cache/memory_store.rb +28 -0
  30. data/lib/makara/cache/noop_store.rb +15 -0
  31. data/lib/makara/config_parser.rb +200 -0
  32. data/lib/makara/connection_wrapper.rb +170 -0
  33. data/lib/makara/context.rb +46 -0
  34. data/lib/makara/error_handler.rb +39 -0
  35. data/lib/makara/errors/all_connections_blacklisted.rb +13 -0
  36. data/lib/makara/errors/blacklist_connection.rb +14 -0
  37. data/lib/makara/errors/no_connections_available.rb +14 -0
  38. data/lib/makara/logging/logger.rb +23 -0
  39. data/lib/makara/logging/subscriber.rb +38 -0
  40. data/lib/makara/middleware.rb +109 -0
  41. data/lib/makara/pool.rb +188 -0
  42. data/lib/makara/proxy.rb +277 -0
  43. data/lib/makara/railtie.rb +14 -0
  44. data/lib/makara/version.rb +15 -0
  45. data/makara.gemspec +19 -0
  46. data/spec/active_record/connection_adapters/makara_abstract_adapter_error_handling_spec.rb +92 -0
  47. data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +114 -0
  48. data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +183 -0
  49. data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +121 -0
  50. data/spec/cache_spec.rb +59 -0
  51. data/spec/config_parser_spec.rb +102 -0
  52. data/spec/connection_wrapper_spec.rb +33 -0
  53. data/spec/context_spec.rb +107 -0
  54. data/spec/middleware_spec.rb +84 -0
  55. data/spec/pool_spec.rb +158 -0
  56. data/spec/proxy_spec.rb +182 -0
  57. data/spec/spec_helper.rb +46 -0
  58. data/spec/support/configurator.rb +13 -0
  59. data/spec/support/deep_dup.rb +12 -0
  60. data/spec/support/mock_objects.rb +67 -0
  61. data/spec/support/mysql2_database.yml +17 -0
  62. data/spec/support/mysql2_database_with_custom_errors.yml +17 -0
  63. data/spec/support/pool_extensions.rb +14 -0
  64. data/spec/support/postgresql_database.yml +13 -0
  65. data/spec/support/proxy_extensions.rb +33 -0
  66. data/spec/support/schema.rb +7 -0
  67. metadata +144 -0
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::Cache do
4
+
5
+ it 'should not require a store be set' do
6
+ described_class.store = nil
7
+
8
+ expect(
9
+ described_class.send(:store)
10
+ ).to be_nil
11
+
12
+ expect{
13
+ described_class.read('test')
14
+ }.not_to raise_error
15
+ end
16
+
17
+ it 'provides a few stores for testing purposes' do
18
+ described_class.store = :memory
19
+ described_class.write('test', 'value', 10)
20
+ expect(described_class.read('test')).to eq('value')
21
+
22
+ described_class.store = :noop
23
+ described_class.write('test', 'value', 10)
24
+ expect(described_class.read('test')).to be_nil
25
+ end
26
+
27
+
28
+ # this will be used in tests so we have to ensure this works as expected
29
+ context Makara::Cache::MemoryStore do
30
+
31
+ let(:store){ Makara::Cache::MemoryStore.new }
32
+ let(:data){ store.instance_variable_get('@data') }
33
+
34
+ it 'should read and write keys' do
35
+ expect(store.read('test')).to be_nil
36
+ store.write('test', 'value')
37
+ expect(store.read('test')).to eq('value')
38
+ end
39
+
40
+ it 'provides time based expiration' do
41
+ store.write('test', 'value', :expires_in => 5)
42
+ expect(store.read('test')).to eq('value')
43
+
44
+ Timecop.travel Time.now + 6 do
45
+ expect(store.read('test')).to be_nil
46
+ end
47
+ end
48
+
49
+ it 'cleans the data' do
50
+ store.write('test', 'value', :expires_in => -5)
51
+ expect(store.read('test')).to be_nil
52
+ expect(data).not_to have_key('test')
53
+ end
54
+
55
+ end
56
+
57
+
58
+
59
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::ConfigParser do
4
+
5
+ let(:config){
6
+ {
7
+ :top_level => 'value',
8
+ :makara => {
9
+ :connections => [
10
+ {
11
+ :role => 'master',
12
+ :name => 'themaster'
13
+ },
14
+ {
15
+ :name => 'slave1'
16
+ },
17
+ {
18
+ :name => 'slave2'
19
+ }
20
+ ]
21
+ }
22
+ }
23
+ }
24
+
25
+ context '::merge_and_resolve_default_url_config' do
26
+ let(:config_without_url) do
27
+ {
28
+ :master_ttl => 5,
29
+ :blacklist_duration => 30,
30
+ :sticky => true,
31
+ :adapter => 'mysql2_makara',
32
+ :encoding => 'utf8',
33
+ :host => 'localhost',
34
+ :database => 'db_name',
35
+ :username => 'db_username',
36
+ :password => 'db_password',
37
+ :port => 3306
38
+ }
39
+ end
40
+
41
+ let(:config_with_url) do
42
+ {
43
+ :master_ttl => 5,
44
+ :blacklist_duration => 30,
45
+ :sticky => true,
46
+ :adapter => 'mysql2_makara',
47
+ :encoding => 'utf8',
48
+ :url => 'mysql2://db_username:db_password@localhost:3306/db_name'
49
+ }
50
+ end
51
+
52
+ it 'does nothing to a config without a url parameter' do
53
+ config = config_without_url.dup
54
+ expected = config_without_url.dup
55
+ actual = described_class.merge_and_resolve_default_url_config(config)
56
+ expect(actual).to eq(expected)
57
+ end
58
+
59
+ it 'parses the url parameter and merges it into the config' do
60
+ config = config_with_url.dup
61
+ expected = config_without_url.dup
62
+ actual = described_class.merge_and_resolve_default_url_config(config)
63
+ expect(actual).to eq(expected)
64
+ end
65
+
66
+ it 'does not use DATABASE_URL env variable' do
67
+ database_url = ENV['DATABASE_URL']
68
+ ENV['DATABASE_URL'] = config_with_url[:url]
69
+ begin
70
+ config = config_with_url.dup
71
+ config.delete(:url)
72
+ expected = config.dup
73
+ actual = described_class.merge_and_resolve_default_url_config(config)
74
+ expect(actual).to eq(expected)
75
+ ensure
76
+ ENV['DATABASE_URL'] = database_url
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ it 'should provide an id based on the recursively sorted config' do
83
+ parsera = described_class.new(config)
84
+ parserb = described_class.new(config.merge(:other => 'value'))
85
+ parserc = described_class.new(config)
86
+
87
+ expect(parsera.id).not_to eq(parserb.id)
88
+ expect(parsera.id).to eq(parserc.id)
89
+ end
90
+
91
+ it 'should provide master and slave configs' do
92
+ parser = described_class.new(config)
93
+ expect(parser.master_configs).to eq([
94
+ {:name => 'themaster', :top_level => 'value', :blacklist_duration => 30, :master_ttl => 5, :sticky => true}
95
+ ])
96
+ expect(parser.slave_configs).to eq([
97
+ {:name => 'slave1', :top_level => 'value', :blacklist_duration => 30, :master_ttl => 5, :sticky => true},
98
+ {:name => 'slave2', :top_level => 'value', :blacklist_duration => 30, :master_ttl => 5, :sticky => true}
99
+ ])
100
+ end
101
+
102
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::ConnectionWrapper do
4
+
5
+ let(:proxy){ FakeProxy.new({:makara => {:blacklist_duration => 5, :connections => [{:role => 'master'}, {:role => 'slave'}, {:role => 'slave'}]}}) }
6
+ let(:connection){ subject._makara_connection }
7
+
8
+ subject{ proxy.master_pool.connections.first }
9
+
10
+ it 'should extend the connection with new functionality' do
11
+ expect(connection).to respond_to(:_makara_name)
12
+ expect(connection).to respond_to(:_makara)
13
+ expect(connection).to respond_to(:_makara_hijack)
14
+ end
15
+
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")
19
+ end
20
+
21
+ it 'should have a default weight of 1' do
22
+ expect(subject._makara_weight).to eq(1)
23
+ end
24
+
25
+ it 'should store the blacklist status' do
26
+ expect(subject._makara_blacklisted?).to eq(false)
27
+ subject._makara_blacklist!
28
+ expect(subject._makara_blacklisted?).to eq(true)
29
+ subject._makara_whitelist!
30
+ expect(subject._makara_blacklisted?).to eq(false)
31
+ end
32
+
33
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::Context do
4
+
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
21
+
22
+ threads.map(&:join)
23
+ expect(uniq_curret_contexts.uniq.count).to eq(3)
24
+ end
25
+ end
26
+
27
+ describe 'set_current' do
28
+
29
+ it 'does not share context acorss threads' do
30
+ uniq_curret_contexts = []
31
+ threads = []
32
+
33
+ 1.upto(3).map do |i|
34
+ threads << Thread.new do
35
+
36
+ current = Makara::Context.set_current("val#{i}")
37
+ expect(current).to_not be_nil
38
+ expect(current).to eq("val#{i}")
39
+
40
+ sleep(0.2)
41
+
42
+ current = Makara::Context.get_current
43
+ expect(current).to_not be_nil
44
+ expect(current).to eq("val#{i}")
45
+
46
+ uniq_curret_contexts << current
47
+ end
48
+ sleep(0.1)
49
+ end
50
+
51
+ threads.map(&:join)
52
+ expect(uniq_curret_contexts.uniq.count).to eq(3)
53
+ end
54
+ end
55
+
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
72
+
73
+ threads.map(&:join)
74
+ expect(uniq_curret_contexts.uniq.count).to eq(3)
75
+ end
76
+ end
77
+
78
+ describe 'set_previous' do
79
+ it 'does not share context acorss threads' do
80
+ uniq_curret_contexts = []
81
+ threads = []
82
+
83
+ 1.upto(3).map do |i|
84
+ threads << Thread.new do
85
+
86
+ current = Makara::Context.set_previous("val#{i}")
87
+ expect(current).to_not be_nil
88
+ expect(current).to eq("val#{i}")
89
+
90
+ sleep(0.2)
91
+
92
+ current = Makara::Context.get_previous
93
+ expect(current).to_not be_nil
94
+ expect(current).to eq("val#{i}")
95
+
96
+ uniq_curret_contexts << current
97
+ end
98
+ sleep(0.1)
99
+ end
100
+
101
+ threads.map(&:join)
102
+ expect(uniq_curret_contexts.uniq.count).to eq(3)
103
+ end
104
+ end
105
+
106
+ end
107
+
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::Middleware do
4
+
5
+ let(:app){
6
+ lambda{|env|
7
+ proxy.query(env[:query] || 'select * from users')
8
+ [200, {}, ["#{Makara::Context.get_current}-#{Makara::Context.get_previous}"]]
9
+ }
10
+ }
11
+
12
+ let(:env){ {} }
13
+ let(:proxy){ FakeProxy.new(config(1,2)) }
14
+ let(:middleware){ described_class.new(app) }
15
+
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)
24
+
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)
30
+ end
31
+
32
+ it 'should use the cookie-provided context if present' do
33
+ env['HTTP_COOKIE'] = "#{key}=abcdefg--200; path=/; max-age=5"
34
+
35
+ response = middleware.call(env)
36
+ current, prev = context_from(response)
37
+
38
+ expect(prev).to eq('abcdefg')
39
+ expect(current).to eq(Makara::Context.get_current)
40
+ expect(current).not_to eq('abcdefg')
41
+ end
42
+
43
+ it 'should use the param-provided context if present' do
44
+ env['QUERY_STRING'] = "dog=true&#{key}=abcdefg&cat=false"
45
+
46
+ response = middleware.call(env)
47
+ current, prev = context_from(response)
48
+
49
+ expect(prev).to eq('abcdefg')
50
+ expect(current).to eq(Makara::Context.get_current)
51
+ expect(current).not_to eq('abcdefg')
52
+ end
53
+
54
+ it 'should set the cookie if master is used' do
55
+ env[:query] = 'update users set name = "phil"'
56
+
57
+ status, headers, body = middleware.call(env)
58
+
59
+ expect(headers['Set-Cookie']).to eq("#{key}=#{Makara::Context.get_current}--200; path=/; max-age=5; HttpOnly")
60
+ 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
+ end
@@ -0,0 +1,158 @@
1
+ require 'spec_helper'
2
+
3
+ describe Makara::Pool do
4
+
5
+ let(:proxy){ FakeProxy.new({:makara => pool_config.merge(:connections => [])}) }
6
+ let(:pool){ Makara::Pool.new('test', proxy) }
7
+ let(:pool_config){ {:blacklist_duration => 5} }
8
+
9
+ it 'should wrap connections with a ConnectionWrapper as theyre added to the pool' do
10
+ expect(pool.connections).to be_empty
11
+
12
+ connection_a = FakeConnection.new
13
+ connection_a.something = 'a'
14
+
15
+ wrapper_a = pool.add(pool_config){ connection_a }
16
+ wrapper_b = pool.add(pool_config.merge(:weight => 2)){ FakeConnection.new }
17
+
18
+ expect(pool.connections.length).to eq(3)
19
+
20
+ expect(wrapper_a).to be_a(Makara::ConnectionWrapper)
21
+ expect(wrapper_a.irespondtothis).to eq('hey!')
22
+
23
+ as, bs = pool.connections.partition{|c| c.something == 'a'}
24
+ expect(as.length).to eq(1)
25
+ expect(bs.length).to eq(2)
26
+ end
27
+
28
+ it 'should determine if its completely blacklisted' do
29
+
30
+ pool.add(pool_config){ FakeConnection.new }
31
+ pool.add(pool_config){ FakeConnection.new }
32
+
33
+ expect(pool).not_to be_completely_blacklisted
34
+
35
+ pool.connections.each(&:_makara_blacklist!)
36
+
37
+ expect(pool).to be_completely_blacklisted
38
+ end
39
+
40
+ it 'sends methods to all underlying objects if asked to' do
41
+
42
+ a = FakeConnection.new
43
+ b = FakeConnection.new
44
+
45
+ pool.add(pool_config){ a }
46
+ pool.add(pool_config){ b }
47
+
48
+ expect(a).to receive(:query).with('test').once
49
+ expect(b).to receive(:query).with('test').once
50
+
51
+ pool.send_to_all :query, 'test'
52
+
53
+ end
54
+
55
+ it 'only sends methods to underlying objects which are not blacklisted' do
56
+
57
+ a = FakeConnection.new
58
+ b = FakeConnection.new
59
+ c = FakeConnection.new
60
+
61
+ pool.add(pool_config){ a }
62
+ pool.add(pool_config){ b }
63
+ wrapper_c = pool.add(pool_config){ c }
64
+
65
+ expect(a).to receive(:query).with('test').once
66
+ expect(b).to receive(:query).with('test').once
67
+ expect(c).to receive(:query).with('test').never
68
+
69
+ wrapper_c._makara_blacklist!
70
+
71
+ pool.send_to_all :query, 'test'
72
+
73
+ end
74
+
75
+ it 'provides the next connection and blacklists' do
76
+
77
+ connection_a = FakeConnection.new
78
+ connection_b = FakeConnection.new
79
+
80
+ wrapper_a = pool.add(pool_config){ connection_a }
81
+ wrapper_b = pool.add(pool_config){ connection_b }
82
+
83
+ pool.provide do |connection|
84
+ if connection == wrapper_a
85
+ raise Makara::Errors::BlacklistConnection.new(wrapper_a, StandardError.new('failure'))
86
+ end
87
+ end
88
+
89
+ expect(wrapper_a._makara_blacklisted?).to eq(true)
90
+ expect(wrapper_b._makara_blacklisted?).to eq(false)
91
+
92
+ Timecop.travel Time.now + 10 do
93
+ expect(wrapper_a._makara_blacklisted?).to eq(false)
94
+ expect(wrapper_b._makara_blacklisted?).to eq(false)
95
+ end
96
+
97
+ end
98
+
99
+ it 'provides the same connection if the context has not changed and the proxy is sticky' do
100
+ allow(proxy).to receive(:sticky){ true }
101
+
102
+ pool.add(pool_config){ FakeConnection.new }
103
+ pool.add(pool_config){ FakeConnection.new }
104
+
105
+ provided = []
106
+
107
+ 10.times{ pool.provide{|con| provided << con } }
108
+
109
+ expect(provided.uniq.length).to eq(1)
110
+ end
111
+
112
+ it 'does not provide the same connection if the proxy is not sticky' do
113
+ allow(proxy).to receive(:sticky){ false }
114
+
115
+ pool.add(pool_config){ FakeConnection.new }
116
+ pool.add(pool_config){ FakeConnection.new }
117
+
118
+ provided = []
119
+
120
+ 10.times{ pool.provide{|con| provided << con } }
121
+
122
+ expect(provided.uniq.length).to eq(2)
123
+ end
124
+
125
+ it 'raises an error when all connections are blacklisted' do
126
+
127
+ wrapper_a = pool.add(pool_config.dup){ FakeConnection.new }
128
+ wrapper_b = pool.add(pool_config.dup){ FakeConnection.new }
129
+
130
+ # make the connection
131
+ pool.send_to_all :to_s
132
+
133
+ allow(pool).to receive(:next).and_return(wrapper_a, wrapper_b, nil)
134
+
135
+
136
+ begin
137
+ pool.provide do |connection|
138
+ raise Makara::Errors::BlacklistConnection.new(connection, StandardError.new('failure'))
139
+ end
140
+ rescue Makara::Errors::AllConnectionsBlacklisted => e
141
+ expect(e).to be_present
142
+ expect(e.message).to eq("[Makara/test] All connections are blacklisted -> [Makara/test/2] failure -> [Makara/test/1] failure")
143
+ end
144
+ end
145
+
146
+ it 'skips blacklisted connections when choosing the next one' do
147
+
148
+ pool.add(pool_config){ FakeConnection.new }
149
+ pool.add(pool_config){ FakeConnection.new }
150
+
151
+ wrapper_b = pool.add(pool_config){ FakeConnection.new }
152
+ wrapper_b._makara_blacklist!
153
+
154
+ 10.times{ pool.provide{|connection| expect(connection).not_to eq(wrapper_b) } }
155
+
156
+ end
157
+
158
+ end