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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +28 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +278 -0
- data/Rakefile +9 -0
- data/gemfiles/ar30.gemfile +30 -0
- data/gemfiles/ar31.gemfile +29 -0
- data/gemfiles/ar32.gemfile +29 -0
- data/gemfiles/ar40.gemfile +15 -0
- data/gemfiles/ar41.gemfile +15 -0
- data/gemfiles/ar42.gemfile +15 -0
- data/lib/active_record/connection_adapters/jdbcmysql_makara_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/jdbcpostgresql_makara_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +209 -0
- data/lib/active_record/connection_adapters/makara_jdbcmysql_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_jdbcpostgresql_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_mysql2_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/makara_postgresql_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/mysql2_makara_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/postgresql_makara_adapter.rb +44 -0
- data/lib/makara.rb +25 -0
- data/lib/makara/cache.rb +53 -0
- data/lib/makara/cache/memory_store.rb +28 -0
- data/lib/makara/cache/noop_store.rb +15 -0
- data/lib/makara/config_parser.rb +200 -0
- data/lib/makara/connection_wrapper.rb +170 -0
- data/lib/makara/context.rb +46 -0
- data/lib/makara/error_handler.rb +39 -0
- data/lib/makara/errors/all_connections_blacklisted.rb +13 -0
- data/lib/makara/errors/blacklist_connection.rb +14 -0
- data/lib/makara/errors/no_connections_available.rb +14 -0
- data/lib/makara/logging/logger.rb +23 -0
- data/lib/makara/logging/subscriber.rb +38 -0
- data/lib/makara/middleware.rb +109 -0
- data/lib/makara/pool.rb +188 -0
- data/lib/makara/proxy.rb +277 -0
- data/lib/makara/railtie.rb +14 -0
- data/lib/makara/version.rb +15 -0
- data/makara.gemspec +19 -0
- data/spec/active_record/connection_adapters/makara_abstract_adapter_error_handling_spec.rb +92 -0
- data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +114 -0
- data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +183 -0
- data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +121 -0
- data/spec/cache_spec.rb +59 -0
- data/spec/config_parser_spec.rb +102 -0
- data/spec/connection_wrapper_spec.rb +33 -0
- data/spec/context_spec.rb +107 -0
- data/spec/middleware_spec.rb +84 -0
- data/spec/pool_spec.rb +158 -0
- data/spec/proxy_spec.rb +182 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/configurator.rb +13 -0
- data/spec/support/deep_dup.rb +12 -0
- data/spec/support/mock_objects.rb +67 -0
- data/spec/support/mysql2_database.yml +17 -0
- data/spec/support/mysql2_database_with_custom_errors.yml +17 -0
- data/spec/support/pool_extensions.rb +14 -0
- data/spec/support/postgresql_database.yml +13 -0
- data/spec/support/proxy_extensions.rb +33 -0
- data/spec/support/schema.rb +7 -0
- metadata +144 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_record/connection_adapters/makara_abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ConnectionHandling
|
8
|
+
def makara_postgresql_connection(config)
|
9
|
+
ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter.new(config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
else
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
class Base
|
18
|
+
def self.makara_postgresql_connection(config)
|
19
|
+
ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter.new(config)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module ActiveRecord
|
27
|
+
module ConnectionAdapters
|
28
|
+
class MakaraPostgreSQLAdapter < ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def visitor_for(*args)
|
32
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.visitor_for(*args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def active_record_connection_for(config)
|
39
|
+
::ActiveRecord::Base.postgresql_connection(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_record/connection_adapters/makara_abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
3
|
+
|
4
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ConnectionHandling
|
8
|
+
def mysql2_makara_connection(config)
|
9
|
+
ActiveRecord::ConnectionAdapters::MakaraMysql2Adapter.new(config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
else
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
class Base
|
18
|
+
def self.mysql2_makara_connection(config)
|
19
|
+
ActiveRecord::ConnectionAdapters::MakaraMysql2Adapter.new(config)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module ActiveRecord
|
27
|
+
module ConnectionAdapters
|
28
|
+
class MakaraMysql2Adapter < ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def visitor_for(*args)
|
32
|
+
ActiveRecord::ConnectionAdapters::Mysql2Adapter.visitor_for(*args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def active_record_connection_for(config)
|
39
|
+
::ActiveRecord::Base.mysql2_connection(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'active_record/connection_adapters/makara_abstract_adapter'
|
2
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
|
4
|
+
if ActiveRecord::VERSION::MAJOR >= 4
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module ConnectionHandling
|
8
|
+
def postgresql_makara_connection(config)
|
9
|
+
ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter.new(config)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
else
|
15
|
+
|
16
|
+
module ActiveRecord
|
17
|
+
class Base
|
18
|
+
def self.postgresql_makara_connection(config)
|
19
|
+
ActiveRecord::ConnectionAdapters::MakaraPostgreSQLAdapter.new(config)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
module ActiveRecord
|
27
|
+
module ConnectionAdapters
|
28
|
+
class MakaraPostgreSQLAdapter < ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter
|
29
|
+
|
30
|
+
class << self
|
31
|
+
def visitor_for(*args)
|
32
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.visitor_for(*args)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def active_record_connection_for(config)
|
39
|
+
::ActiveRecord::Base.postgresql_connection(config)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/makara.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'makara/version'
|
2
|
+
require 'makara/railtie' if defined?(Rails)
|
3
|
+
module Makara
|
4
|
+
|
5
|
+
autoload :Cache, 'makara/cache'
|
6
|
+
autoload :ConfigParser, 'makara/config_parser'
|
7
|
+
autoload :ConnectionWrapper, 'makara/connection_wrapper'
|
8
|
+
autoload :Context, 'makara/context'
|
9
|
+
autoload :ErrorHandler, 'makara/error_handler'
|
10
|
+
autoload :Middleware, 'makara/middleware'
|
11
|
+
autoload :Pool, 'makara/pool'
|
12
|
+
autoload :Proxy, 'makara/proxy'
|
13
|
+
|
14
|
+
module Errors
|
15
|
+
autoload :AllConnectionsBlacklisted, 'makara/errors/all_connections_blacklisted'
|
16
|
+
autoload :BlacklistConnection, 'makara/errors/blacklist_connection'
|
17
|
+
autoload :NoConnectionsAvailable, 'makara/errors/no_connections_available'
|
18
|
+
end
|
19
|
+
|
20
|
+
module Logging
|
21
|
+
autoload :Logger, 'makara/logging/logger'
|
22
|
+
autoload :Subscriber, 'makara/logging/subscriber'
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/makara/cache.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'active_support/core_ext/object/try'
|
2
|
+
|
3
|
+
# The Makara Cache should have access to your centralized cache store.
|
4
|
+
# It serves the purpose of storing the Makara::Context across requests, servers, etc.
|
5
|
+
|
6
|
+
module Makara
|
7
|
+
module Cache
|
8
|
+
|
9
|
+
autoload :MemoryStore, 'makara/cache/memory_store'
|
10
|
+
autoload :NoopStore, 'makara/cache/noop_store'
|
11
|
+
|
12
|
+
class << self
|
13
|
+
|
14
|
+
def store=(store)
|
15
|
+
@store = store
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(key)
|
19
|
+
store.try(:read, key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(key, value, ttl)
|
23
|
+
store.try(:write, key, value, :expires_in => ttl.to_i)
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def store
|
29
|
+
case @store
|
30
|
+
when :noop, :null
|
31
|
+
@store = Makara::Cache::NoopStore.new
|
32
|
+
when :memory
|
33
|
+
@store = Makara::Cache::MemoryStore.new
|
34
|
+
else
|
35
|
+
if defined?(Rails)
|
36
|
+
|
37
|
+
# in AR3 RAILS_CACHE may not be loaded if the full env is not present
|
38
|
+
# Rails.cache will throw an error because of it.
|
39
|
+
if ActiveRecord::VERSION::MAJOR < 4
|
40
|
+
@store ||= Rails.cache if defined?(RAILS_CACHE)
|
41
|
+
else
|
42
|
+
@store ||= Rails.cache if defined?(Rails)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
@store
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Makara
|
2
|
+
module Cache
|
3
|
+
class MemoryStore
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@data = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def read(key)
|
10
|
+
clean
|
11
|
+
@data[key].try(:[], 0)
|
12
|
+
end
|
13
|
+
|
14
|
+
def write(key, value, options = {})
|
15
|
+
clean
|
16
|
+
@data[key] = [value, Time.now.to_i + (options[:expires_in] || 5).to_i]
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def clean
|
23
|
+
@data.delete_if{|k,v| v[1] <= Time.now.to_i }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
require 'active_support/core_ext/hash/keys'
|
3
|
+
require 'active_support/core_ext/hash/except'
|
4
|
+
|
5
|
+
# Convenience methods to grab subconfigs out of the primary configuration.
|
6
|
+
# Provides a way to generate a consistent ID based on a unique config.
|
7
|
+
# Makara configs should be formatted like so:
|
8
|
+
# --
|
9
|
+
# top_level: 'variable'
|
10
|
+
# another: 'top level variable'
|
11
|
+
# makara:
|
12
|
+
# master_ttl: 3
|
13
|
+
# blacklist_duration: 20
|
14
|
+
# connections:
|
15
|
+
# - role: 'master'
|
16
|
+
# - role: 'slave'
|
17
|
+
# - role: 'slave'
|
18
|
+
# name: 'slave2'
|
19
|
+
|
20
|
+
module Makara
|
21
|
+
class ConfigParser
|
22
|
+
|
23
|
+
DEFAULTS = {
|
24
|
+
:master_ttl => 5,
|
25
|
+
:blacklist_duration => 30,
|
26
|
+
:sticky => true
|
27
|
+
}
|
28
|
+
|
29
|
+
# ConnectionUrlResolver is borrowed from Rails 4-2 since its location and implementation
|
30
|
+
# vary slightly among Rails versions, but the behavior is the same. Thus, borrowing the
|
31
|
+
# class should be the most future-safe way to parse a database url.
|
32
|
+
#
|
33
|
+
# Expands a connection string into a hash.
|
34
|
+
class ConnectionUrlResolver # :nodoc:
|
35
|
+
|
36
|
+
# == Example
|
37
|
+
#
|
38
|
+
# url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
|
39
|
+
# ConnectionUrlResolver.new(url).to_hash
|
40
|
+
# # => {
|
41
|
+
# "adapter" => "postgresql",
|
42
|
+
# "host" => "localhost",
|
43
|
+
# "port" => 9000,
|
44
|
+
# "database" => "foo_test",
|
45
|
+
# "username" => "foo",
|
46
|
+
# "password" => "bar",
|
47
|
+
# "pool" => "5",
|
48
|
+
# "timeout" => "3000"
|
49
|
+
# }
|
50
|
+
def initialize(url)
|
51
|
+
raise "Database URL cannot be empty" if url.blank?
|
52
|
+
@uri = URI.parse(url)
|
53
|
+
@adapter = @uri.scheme.tr('-', '_')
|
54
|
+
@adapter = "postgresql" if @adapter == "postgres"
|
55
|
+
|
56
|
+
if @uri.opaque
|
57
|
+
@uri.opaque, @query = @uri.opaque.split('?', 2)
|
58
|
+
else
|
59
|
+
@query = @uri.query
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Converts the given URL to a full connection hash.
|
64
|
+
def to_hash
|
65
|
+
config = raw_config.reject { |_,value| value.blank? }
|
66
|
+
config.map { |key,value| config[key] = URI.unescape(value) if value.is_a? String }
|
67
|
+
config
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def uri
|
73
|
+
@uri
|
74
|
+
end
|
75
|
+
|
76
|
+
# Converts the query parameters of the URI into a hash.
|
77
|
+
#
|
78
|
+
# "localhost?pool=5&reaping_frequency=2"
|
79
|
+
# # => { "pool" => "5", "reaping_frequency" => "2" }
|
80
|
+
#
|
81
|
+
# returns empty hash if no query present.
|
82
|
+
#
|
83
|
+
# "localhost"
|
84
|
+
# # => {}
|
85
|
+
def query_hash
|
86
|
+
Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
|
87
|
+
end
|
88
|
+
|
89
|
+
def raw_config
|
90
|
+
if uri.opaque
|
91
|
+
query_hash.merge({
|
92
|
+
"adapter" => @adapter,
|
93
|
+
"database" => uri.opaque })
|
94
|
+
else
|
95
|
+
query_hash.merge({
|
96
|
+
"adapter" => @adapter,
|
97
|
+
"username" => uri.user,
|
98
|
+
"password" => uri.password,
|
99
|
+
"port" => uri.port,
|
100
|
+
"database" => database_from_path,
|
101
|
+
"host" => uri.host })
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns name of the database.
|
106
|
+
def database_from_path
|
107
|
+
if @adapter == 'sqlite3'
|
108
|
+
# 'sqlite3:/foo' is absolute, because that makes sense. The
|
109
|
+
# corresponding relative version, 'sqlite3:foo', is handled
|
110
|
+
# elsewhere, as an "opaque".
|
111
|
+
|
112
|
+
uri.path
|
113
|
+
else
|
114
|
+
# Only SQLite uses a filename as the "database" name; for
|
115
|
+
# anything else, a leading slash would be silly.
|
116
|
+
|
117
|
+
uri.path.sub(%r{^/}, "")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# NOTE: url format must be, e.g.
|
123
|
+
# url: mysql2://...
|
124
|
+
# NOT
|
125
|
+
# url: mysql2_makara://...
|
126
|
+
# since the '_' in the protocol (mysql2_makara) makes the URI invalid
|
127
|
+
# NOTE: Does not use ENV['DATABASE_URL']
|
128
|
+
def self.merge_and_resolve_default_url_config(config)
|
129
|
+
if ENV['DATABASE_URL']
|
130
|
+
Logging::Logger.log "Please rename DATABASE_URL to use in the database.yml", :warn
|
131
|
+
end
|
132
|
+
return config unless config.key?(:url)
|
133
|
+
url = config[:url]
|
134
|
+
url_config = ConnectionUrlResolver.new(url).to_hash
|
135
|
+
url_config = url_config.symbolize_keys
|
136
|
+
url_config.delete(:adapter)
|
137
|
+
config.delete(:url)
|
138
|
+
config.update(url_config)
|
139
|
+
end
|
140
|
+
|
141
|
+
attr_reader :makara_config
|
142
|
+
|
143
|
+
def initialize(config)
|
144
|
+
@config = config.symbolize_keys
|
145
|
+
@makara_config = DEFAULTS.merge(@config[:makara] || {})
|
146
|
+
@makara_config = @makara_config.symbolize_keys
|
147
|
+
@id = @makara_config[:id]
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
def id
|
152
|
+
@id ||= begin
|
153
|
+
sorted = recursive_sort(@config)
|
154
|
+
Digest::MD5.hexdigest(sorted.to_s)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
def master_configs
|
160
|
+
all_configs.
|
161
|
+
select{|config| config[:role] == 'master' }.
|
162
|
+
map{|config| config.except(:role) }
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
def slave_configs
|
167
|
+
all_configs.
|
168
|
+
reject{|config| config[:role] == 'master' }.
|
169
|
+
map{|config| config.except(:role) }
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
protected
|
174
|
+
|
175
|
+
|
176
|
+
def all_configs
|
177
|
+
@makara_config[:connections].map do |connection|
|
178
|
+
base_config.merge(connection.symbolize_keys)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
def base_config
|
184
|
+
@base_config ||= DEFAULTS.merge(@config).except(:makara)
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
def recursive_sort(thing)
|
189
|
+
return thing.to_s unless thing.include?(Enumerable)
|
190
|
+
|
191
|
+
thing.map do |part|
|
192
|
+
recursive_sort(part)
|
193
|
+
end
|
194
|
+
|
195
|
+
thing.sort_by(&:to_s)
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
end
|