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
@@ -82,7 +82,7 @@ describe 'MakaraMysql2Adapter' do
82
82
  ActiveRecord::Base.connection
83
83
 
84
84
  load(File.dirname(__FILE__) + '/../../support/schema.rb')
85
- Makara::Context.set_current Makara::Context.generate
85
+ change_context
86
86
 
87
87
  allow(ActiveRecord::Base).to receive(:mysql2_connection) do |config|
88
88
  config[:username] = db_username
@@ -150,7 +150,7 @@ describe 'MakaraMysql2Adapter' do
150
150
 
151
151
  it 'should send reads to the slave' do
152
152
  # ensure the next connection will be the first one
153
- connection.slave_pool.strategy.instance_variable_set('@current_idx', connection.slave_pool.connections.length)
153
+ allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:single_one?){ true }
154
154
 
155
155
  con = connection.slave_pool.connections.first
156
156
  expect(con).to receive(:execute).with('SELECT * FROM users').once
@@ -158,6 +158,21 @@ describe 'MakaraMysql2Adapter' do
158
158
  connection.execute('SELECT * FROM users')
159
159
  end
160
160
 
161
+ it 'should send exists? to slave' do
162
+ next if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 # query doesn't work?
163
+
164
+ allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:single_one?){ true }
165
+ Test::User.exists? # flush other (schema) things that need to happen
166
+
167
+ con = connection.slave_pool.connections.first
168
+ if (ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR <= 0)
169
+ expect(con).to receive(:execute).with(/SELECT\s+1\s*(AS one)?\s+FROM .?users.?\s+LIMIT\s+.?1/, any_args).once.and_call_original
170
+ else
171
+ expect(con).to receive(:exec_query).with(/SELECT\s+1\s*(AS one)?\s+FROM .?users.?\s+LIMIT\s+.?1/, any_args).once.and_call_original
172
+ end
173
+ Test::User.exists?
174
+ end
175
+
161
176
  it 'should send writes to master' do
162
177
  con = connection.master_pool.connections.first
163
178
  expect(con).to receive(:execute).with('UPDATE users SET name = "bob" WHERE id = 1')
