semian_extension 0.11.4.1
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/ext/semian/extconf.rb +33 -0
- data/ext/semian/resource.c +394 -0
- data/ext/semian/resource.h +133 -0
- data/ext/semian/semian.c +72 -0
- data/ext/semian/semian.h +14 -0
- data/ext/semian/sysv_semaphores.c +271 -0
- data/ext/semian/sysv_semaphores.h +122 -0
- data/ext/semian/tickets.c +76 -0
- data/ext/semian/tickets.h +13 -0
- data/ext/semian/types.h +41 -0
- data/lib/semian/adapter.rb +75 -0
- data/lib/semian/circuit_breaker.rb +167 -0
- data/lib/semian/grpc.rb +104 -0
- data/lib/semian/instrumentable.rb +28 -0
- data/lib/semian/lru_hash.rb +174 -0
- data/lib/semian/mysql2.rb +135 -0
- data/lib/semian/net_http.rb +117 -0
- data/lib/semian/platform.rb +16 -0
- data/lib/semian/protected_resource.rb +65 -0
- data/lib/semian/rails.rb +7 -0
- data/lib/semian/redis.rb +143 -0
- data/lib/semian/resource.rb +65 -0
- data/lib/semian/simple_integer.rb +38 -0
- data/lib/semian/simple_sliding_window.rb +68 -0
- data/lib/semian/simple_state.rb +50 -0
- data/lib/semian/typhoeus.rb +103 -0
- data/lib/semian/unprotected_resource.rb +73 -0
- data/lib/semian/version.rb +3 -0
- data/lib/semian.rb +310 -0
- metadata +260 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
module Semian
|
2
|
+
class CircuitBreaker #:nodoc:
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@state, :closed?, :open?, :half_open?
|
6
|
+
|
7
|
+
attr_reader :name, :half_open_resource_timeout, :error_timeout, :state, :last_error
|
8
|
+
|
9
|
+
def initialize(name, exceptions:, success_threshold:, error_threshold:,
|
10
|
+
error_timeout:, implementation:, half_open_resource_timeout: nil)
|
11
|
+
@name = name.to_sym
|
12
|
+
@success_count_threshold = success_threshold
|
13
|
+
@error_count_threshold = error_threshold
|
14
|
+
@error_timeout = error_timeout
|
15
|
+
@exceptions = exceptions
|
16
|
+
@half_open_resource_timeout = half_open_resource_timeout
|
17
|
+
|
18
|
+
@errors = implementation::SlidingWindow.new(max_size: @error_count_threshold)
|
19
|
+
@successes = implementation::Integer.new
|
20
|
+
@state = implementation::State.new
|
21
|
+
|
22
|
+
reset
|
23
|
+
end
|
24
|
+
|
25
|
+
def acquire(resource = nil, &block)
|
26
|
+
return yield if disabled?
|
27
|
+
transition_to_half_open if transition_to_half_open?
|
28
|
+
|
29
|
+
raise OpenCircuitError unless request_allowed?
|
30
|
+
|
31
|
+
result = nil
|
32
|
+
begin
|
33
|
+
result = maybe_with_half_open_resource_timeout(resource, &block)
|
34
|
+
rescue *@exceptions => error
|
35
|
+
if !error.respond_to?(:marks_semian_circuits?) || error.marks_semian_circuits?
|
36
|
+
mark_failed(error)
|
37
|
+
end
|
38
|
+
raise error
|
39
|
+
else
|
40
|
+
mark_success
|
41
|
+
end
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
def transition_to_half_open?
|
46
|
+
open? && error_timeout_expired? && !half_open?
|
47
|
+
end
|
48
|
+
|
49
|
+
def request_allowed?
|
50
|
+
closed? || half_open? || transition_to_half_open?
|
51
|
+
end
|
52
|
+
|
53
|
+
def mark_failed(error)
|
54
|
+
push_error(error)
|
55
|
+
push_time(@errors)
|
56
|
+
if closed?
|
57
|
+
transition_to_open if error_threshold_reached?
|
58
|
+
elsif half_open?
|
59
|
+
transition_to_open
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def mark_success
|
64
|
+
return unless half_open?
|
65
|
+
@successes.increment
|
66
|
+
transition_to_close if success_threshold_reached?
|
67
|
+
end
|
68
|
+
|
69
|
+
def reset
|
70
|
+
@errors.clear
|
71
|
+
@successes.reset
|
72
|
+
transition_to_close
|
73
|
+
end
|
74
|
+
|
75
|
+
def destroy
|
76
|
+
@errors.destroy
|
77
|
+
@successes.destroy
|
78
|
+
@state.destroy
|
79
|
+
end
|
80
|
+
|
81
|
+
def in_use?
|
82
|
+
return false if error_timeout_expired?
|
83
|
+
@errors.size > 0
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def transition_to_close
|
89
|
+
notify_state_transition(:closed)
|
90
|
+
log_state_transition(:closed)
|
91
|
+
@state.close!
|
92
|
+
@errors.clear
|
93
|
+
end
|
94
|
+
|
95
|
+
def transition_to_open
|
96
|
+
notify_state_transition(:open)
|
97
|
+
log_state_transition(:open)
|
98
|
+
@state.open!
|
99
|
+
end
|
100
|
+
|
101
|
+
def transition_to_half_open
|
102
|
+
notify_state_transition(:half_open)
|
103
|
+
log_state_transition(:half_open)
|
104
|
+
@state.half_open!
|
105
|
+
@successes.reset
|
106
|
+
end
|
107
|
+
|
108
|
+
def success_threshold_reached?
|
109
|
+
@successes.value >= @success_count_threshold
|
110
|
+
end
|
111
|
+
|
112
|
+
def error_threshold_reached?
|
113
|
+
@errors.size == @error_count_threshold
|
114
|
+
end
|
115
|
+
|
116
|
+
def error_timeout_expired?
|
117
|
+
last_error_time = @errors.last
|
118
|
+
return false unless last_error_time
|
119
|
+
Time.at(last_error_time) + @error_timeout < Time.now
|
120
|
+
end
|
121
|
+
|
122
|
+
def push_error(error)
|
123
|
+
@last_error = error
|
124
|
+
end
|
125
|
+
|
126
|
+
def push_time(window, time: Time.now)
|
127
|
+
window.reject! { |err_time| err_time + @error_timeout < time.to_i }
|
128
|
+
window << time.to_i
|
129
|
+
end
|
130
|
+
|
131
|
+
def log_state_transition(new_state)
|
132
|
+
return if @state.nil? || new_state == @state.value
|
133
|
+
|
134
|
+
str = "[#{self.class.name}] State transition from #{@state.value} to #{new_state}."
|
135
|
+
str << " success_count=#{@successes.value} error_count=#{@errors.size}"
|
136
|
+
str << " success_count_threshold=#{@success_count_threshold} error_count_threshold=#{@error_count_threshold}"
|
137
|
+
str << " error_timeout=#{@error_timeout} error_last_at=\"#{@errors.last}\""
|
138
|
+
str << " name=\"#{@name}\""
|
139
|
+
if new_state == :open && @last_error
|
140
|
+
str << " last_error_message=#{@last_error.message.inspect}"
|
141
|
+
end
|
142
|
+
|
143
|
+
Semian.logger.info(str)
|
144
|
+
end
|
145
|
+
|
146
|
+
def notify_state_transition(new_state)
|
147
|
+
Semian.notify(:state_change, self, nil, nil, state: new_state)
|
148
|
+
end
|
149
|
+
|
150
|
+
def disabled?
|
151
|
+
ENV['SEMIAN_CIRCUIT_BREAKER_DISABLED'] || ENV['SEMIAN_DISABLED']
|
152
|
+
end
|
153
|
+
|
154
|
+
def maybe_with_half_open_resource_timeout(resource, &block)
|
155
|
+
result =
|
156
|
+
if half_open? && @half_open_resource_timeout && resource.respond_to?(:with_resource_timeout)
|
157
|
+
resource.with_resource_timeout(@half_open_resource_timeout) do
|
158
|
+
block.call
|
159
|
+
end
|
160
|
+
else
|
161
|
+
block.call
|
162
|
+
end
|
163
|
+
|
164
|
+
result
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
data/lib/semian/grpc.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'semian/adapter'
|
2
|
+
require 'grpc'
|
3
|
+
|
4
|
+
module GRPC
|
5
|
+
GRPC::Unavailable.include(::Semian::AdapterError)
|
6
|
+
GRPC::Unknown.include(::Semian::AdapterError)
|
7
|
+
GRPC::ResourceExhausted.include(::Semian::AdapterError)
|
8
|
+
|
9
|
+
class SemianError < GRPC::Unavailable
|
10
|
+
attr_reader :details
|
11
|
+
|
12
|
+
def initialize(semian_identifier, *args)
|
13
|
+
super(*args)
|
14
|
+
@details = message
|
15
|
+
@semian_identifier = semian_identifier
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ResourceBusyError = Class.new(SemianError)
|
20
|
+
CircuitOpenError = Class.new(SemianError)
|
21
|
+
end
|
22
|
+
|
23
|
+
module Semian
|
24
|
+
module GRPC
|
25
|
+
attr_reader :raw_semian_options
|
26
|
+
include Semian::Adapter
|
27
|
+
|
28
|
+
ResourceBusyError = ::GRPC::ResourceBusyError
|
29
|
+
CircuitOpenError = ::GRPC::CircuitOpenError
|
30
|
+
|
31
|
+
class SemianConfigurationChangedError < RuntimeError
|
32
|
+
def initialize(msg = "Cannot re-initialize semian_configuration")
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
attr_accessor :exceptions
|
39
|
+
attr_reader :semian_configuration
|
40
|
+
|
41
|
+
def semian_configuration=(configuration)
|
42
|
+
raise Semian::GRPC::SemianConfigurationChangedError unless @semian_configuration.nil?
|
43
|
+
@semian_configuration = configuration
|
44
|
+
end
|
45
|
+
|
46
|
+
def retrieve_semian_configuration(host)
|
47
|
+
@semian_configuration.call(host) if @semian_configuration.respond_to?(:call)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def raw_semian_options
|
52
|
+
@raw_semian_options ||= begin
|
53
|
+
# If the host is empty, it's possible that the adapter was initialized
|
54
|
+
# with the channel. Therefore, we look into the channel to find the host
|
55
|
+
if @host.empty?
|
56
|
+
host = @ch.target
|
57
|
+
else
|
58
|
+
host = @host
|
59
|
+
end
|
60
|
+
@raw_semian_options = Semian::GRPC.retrieve_semian_configuration(host)
|
61
|
+
@raw_semian_options = @raw_semian_options.dup unless @raw_semian_options.nil?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def semian_identifier
|
66
|
+
@semian_identifier ||= raw_semian_options[:name]
|
67
|
+
end
|
68
|
+
|
69
|
+
def resource_exceptions
|
70
|
+
[
|
71
|
+
::GRPC::DeadlineExceeded,
|
72
|
+
::GRPC::ResourceExhausted,
|
73
|
+
::GRPC::Unavailable,
|
74
|
+
::GRPC::Unknown,
|
75
|
+
]
|
76
|
+
end
|
77
|
+
|
78
|
+
def disabled?
|
79
|
+
raw_semian_options.nil?
|
80
|
+
end
|
81
|
+
|
82
|
+
def request_response(*, **)
|
83
|
+
return super if disabled?
|
84
|
+
acquire_semian_resource(adapter: :grpc, scope: :request_response) { super }
|
85
|
+
end
|
86
|
+
|
87
|
+
def client_streamer(*, **)
|
88
|
+
return super if disabled?
|
89
|
+
acquire_semian_resource(adapter: :grpc, scope: :client_streamer) { super }
|
90
|
+
end
|
91
|
+
|
92
|
+
def server_streamer(*, **)
|
93
|
+
return super if disabled?
|
94
|
+
acquire_semian_resource(adapter: :grpc, scope: :server_streamer) { super }
|
95
|
+
end
|
96
|
+
|
97
|
+
def bidi_streamer(*, **)
|
98
|
+
return super if disabled?
|
99
|
+
acquire_semian_resource(adapter: :grpc, scope: :bidi_streamer) { super }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
::GRPC::ClientStub.prepend(Semian::GRPC)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Semian
|
2
|
+
module Instrumentable
|
3
|
+
def subscribe(name = rand, &block)
|
4
|
+
subscribers[name] = block
|
5
|
+
name
|
6
|
+
end
|
7
|
+
|
8
|
+
def unsubscribe(name)
|
9
|
+
subscribers.delete(name)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Args:
|
13
|
+
# event (string)
|
14
|
+
# resource (Object)
|
15
|
+
# scope (string)
|
16
|
+
# adapter (string)
|
17
|
+
# payload (optional)
|
18
|
+
def notify(*args)
|
19
|
+
subscribers.values.each { |subscriber| subscriber.call(*args) }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def subscribers
|
25
|
+
@subscribers ||= {}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
class LRUHash
|
2
|
+
# This LRU (Least Recently Used) hash will allow
|
3
|
+
# the cleaning of resources as time goes on.
|
4
|
+
# The goal is to remove the least recently used resources
|
5
|
+
# everytime we set a new resource. A default window of
|
6
|
+
# 5 minutes will allow empty item to stay in the hash
|
7
|
+
# for a maximum of 5 minutes
|
8
|
+
class NoopMutex
|
9
|
+
def synchronize(*)
|
10
|
+
yield
|
11
|
+
end
|
12
|
+
|
13
|
+
def try_lock
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
def unlock
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def locked?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def owned?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def keys
|
31
|
+
@lock.synchronize { @table.keys }
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear
|
35
|
+
@lock.synchronize { @table.clear }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create an LRU hash
|
39
|
+
#
|
40
|
+
# Arguments:
|
41
|
+
# +max_size+ The maximum size of the table
|
42
|
+
# +min_time+ The minimum time a resource can live in the cache
|
43
|
+
#
|
44
|
+
# Note:
|
45
|
+
# The +min_time+ is a stronger guarantee than +max_size+. That is, if there are
|
46
|
+
# more than +max_size+ entries in the cache, but they've all been updated more
|
47
|
+
# recently than +min_time+, the garbage collection will not remove them and the
|
48
|
+
# cache can grow without bound. This usually means that you have many active
|
49
|
+
# circuits to disparate endpoints (or your circuit names are bad).
|
50
|
+
# If the max_size is 0, the garbage collection will be very aggressive and
|
51
|
+
# potentially computationally expensive.
|
52
|
+
def initialize(max_size: Semian.maximum_lru_size, min_time: Semian.minimum_lru_time)
|
53
|
+
@max_size = max_size
|
54
|
+
@min_time = min_time
|
55
|
+
@table = {}
|
56
|
+
@lock =
|
57
|
+
if Semian.thread_safe?
|
58
|
+
Mutex.new
|
59
|
+
else
|
60
|
+
NoopMutex.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def size
|
65
|
+
@lock.synchronize { @table.size }
|
66
|
+
end
|
67
|
+
|
68
|
+
def count(&block)
|
69
|
+
@lock.synchronize { @table.count(&block) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty?
|
73
|
+
@lock.synchronize { @table.empty? }
|
74
|
+
end
|
75
|
+
|
76
|
+
def values
|
77
|
+
@lock.synchronize { @table.values }
|
78
|
+
end
|
79
|
+
|
80
|
+
def set(key, resource)
|
81
|
+
@lock.synchronize do
|
82
|
+
@table.delete(key)
|
83
|
+
@table[key] = resource
|
84
|
+
resource.updated_at = Time.now
|
85
|
+
end
|
86
|
+
clear_unused_resources if @table.length > @max_size
|
87
|
+
end
|
88
|
+
|
89
|
+
# This method uses the property that "Hashes enumerate their values in the
|
90
|
+
# order that the corresponding keys were inserted." Deleting a key and
|
91
|
+
# re-inserting it effectively moves it to the front of the cache.
|
92
|
+
# Update the `updated_at` field so we can use it later do decide if the
|
93
|
+
# resource is "in use".
|
94
|
+
def get(key)
|
95
|
+
@lock.synchronize do
|
96
|
+
found = @table.delete(key)
|
97
|
+
if found
|
98
|
+
@table[key] = found
|
99
|
+
found.updated_at = Time.now
|
100
|
+
end
|
101
|
+
found
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def delete(key)
|
106
|
+
@lock.synchronize do
|
107
|
+
@table.delete(key)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def []=(key, resource)
|
112
|
+
set(key, resource)
|
113
|
+
end
|
114
|
+
|
115
|
+
def [](key)
|
116
|
+
get(key)
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def clear_unused_resources
|
122
|
+
payload = {
|
123
|
+
size: @table.size,
|
124
|
+
examined: 0,
|
125
|
+
cleared: 0,
|
126
|
+
elapsed: nil,
|
127
|
+
}
|
128
|
+
timer_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
129
|
+
|
130
|
+
ran = try_synchronize do
|
131
|
+
# Clears resources that have not been used in the last 5 minutes.
|
132
|
+
|
133
|
+
stop_time = Time.now - @min_time # Don't process resources updated after this time
|
134
|
+
@table.each do |_, resource|
|
135
|
+
payload[:examined] += 1
|
136
|
+
|
137
|
+
# The update times of the resources in the LRU are monotonically increasing,
|
138
|
+
# time, so we can stop looking once we find the first resource with an
|
139
|
+
# update time after the stop_time.
|
140
|
+
break if resource.updated_at > stop_time
|
141
|
+
|
142
|
+
next if resource.in_use?
|
143
|
+
|
144
|
+
resource = @table.delete(resource.name)
|
145
|
+
if resource
|
146
|
+
payload[:cleared] += 1
|
147
|
+
resource.destroy
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
if ran
|
153
|
+
payload[:elapsed] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - timer_start
|
154
|
+
Semian.notify(:lru_hash_gc, self, nil, nil, payload)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
EXCEPTION_NEVER = {Exception => :never}.freeze
|
159
|
+
EXCEPTION_IMMEDIATE = {Exception => :immediate}.freeze
|
160
|
+
private_constant :EXCEPTION_NEVER
|
161
|
+
private_constant :EXCEPTION_IMMEDIATE
|
162
|
+
|
163
|
+
def try_synchronize
|
164
|
+
Thread.handle_interrupt(EXCEPTION_NEVER) do
|
165
|
+
begin
|
166
|
+
return false unless @lock.try_lock
|
167
|
+
Thread.handle_interrupt(EXCEPTION_IMMEDIATE) { yield }
|
168
|
+
true
|
169
|
+
ensure
|
170
|
+
@lock.unlock if @lock.owned?
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'semian/adapter'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
module Mysql2
|
5
|
+
Mysql2::Error.include(::Semian::AdapterError)
|
6
|
+
|
7
|
+
class SemianError < Mysql2::Error
|
8
|
+
def initialize(semian_identifier, *args)
|
9
|
+
super(*args)
|
10
|
+
@semian_identifier = semian_identifier
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
ResourceBusyError = Class.new(SemianError)
|
15
|
+
CircuitOpenError = Class.new(SemianError)
|
16
|
+
end
|
17
|
+
|
18
|
+
module Semian
|
19
|
+
module Mysql2
|
20
|
+
include Semian::Adapter
|
21
|
+
|
22
|
+
CONNECTION_ERROR = Regexp.union(
|
23
|
+
/Can't connect to MySQL server on/i,
|
24
|
+
/Lost connection to MySQL server/i,
|
25
|
+
/MySQL server has gone away/i,
|
26
|
+
/Too many connections/i,
|
27
|
+
/closed MySQL connection/i,
|
28
|
+
/Timeout waiting for a response/i,
|
29
|
+
)
|
30
|
+
|
31
|
+
ResourceBusyError = ::Mysql2::ResourceBusyError
|
32
|
+
CircuitOpenError = ::Mysql2::CircuitOpenError
|
33
|
+
PingFailure = Class.new(::Mysql2::Error)
|
34
|
+
|
35
|
+
DEFAULT_HOST = 'localhost'
|
36
|
+
DEFAULT_PORT = 3306
|
37
|
+
|
38
|
+
QUERY_WHITELIST = Regexp.union(
|
39
|
+
/\A(?:\/\*.*?\*\/)?\s*ROLLBACK/i,
|
40
|
+
/\A(?:\/\*.*?\*\/)?\s*COMMIT/i,
|
41
|
+
/\A(?:\/\*.*?\*\/)?\s*RELEASE\s+SAVEPOINT/i,
|
42
|
+
)
|
43
|
+
|
44
|
+
# The naked methods are exposed as `raw_query` and `raw_connect` for instrumentation purpose
|
45
|
+
def self.included(base)
|
46
|
+
base.send(:alias_method, :raw_query, :query)
|
47
|
+
base.send(:remove_method, :query)
|
48
|
+
|
49
|
+
base.send(:alias_method, :raw_connect, :connect)
|
50
|
+
base.send(:remove_method, :connect)
|
51
|
+
|
52
|
+
base.send(:alias_method, :raw_ping, :ping)
|
53
|
+
base.send(:remove_method, :ping)
|
54
|
+
end
|
55
|
+
|
56
|
+
def semian_identifier
|
57
|
+
@semian_identifier ||= begin
|
58
|
+
unless name = semian_options && semian_options[:name]
|
59
|
+
host = query_options[:host] || DEFAULT_HOST
|
60
|
+
port = query_options[:port] || DEFAULT_PORT
|
61
|
+
name = "#{host}:#{port}"
|
62
|
+
end
|
63
|
+
:"mysql_#{name}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def ping
|
68
|
+
result = nil
|
69
|
+
acquire_semian_resource(adapter: :mysql, scope: :ping) do
|
70
|
+
result = raw_ping
|
71
|
+
raise PingFailure.new(result.to_s) unless result
|
72
|
+
end
|
73
|
+
result
|
74
|
+
rescue ResourceBusyError, CircuitOpenError, PingFailure
|
75
|
+
false
|
76
|
+
end
|
77
|
+
|
78
|
+
def query(*args)
|
79
|
+
if query_whitelisted?(*args)
|
80
|
+
raw_query(*args)
|
81
|
+
else
|
82
|
+
acquire_semian_resource(adapter: :mysql, scope: :query) { raw_query(*args) }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# TODO: write_timeout and connect_timeout can't be configured currently
|
87
|
+
# dynamically, await https://github.com/brianmario/mysql2/pull/955
|
88
|
+
def with_resource_timeout(temp_timeout)
|
89
|
+
prev_read_timeout = @read_timeout
|
90
|
+
|
91
|
+
begin
|
92
|
+
# C-ext reads this directly, writer method will configure
|
93
|
+
# properly on the client but based on my read--this is good enough
|
94
|
+
# until we get https://github.com/brianmario/mysql2/pull/955 in
|
95
|
+
@read_timeout = temp_timeout
|
96
|
+
yield
|
97
|
+
ensure
|
98
|
+
@read_timeout = prev_read_timeout
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def query_whitelisted?(sql, *)
|
105
|
+
QUERY_WHITELIST =~ sql
|
106
|
+
rescue ArgumentError
|
107
|
+
# The above regexp match can fail if the input SQL string contains binary
|
108
|
+
# data that is not recognized as a valid encoding, in which case we just
|
109
|
+
# return false.
|
110
|
+
return false unless sql.valid_encoding?
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
|
114
|
+
def connect(*args)
|
115
|
+
acquire_semian_resource(adapter: :mysql, scope: :connection) { raw_connect(*args) }
|
116
|
+
end
|
117
|
+
|
118
|
+
def acquire_semian_resource(**)
|
119
|
+
super
|
120
|
+
rescue ::Mysql2::Error => error
|
121
|
+
if error.is_a?(PingFailure) || (!error.is_a?(::Mysql2::SemianError) && error.message.match?(CONNECTION_ERROR))
|
122
|
+
semian_resource.mark_failed(error)
|
123
|
+
error.semian_identifier = semian_identifier
|
124
|
+
end
|
125
|
+
raise
|
126
|
+
end
|
127
|
+
|
128
|
+
def raw_semian_options
|
129
|
+
return query_options[:semian] if query_options.key?(:semian)
|
130
|
+
return query_options['semian'.freeze] if query_options.key?('semian'.freeze)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
::Mysql2::Client.include(Semian::Mysql2)
|