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.
- checksums.yaml +5 -5
- data/.github/workflows/gem-publish-public.yml +36 -0
- data/.travis.yml +71 -9
- data/CHANGELOG.md +84 -25
- data/Gemfile +4 -3
- data/README.md +37 -34
- data/gemfiles/ar-head.gemfile +9 -0
- data/gemfiles/ar30.gemfile +7 -1
- data/gemfiles/ar31.gemfile +8 -1
- data/gemfiles/ar32.gemfile +8 -1
- data/gemfiles/ar40.gemfile +10 -1
- data/gemfiles/ar41.gemfile +10 -1
- data/gemfiles/ar42.gemfile +10 -1
- data/gemfiles/ar50.gemfile +11 -2
- data/gemfiles/ar51.gemfile +11 -2
- data/gemfiles/ar52.gemfile +24 -0
- data/gemfiles/ar60.gemfile +24 -0
- data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +109 -3
- data/lib/active_record/connection_adapters/makara_postgis_adapter.rb +41 -0
- data/lib/makara.rb +15 -4
- data/lib/makara/cache.rb +4 -40
- data/lib/makara/config_parser.rb +14 -3
- data/lib/makara/connection_wrapper.rb +26 -2
- data/lib/makara/context.rb +108 -38
- data/lib/makara/cookie.rb +52 -0
- data/lib/makara/error_handler.rb +2 -2
- data/lib/makara/errors/blacklisted_while_in_transaction.rb +14 -0
- data/lib/makara/errors/invalid_shard.rb +16 -0
- data/lib/makara/logging/logger.rb +1 -1
- data/lib/makara/middleware.rb +12 -75
- data/lib/makara/pool.rb +53 -40
- data/lib/makara/proxy.rb +52 -30
- data/lib/makara/railtie.rb +0 -6
- data/lib/makara/strategies/round_robin.rb +6 -0
- data/lib/makara/strategies/shard_aware.rb +47 -0
- data/lib/makara/version.rb +2 -2
- data/makara.gemspec +5 -1
- data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +10 -5
- data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +17 -2
- data/spec/active_record/connection_adapters/makara_postgis_adapter_spec.rb +155 -0
- data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +76 -3
- data/spec/cache_spec.rb +2 -52
- data/spec/config_parser_spec.rb +27 -13
- data/spec/connection_wrapper_spec.rb +5 -2
- data/spec/context_spec.rb +163 -100
- data/spec/cookie_spec.rb +72 -0
- data/spec/middleware_spec.rb +26 -55
- data/spec/pool_spec.rb +24 -0
- data/spec/proxy_spec.rb +51 -36
- data/spec/spec_helper.rb +5 -9
- data/spec/strategies/shard_aware_spec.rb +219 -0
- data/spec/support/helpers.rb +6 -2
- data/spec/support/mock_objects.rb +5 -1
- data/spec/support/mysql2_database.yml +1 -0
- data/spec/support/mysql2_database_with_custom_errors.yml +5 -0
- data/spec/support/postgis_database.yml +15 -0
- data/spec/support/postgis_schema.rb +11 -0
- data/spec/support/postgresql_database.yml +2 -0
- data/spec/support/proxy_extensions.rb +1 -1
- data/spec/support/schema.rb +5 -5
- data/spec/support/user.rb +5 -0
- metadata +28 -9
- data/lib/makara/cache/memory_store.rb +0 -28
- data/lib/makara/cache/noop_store.rb +0 -15
data/lib/makara/config_parser.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
require 'active_support/core_ext/hash/keys'
|
3
3
|
require 'active_support/core_ext/hash/except'
|
4
|
+
require 'cgi'
|
4
5
|
|
5
6
|
# Convenience methods to grab subconfigs out of the primary configuration.
|
6
7
|
# Provides a way to generate a consistent ID based on a unique config.
|
@@ -63,7 +64,7 @@ module Makara
|
|
63
64
|
# Converts the given URL to a full connection hash.
|
64
65
|
def to_hash
|
65
66
|
config = raw_config.reject { |_,value| value.blank? }
|
66
|
-
config.map { |key,value| config[key] =
|
67
|
+
config.map { |key,value| config[key] = CGI.unescape(value) if value.is_a? String }
|
67
68
|
config
|
68
69
|
end
|
69
70
|
|
@@ -127,7 +128,7 @@ module Makara
|
|
127
128
|
# NOTE: Does not use ENV['DATABASE_URL']
|
128
129
|
def self.merge_and_resolve_default_url_config(config)
|
129
130
|
if ENV['DATABASE_URL']
|
130
|
-
Logging::Logger.log "Please rename DATABASE_URL to use in the database.yml", :warn
|
131
|
+
Makara::Logging::Logger.log "Please rename DATABASE_URL to use in the database.yml", :warn
|
131
132
|
end
|
132
133
|
return config unless config.key?(:url)
|
133
134
|
url = config[:url]
|
@@ -144,7 +145,7 @@ module Makara
|
|
144
145
|
@config = config.symbolize_keys
|
145
146
|
@makara_config = DEFAULTS.merge(@config[:makara] || {})
|
146
147
|
@makara_config = @makara_config.symbolize_keys
|
147
|
-
@id = @makara_config[:id]
|
148
|
+
@id = sanitize_id(@makara_config[:id])
|
148
149
|
end
|
149
150
|
|
150
151
|
|
@@ -197,5 +198,15 @@ module Makara
|
|
197
198
|
|
198
199
|
end
|
199
200
|
|
201
|
+
|
202
|
+
def sanitize_id(id)
|
203
|
+
return if id.nil? || id.empty?
|
204
|
+
|
205
|
+
id.gsub(/[\|:]/, '').tap do |sanitized_id|
|
206
|
+
if sanitized_id.size != id.size
|
207
|
+
Makara::Logging::Logger.log "Proxy id '#{id}' changed to '#{sanitized_id}'", :warn
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
200
211
|
end
|
201
212
|
end
|
@@ -35,11 +35,19 @@ module Makara
|
|
35
35
|
@config[:name]
|
36
36
|
end
|
37
37
|
|
38
|
+
def _makara_shard_id
|
39
|
+
@config[:shard_id]
|
40
|
+
end
|
41
|
+
|
38
42
|
# has this node been blacklisted?
|
39
43
|
def _makara_blacklisted?
|
40
44
|
@blacklisted_until.present? && @blacklisted_until.to_i > Time.now.to_i
|
41
45
|
end
|
42
46
|
|
47
|
+
def _makara_in_transaction?
|
48
|
+
@connection && @connection.open_transactions > 0 ? true : false
|
49
|
+
end
|
50
|
+
|
43
51
|
# blacklist this node for @config[:blacklist_duration] seconds
|
44
52
|
def _makara_blacklist!
|
45
53
|
@connection.disconnect! if @connection
|
@@ -146,10 +154,10 @@ module Makara
|
|
146
154
|
# The new definition should allow for the proxy to intercept the invocation if required.
|
147
155
|
@proxy.class.hijack_methods.each do |meth|
|
148
156
|
extension << %Q{
|
149
|
-
def #{meth}(*args)
|
157
|
+
def #{meth}(*args, &block)
|
150
158
|
_makara_hijack do |proxy|
|
151
159
|
if proxy
|
152
|
-
proxy.#{meth}(*args)
|
160
|
+
proxy.#{meth}(*args, &block)
|
153
161
|
else
|
154
162
|
super
|
155
163
|
end
|
@@ -158,6 +166,22 @@ module Makara
|
|
158
166
|
}
|
159
167
|
end
|
160
168
|
|
169
|
+
# Control methods must always be passed to the
|
170
|
+
# Makara::Proxy control object for handling (typically
|
171
|
+
# related to ActiveRecord connection pool management)
|
172
|
+
@proxy.class.control_methods.each do |meth|
|
173
|
+
extension << %Q{
|
174
|
+
def #{meth}(*args, &block)
|
175
|
+
proxy = _makara
|
176
|
+
if proxy
|
177
|
+
proxy.control.#{meth}(*args=args, block)
|
178
|
+
else
|
179
|
+
super # Only if we are not wrapped any longer
|
180
|
+
end
|
181
|
+
end
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
161
185
|
# extend the instance
|
162
186
|
con.instance_eval(extension)
|
163
187
|
# set the makara context
|
data/lib/makara/context.rb
CHANGED
@@ -1,63 +1,134 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
|
3
|
-
# Keeps track of the current
|
4
|
-
# If a new context is needed it can be generated via Makara::Context.generate
|
5
|
-
|
3
|
+
# Keeps track of the current stickiness state for different Makara proxies
|
6
4
|
module Makara
|
7
5
|
class Context
|
8
|
-
|
6
|
+
attr_accessor :stored_data, :staged_data
|
7
|
+
attr_reader :current_timestamp
|
8
|
+
|
9
|
+
def initialize(context_data)
|
10
|
+
@stored_data = context_data
|
11
|
+
@staged_data = {}
|
12
|
+
@dirty = @was_dirty = false
|
13
|
+
|
14
|
+
freeze_time
|
15
|
+
end
|
9
16
|
|
10
|
-
|
11
|
-
|
12
|
-
|
17
|
+
def stage(proxy_id, ttl)
|
18
|
+
staged_data[proxy_id] = [staged_data[proxy_id].to_f, ttl.to_f].max
|
19
|
+
end
|
20
|
+
|
21
|
+
def stuck?(proxy_id)
|
22
|
+
stored_data[proxy_id] && !expired?(stored_data[proxy_id])
|
23
|
+
end
|
24
|
+
|
25
|
+
def staged?(proxy_id)
|
26
|
+
staged_data.key?(proxy_id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def release(proxy_id)
|
30
|
+
@dirty ||= !!stored_data.delete(proxy_id)
|
31
|
+
staged_data.delete(proxy_id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def release_all
|
35
|
+
if self.stored_data.any?
|
36
|
+
self.stored_data = {}
|
37
|
+
# We need to track a change made to the current stored data
|
38
|
+
# so we can commit it later
|
39
|
+
@dirty = true
|
13
40
|
end
|
41
|
+
self.staged_data = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Stores the staged data with an expiration time based on the current time,
|
45
|
+
# and clears any expired entries. Returns true if any changes were made to
|
46
|
+
# the current store
|
47
|
+
def commit
|
48
|
+
freeze_time
|
49
|
+
release_expired
|
50
|
+
store_staged_data
|
51
|
+
clean
|
14
52
|
|
15
|
-
|
16
|
-
|
53
|
+
was_dirty?
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def freeze_time
|
59
|
+
@current_timestamp = Time.now.to_f
|
60
|
+
end
|
61
|
+
|
62
|
+
# Indicates whether there have been changes to the context that need
|
63
|
+
# to be persisted when the request finishes
|
64
|
+
def dirty?
|
65
|
+
@dirty
|
66
|
+
end
|
67
|
+
|
68
|
+
def was_dirty?
|
69
|
+
@was_dirty
|
70
|
+
end
|
71
|
+
|
72
|
+
def expired?(timestamp)
|
73
|
+
timestamp <= current_timestamp
|
74
|
+
end
|
75
|
+
|
76
|
+
def release_expired
|
77
|
+
previous_size = stored_data.size
|
78
|
+
stored_data.delete_if { |_, timestamp| expired?(timestamp) }
|
79
|
+
@dirty ||= previous_size != stored_data.size
|
80
|
+
end
|
81
|
+
|
82
|
+
def store_staged_data
|
83
|
+
staged_data.each do |proxy_id, ttl|
|
84
|
+
if ttl > 0 && self.stored_data[proxy_id].to_f < current_timestamp + ttl
|
85
|
+
self.stored_data[proxy_id] = current_timestamp + ttl
|
86
|
+
@dirty = true
|
87
|
+
end
|
17
88
|
end
|
89
|
+
end
|
18
90
|
|
19
|
-
|
20
|
-
|
21
|
-
|
91
|
+
def clean
|
92
|
+
@was_dirty = dirty?
|
93
|
+
@dirty = false
|
94
|
+
@staged_data = {}
|
95
|
+
end
|
96
|
+
|
97
|
+
class << self
|
98
|
+
def set_current(context_data)
|
99
|
+
set(:makara_current_context, new(context_data))
|
22
100
|
end
|
23
101
|
|
24
|
-
|
25
|
-
|
102
|
+
# Called by `Proxy#stick_to_master!` to use master in subsequent requests
|
103
|
+
def stick(proxy_id, ttl)
|
104
|
+
current.stage(proxy_id, ttl)
|
26
105
|
end
|
27
106
|
|
28
|
-
def
|
29
|
-
|
107
|
+
def stuck?(proxy_id)
|
108
|
+
current.staged?(proxy_id) || current.stuck?(proxy_id)
|
30
109
|
end
|
31
110
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
111
|
+
def next
|
112
|
+
if current.commit
|
113
|
+
current.stored_data
|
35
114
|
end
|
36
115
|
end
|
37
116
|
|
38
|
-
|
39
|
-
|
40
|
-
# when they're asking whether they should be stuck to master.
|
41
|
-
def stick(context, config_id, ttl)
|
42
|
-
Makara::Cache.write(cache_key_for(context, config_id), '1', ttl)
|
117
|
+
def release(proxy_id)
|
118
|
+
current.release(proxy_id)
|
43
119
|
end
|
44
120
|
|
45
|
-
def
|
46
|
-
|
121
|
+
def release_all
|
122
|
+
current.release_all
|
47
123
|
end
|
48
124
|
|
49
125
|
protected
|
50
|
-
|
51
|
-
|
52
|
-
fetch(:makara_previously_sticky) { Hash.new }
|
53
|
-
end
|
54
|
-
|
55
|
-
def cache_key_for(context, config_id)
|
56
|
-
"makara::#{context}-#{config_id}"
|
126
|
+
def current
|
127
|
+
fetch(:makara_current_context) { new({}) }
|
57
128
|
end
|
58
129
|
|
59
130
|
def fetch(key)
|
60
|
-
get(key) || set(key,yield)
|
131
|
+
get(key) || set(key, yield)
|
61
132
|
end
|
62
133
|
|
63
134
|
if Thread.current.respond_to?(:thread_variable_get)
|
@@ -65,19 +136,18 @@ module Makara
|
|
65
136
|
Thread.current.thread_variable_get(key)
|
66
137
|
end
|
67
138
|
|
68
|
-
def set(key,value)
|
69
|
-
Thread.current.thread_variable_set(key,value)
|
139
|
+
def set(key, value)
|
140
|
+
Thread.current.thread_variable_set(key, value)
|
70
141
|
end
|
71
142
|
else
|
72
143
|
def get(key)
|
73
144
|
Thread.current[key]
|
74
145
|
end
|
75
146
|
|
76
|
-
def set(key,value)
|
147
|
+
def set(key, value)
|
77
148
|
Thread.current[key]=value
|
78
149
|
end
|
79
150
|
end
|
80
|
-
|
81
151
|
end
|
82
152
|
end
|
83
153
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Makara
|
2
|
+
module Cookie
|
3
|
+
extend self
|
4
|
+
|
5
|
+
IDENTIFIER = '_mkra_stck'.freeze
|
6
|
+
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
path: "/",
|
9
|
+
http_only: true
|
10
|
+
}
|
11
|
+
|
12
|
+
MAX_AGE_BUFFER = 5
|
13
|
+
|
14
|
+
def fetch(request)
|
15
|
+
parse(request.cookies[IDENTIFIER].to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def store(context_data, headers, options = {})
|
19
|
+
unless context_data.nil?
|
20
|
+
Rack::Utils.set_cookie_header! headers, IDENTIFIER, build_cookie(context_data, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
# Pairs of {proxy_id}:{timestamp}, separated by "|"
|
26
|
+
# proxy_id1:1518270031.3132212|proxy_id2:1518270030.313232 ..
|
27
|
+
def parse(cookie_string)
|
28
|
+
return {} if cookie_string.empty?
|
29
|
+
|
30
|
+
states = cookie_string.split("|")
|
31
|
+
states.each_with_object({}) do |state, context_data|
|
32
|
+
proxy_id, timestamp = state.split(":")
|
33
|
+
context_data[proxy_id] = timestamp.to_f if proxy_id && timestamp
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_cookie(context_data, options)
|
38
|
+
cookie = DEFAULT_OPTIONS.merge(options)
|
39
|
+
now = Time.now
|
40
|
+
|
41
|
+
cookie[:max_age] = if context_data.any?
|
42
|
+
(context_data.values.max - now.to_f).ceil + MAX_AGE_BUFFER
|
43
|
+
else
|
44
|
+
0
|
45
|
+
end
|
46
|
+
cookie[:expires] = now + cookie[:max_age]
|
47
|
+
cookie[:value] = context_data.collect { |proxy_id, ttl| "#{proxy_id}:#{ttl}" }.join('|')
|
48
|
+
|
49
|
+
cookie
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/makara/error_handler.rb
CHANGED
@@ -25,13 +25,13 @@ module Makara
|
|
25
25
|
|
26
26
|
def gracefully(connection, e)
|
27
27
|
err = Makara::Errors::BlacklistConnection.new(connection, e)
|
28
|
-
::Makara::Logging::Logger.log("
|
28
|
+
::Makara::Logging::Logger.log("Gracefully handling: #{err}")
|
29
29
|
raise err
|
30
30
|
end
|
31
31
|
|
32
32
|
|
33
33
|
def harshly(e)
|
34
|
-
::Makara::Logging::Logger.log("
|
34
|
+
::Makara::Logging::Logger.log("Harshly handling: #{e}\n#{e.backtrace.join("\n\t")}")
|
35
35
|
raise e
|
36
36
|
end
|
37
37
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Makara
|
2
|
+
module Errors
|
3
|
+
class InvalidShard < MakaraError
|
4
|
+
|
5
|
+
attr_reader :role
|
6
|
+
attr_reader :shard_id
|
7
|
+
|
8
|
+
def initialize(role, shard_id)
|
9
|
+
@role = role
|
10
|
+
@shard_id = shard_id
|
11
|
+
super "[Makara] Invalid shard_id #{shard_id} for the #{role} pool"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/makara/middleware.rb
CHANGED
@@ -1,37 +1,20 @@
|
|
1
1
|
require 'rack'
|
2
2
|
|
3
|
-
# Persists the Makara::Context across requests ensuring the same master pool is used on
|
4
|
-
# Simply sets the cookie with the current context and the status code of this request. The next request then sets
|
5
|
-
# the Makara::Context's previous context based on the the previous request. If a redirect is encountered the middleware
|
6
|
-
# will defer generation of a new context until a non-redirect request occurs.
|
7
|
-
|
3
|
+
# Persists the Makara::Context across requests ensuring the same master pool is used on subsequent requests.
|
8
4
|
module Makara
|
9
5
|
class Middleware
|
10
6
|
|
11
|
-
IDENTIFIER = '_mkra_ctxt'
|
12
|
-
|
13
|
-
DEFAULT_COOKIE = {
|
14
|
-
:path => '/',
|
15
|
-
:http_only => true,
|
16
|
-
:max_age => '5'
|
17
|
-
}
|
18
|
-
|
19
7
|
def initialize(app, cookie_options = {})
|
20
8
|
@app = app
|
21
|
-
@
|
9
|
+
@cookie_options = cookie_options
|
22
10
|
end
|
23
11
|
|
24
|
-
|
25
12
|
def call(env)
|
26
|
-
|
27
13
|
return @app.call(env) if ignore_request?(env)
|
28
|
-
|
29
|
-
Makara::Context.set_previous previous_context(env)
|
30
|
-
Makara::Context.set_current new_context(env)
|
14
|
+
set_current_context(env)
|
31
15
|
|
32
16
|
status, headers, body = @app.call(env)
|
33
|
-
|
34
|
-
store_context(status, headers)
|
17
|
+
store_new_context(headers)
|
35
18
|
|
36
19
|
[status, headers, body]
|
37
20
|
end
|
@@ -39,6 +22,14 @@ module Makara
|
|
39
22
|
|
40
23
|
protected
|
41
24
|
|
25
|
+
def set_current_context(env)
|
26
|
+
context_data = Makara::Cookie.fetch(Rack::Request.new(env))
|
27
|
+
Makara::Context.set_current(context_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def store_new_context(headers)
|
31
|
+
Makara::Cookie.store(Makara::Context.next, headers, @cookie_options)
|
32
|
+
end
|
42
33
|
|
43
34
|
# ignore asset paths
|
44
35
|
# consider allowing a filter proc to be provided in an initializer
|
@@ -50,59 +41,5 @@ module Makara
|
|
50
41
|
end
|
51
42
|
false
|
52
43
|
end
|
53
|
-
|
54
|
-
|
55
|
-
# generate a new context based on the request
|
56
|
-
# if the previous request was a redirect, we keep the same context
|
57
|
-
def new_context(env)
|
58
|
-
|
59
|
-
makara_context, makara_status = makara_values(env)
|
60
|
-
|
61
|
-
context = nil
|
62
|
-
|
63
|
-
# if the previous request was a redirect, let's keep that context
|
64
|
-
if makara_status.to_s =~ /^3/ # 300+ redirect
|
65
|
-
context = makara_context
|
66
|
-
end
|
67
|
-
|
68
|
-
context ||= Makara::Context.get_current if env['rack.test']
|
69
|
-
context ||= Makara::Context.generate(env["action_dispatch.request_id"])
|
70
|
-
context
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
# pulls the previous context out of the request
|
75
|
-
def previous_context(env)
|
76
|
-
context = makara_values(env).first
|
77
|
-
context ||= Makara::Context.get_previous if env['rack.test']
|
78
|
-
context ||= Makara::Context.generate
|
79
|
-
context
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
# retrieve the stored content from the cookie or query
|
84
|
-
# The value contains the hexdigest and status code of the previous
|
85
|
-
# response in the format: $digest--$status
|
86
|
-
def makara_values(env)
|
87
|
-
regex = /#{IDENTIFIER}=([\-a-z0-9A-Z]+)/
|
88
|
-
|
89
|
-
env['HTTP_COOKIE'].to_s =~ regex
|
90
|
-
return $1.split('--') if $1
|
91
|
-
|
92
|
-
env['QUERY_STRING'].to_s =~ regex
|
93
|
-
return $1.split('--') if $1
|
94
|
-
|
95
|
-
[nil, nil]
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
# push the current context into the cookie
|
100
|
-
# it should always be for the same path, only
|
101
|
-
# accessible via http and live for a short amount
|
102
|
-
# of time
|
103
|
-
def store_context(status, headers)
|
104
|
-
cookie = @cookie.merge(:value => "#{Makara::Context.get_current}--#{status}")
|
105
|
-
Rack::Utils.set_cookie_header! headers, IDENTIFIER, cookie
|
106
|
-
end
|
107
44
|
end
|
108
45
|
end
|