@@ -0,0 +1,155 @@
1
+ # RGeo doesn't play well with JRuby and to avoid complicated test setup
2
+ # we're only testing ActiveRecord version ~> 4.2
3
+ # also rgeo only works on 2.1+
4
+
5
+ rmajor, rminor, rpatch = RUBY_VERSION.split(/[^\d]/)[0..2].map(&:to_i)
6
+ require 'active_record'
7
+
8
+ # TODO: test this in AR 5+ ?
9
+
10
+ if RUBY_ENGINE == 'ruby' &&
11
+ ActiveRecord::VERSION::MAJOR == 4 &&
12
+ ActiveRecord::VERSION::MINOR >= 2 &&
13
+ (rmajor > 2 || (rmajor == 2 && rminor >= 1))
14
+
15
+ require 'spec_helper'
16
+ require 'rgeo'
17
+ require 'activerecord-postgis-adapter'
18
+ require 'active_record/connection_adapters/postgis_adapter'
19
+
20
+ describe 'MakaraPostgisAdapter' do
21
+ let(:db_username){ ENV['TRAVIS'] ? 'postgres' : `whoami`.chomp }
22
+
23
+ let(:config) do
24
+ base = YAML.load_file(File.expand_path('spec/support/postgis_database.yml'))['test']
25
+ base['username'] = db_username
26
+ base
27
+ end
28
+
29
+ let(:connection) { ActiveRecord::Base.connection }
30
+
31
+ before :each do
32
+ ActiveRecord::Base.clear_all_connections!
33
+ change_context
34
+ end
35
+
36
+ it 'should allow a connection to be established' do
37
+ ActiveRecord::Base.establish_connection(config)
38
+ expect(ActiveRecord::Base.connection)
39
+ .to be_instance_of(ActiveRecord::ConnectionAdapters::MakaraPostgisAdapter)
40
+ end
41
+
42
+ context 'with the connection established and schema loaded' do
43
+ before do
44
+ ActiveRecord::Base.establish_connection(config)
45
+ load(File.dirname(__FILE__) + '/../../support/schema.rb')
46
+ load(File.dirname(__FILE__) + '/../../support/postgis_schema.rb')
47
+ change_context
48
+ RGeo::ActiveRecord::SpatialFactoryStore.instance.tap do |config|
49
+ # By default, use the GEOS implementation for spatial columns.
50
+ config.default = RGeo::Geos.factory_generator
51
+
52
+ # But use a geographic implementation for point columns.
53
+ config.register(RGeo::Geographic.spherical_factory(srid: 4326), geo_type: "point")
54
+ end
55
+ end
56
+
57
+ let(:town_class) do
58
+ Class.new(ActiveRecord::Base) do
59
+ self.table_name = :towns
60
+ end
61
+ end
62
+
63
+ it 'should have one master and two slaves' do
64
+ expect(connection.master_pool.connection_count).to eq(1)
65
+ expect(connection.slave_pool.connection_count).to eq(2)
66
+ end
67
+
68
+ it 'should allow real queries to work' do
69
+ connection.execute('INSERT INTO users (name) VALUES (\'John\')')
70
+
71
+ connection.master_pool.connections.each do |master|
72
+ expect(master).to receive(:execute).never
73
+ end
74
+
75
+ change_context
76
+ res = connection.execute('SELECT name FROM users ORDER BY id DESC LIMIT 1')
77
+
78
+ expect(res.to_a[0]['name']).to eq('John')
79
+ end
80
+
81
+ it 'should send SET operations to each connection' do
82
+ connection.master_pool.connections.each do |con|
83
+ expect(con).to receive(:execute).with("SET TimeZone = 'UTC'").once
84
+ end
85
+
86
+ connection.slave_pool.connections.each do |con|
87
+ expect(con).to receive(:execute).with("SET TimeZone = 'UTC'").once
88
+ end
89
+ connection.execute("SET TimeZone = 'UTC'")
90
+ end
91
+
92
+ it 'should send reads to the slave' do
93
+ # ensure the next connection will be the first one
94
+ allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:single_one?){ true }
95
+
96
+ con = connection.slave_pool.connections.first
97
+ expect(con).to receive(:execute).with('SELECT * FROM users').once
98
+
99
+ connection.execute('SELECT * FROM users')
100
+ end
101
+
102
+ it 'should send writes to master' do
103
+ con = connection.master_pool.connections.first
104
+ expect(con).to receive(:execute).with('UPDATE users SET name = "bob" WHERE id = 1')
105
+ connection.execute('UPDATE users SET name = "bob" WHERE id = 1')
106
+ end
107
+
108
+ it 'should interpret points correctly' do
109
+ town_class.create!(location: 'Point(1 2)')
110
+ town = town_class.last
111
+ expect(town.location.x).to eq 1
112
+ expect(town.location.y).to eq 2
113
+ end
114
+ end
115
+
116
+ context 'without live connections' do
117
+ it 'should raise errors on read or write' do
118
+ allow(ActiveRecord::Base).to receive(:postgis_connection).and_raise(StandardError.new('could not connect to server: Connection refused'))
119
+
120
+ ActiveRecord::Base.establish_connection(config)
121
+ expect { connection.execute('SELECT * FROM users') }.to raise_error(Makara::Errors::NoConnectionsAvailable)
122
+ expect { connection.execute('INSERT INTO users (name) VALUES (\'John\')') }.to raise_error(Makara::Errors::NoConnectionsAvailable)
123
+ end
124
+ end
125
+
126
+ context 'with only master connection' do
127
+ it 'should not raise errors on read and write' do
128
+ custom_config = config.deep_dup
129
+ custom_config['makara']['connections'].select{|h| h['role'] == 'slave' }.each{|h| h['port'] = '1'}
130
+
131
+ ActiveRecord::Base.establish_connection(custom_config)
132
+ load(File.dirname(__FILE__) + '/../../support/schema.rb')
133
+
134
+ connection.execute('SELECT * FROM users')
135
+ connection.execute('INSERT INTO users (name) VALUES (\'John\')')
136
+ end
137
+ end
138
+
139
+ context 'with only slave connection' do
140
+ it 'should raise error only on write' do
141
+ ActiveRecord::Base.establish_connection(config)
142
+ load(File.dirname(__FILE__) + '/../../support/schema.rb')
143
+ ActiveRecord::Base.clear_all_connections!
144
+
145
+ custom_config = config.deep_dup
146
+ custom_config['makara']['connections'].select{|h| h['role'] == 'master' }.each{|h| h['port'] = '1'}
147
+
148
+ ActiveRecord::Base.establish_connection(custom_config)
149
+
150
+ connection.execute('SELECT * FROM users')
151
+ expect { connection.execute('INSERT INTO users (name) VALUES (\'John\')') }.to raise_error(Makara::Errors::NoConnectionsAvailable)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -1,15 +1,16 @@
1
1
  require 'spec_helper'
2
2
  require 'active_record/connection_adapters/postgresql_adapter'
3
+ require 'active_record/errors'
3
4
 
4
5
  describe 'MakaraPostgreSQLAdapter' do
5
6
 
6
7
  let(:db_username){ ENV['TRAVIS'] ? 'postgres' : `whoami`.chomp }
7
8
 
8
- let(:config){
9
+ let(:config) do
9
10
  base = YAML.load_file(File.expand_path('spec/support/postgresql_database.yml'))['test']
10
11
  base['username'] = db_username
11
12
  base
12
- }
13
+ end
13
14
 
14
15
  let(:connection) { ActiveRecord::Base.connection }
15
16
 
@@ -64,7 +65,7 @@ describe 'MakaraPostgreSQLAdapter' do
64
65
 
65
66
  it 'should send reads to the slave' do
66
67
  # ensure the next connection will be the first one
67
- connection.slave_pool.strategy.instance_variable_set('@current_idx', connection.slave_pool.connections.length)
68
+ allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:single_one?){ true }
68
69
 
69
70
  con = connection.slave_pool.connections.first
70
71
  expect(con).to receive(:execute).with('SELECT * FROM users').once
@@ -72,6 +73,22 @@ describe 'MakaraPostgreSQLAdapter' do
72
73
  connection.execute('SELECT * FROM users')
73
74
  end
74
75
 
76
+ it 'should send exists? to slave' do
77
+ next if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0 # query doesn't work?
78
+
79
+ allow_any_instance_of(Makara::Strategies::RoundRobin).to receive(:single_one?){ true }
80
+ Test::User.exists? # flush other (schema) things that need to happen
81
+
82
+ con = connection.slave_pool.connections.first
83
+ if (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2) ||
84
+ (ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR <= 0)
85
+ expect(con).to receive(:exec_no_cache).with(/SELECT\s+1\s*(AS one)?\s+FROM .?users.?\s+LIMIT\s+.?1/, any_args).once.and_call_original
86
+ else
87
+ expect(con).to receive(:exec_query).with(/SELECT\s+1\s*(AS one)?\s+FROM .?users.?\s+LIMIT\s+.?1/, any_args).once.and_call_original
88
+ end
89
+ Test::User.exists?
90
+ end
91
+
75
92
  it 'should send writes to master' do
76
93
  con = connection.master_pool.connections.first
77
94
  expect(con).to receive(:execute).with('UPDATE users SET name = "bob" WHERE id = 1')
@@ -175,4 +192,60 @@ describe 'MakaraPostgreSQLAdapter' do
175
192
  it_behaves_like 'a transaction supporter'
176
193
  end
177
194
  end
195
+
196
+ context 'with two activerecord connection pools' do
197
+
198
+ before :each do
199
+ class Model1 < ActiveRecord::Base
200
+ end
201
+
202
+ class Model2 < ActiveRecord::Base
203
+ end
204
+
205
+ Model1.establish_connection(config)
206
+ Model2.establish_connection(config)
207
+
208
+ end
209
+
210
+ it 'should not leak raw connection into activerecord pool' do
211
+ # checkout a connection from Model1 pool and remove from the pool
212
+ conn = Model1.connection_pool.checkout
213
+ Model1.connection_pool.remove(conn)
214
+
215
+ # assign the connection to Model2 pool
216
+ conn.pool=Model2.connection_pool
217
+
218
+ # now close the connection to return it back to the pool
219
+ conn.close
220
+
221
+ # checkout the connection and make sure it is still a makara proxy
222
+ expect(Model2.connection).to eq(conn)
223
+ end
224
+
225
+ it 'should be able to steal the connection from a different thread' do
226
+ conn = Model1.connection_pool.checkout
227
+ conn.steal!
228
+ expect(conn.owner).to eq(Thread.current)
229
+ # steal! is not thread safe. it should be done while holding connection pool's mutex
230
+ t = Thread.new { conn.steal! }
231
+ t.join
232
+ expect(conn.owner).to eq(t)
233
+ end
234
+
235
+ it 'should not be able to expire the connection from same thread' do
236
+ conn = Model2.connection_pool.checkout
237
+ # expire is not thread safe. it should be done while holding connection pool's mutex
238
+ expect {
239
+ t = Thread.new { conn.expire }
240
+ t.join
241
+ }.to raise_error(ActiveRecord::ActiveRecordError)
242
+ end
243
+
244
+ it 'should be able to checkin connection back into activerecord pool' do
245
+ conn = Model1.connection_pool.checkout
246
+ Model1.connection_pool.checkin(conn)
247
+ # checkout the connection again and make sure it is same connection
248
+ expect(Model1.connection).to eq(conn)
249
+ end
250
+ end
178
251
  end
@@ -2,58 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe Makara::Cache do
4
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
-
5
+ it 'shows a warning' do
6
+ expect(Makara::Logging::Logger).to receive(:log).with(/Setting the Makara::Cache\.store won't have any effects/, :warn)
22
7
  described_class.store = :noop
23
- described_class.write('test', 'value', 10)
24
- expect(described_class.read('test')).to be_nil
25
8
  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
9
  end
@@ -79,7 +79,7 @@ describe Makara::ConfigParser do
79
79
 
80
80
  end
81
81
 
82
- it 'should provide an id based on the recursively sorted config' do
82
+ it 'should provide a default proxy id based on the recursively sorted config' do
83
83
  parsera = described_class.new(config)
84
84
  parserb = described_class.new(config.merge(:other => 'value'))
85
85
  parserc = described_class.new(config)
@@ -88,6 +88,26 @@ describe Makara::ConfigParser do
88
88
  expect(parsera.id).to eq(parserc.id)
89
89
  end
90
90
 
91
+ it 'should use provided proxy id instead of default' do
92
+ config_with_custom_id = config.dup
93
+ config_with_custom_id[:makara][:id] = 'my_proxy'
94
+
95
+ parser = described_class.new(config_with_custom_id)
96
+
97
+ expect(parser.id).to eq('my_proxy')
98
+ end
99
+
100
+ it 'should replace reserved characters and show a warning for provided proxy ids' do
101
+ config_with_custom_id = config.dup
102
+ config_with_custom_id[:makara][:id] = "my|proxy|id:with:reserved:characters"
103
+ warning = "Proxy id 'my|proxy|id:with:reserved:characters' changed to 'myproxyidwithreservedcharacters'"
104
+ expect(Makara::Logging::Logger).to receive(:log).with(warning, :warn)
105
+
106
+ parser = described_class.new(config_with_custom_id)
107
+
108
+ expect(parser.id).to eq('myproxyidwithreservedcharacters')
109
+ end
110
+
91
111
  context 'master and slave configs' do
92
112
  it 'should provide master and slave configs' do
93
113
  parser = described_class.new(config)
@@ -97,8 +117,7 @@ describe Makara::ConfigParser do
97
117
  :top_level => 'value',
98
118
  :sticky => true,
99
119
  :blacklist_duration => 30,
100
- :master_ttl => 5,
101
- :sticky => true
120
+ :master_ttl => 5
102
121
  }
103
122
  ])
104
123
  expect(parser.slave_configs).to eq([
@@ -107,16 +126,14 @@ describe Makara::ConfigParser do
107
126
  :top_level => 'value',
108
127
  :sticky => true,
109
128
  :blacklist_duration => 30,
110
- :master_ttl => 5,
111
- :sticky => true
129
+ :master_ttl => 5
112
130
  },
113
131
  {
114
132
  :name => 'slave2',
115
133
  :top_level => 'value',
116
134
  :sticky => true,
117
135
  :blacklist_duration => 30,
118
- :master_ttl => 5,
119
- :sticky => true
136
+ :master_ttl => 5
120
137
  }
121
138
  ])
122
139
  end
@@ -133,8 +150,7 @@ describe Makara::ConfigParser do
133
150
  :top_level => 'value',
134
151
  :sticky => true,
135
152
  :blacklist_duration => 456,
136
- :master_ttl => 5,
137
- :sticky => true
153
+ :master_ttl => 5
138
154
  }
139
155
  ])
140
156
  expect(parser.slave_configs).to eq([
@@ -143,16 +159,14 @@ describe Makara::ConfigParser do
143
159
  :top_level => 'slave value',
144
160
  :sticky => true,
145
161
  :blacklist_duration => 123,
146
- :master_ttl => 5,
147
- :sticky => true
162
+ :master_ttl => 5
148
163
  },
149
164
  {
150
165
  :name => 'slave2',
151
166
  :top_level => 'value',
152
167
  :sticky => true,
153
168
  :blacklist_duration => 123,
154
- :master_ttl => 5,
155
- :sticky => true
169
+ :master_ttl => 5
156
170
  }
157
171
  ])
158
172
  end