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.
@@ -0,0 +1,117 @@
1
+ require 'semian/adapter'
2
+ require 'net/http'
3
+
4
+ module Net
5
+ ProtocolError.include(::Semian::AdapterError)
6
+
7
+ class SemianError < ::Net::ProtocolError
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 NetHTTP
20
+ include Semian::Adapter
21
+
22
+ ResourceBusyError = ::Net::ResourceBusyError
23
+ CircuitOpenError = ::Net::CircuitOpenError
24
+
25
+ class SemianConfigurationChangedError < RuntimeError
26
+ def initialize(msg = "Cannot re-initialize semian_configuration")
27
+ super
28
+ end
29
+ end
30
+
31
+ def semian_identifier
32
+ "nethttp_#{raw_semian_options[:name]}"
33
+ end
34
+
35
+ DEFAULT_ERRORS = [
36
+ ::Timeout::Error, # includes ::Net::ReadTimeout and ::Net::OpenTimeout
37
+ ::SocketError,
38
+ ::Net::HTTPBadResponse,
39
+ ::Net::HTTPHeaderSyntaxError,
40
+ ::Net::ProtocolError,
41
+ ::EOFError,
42
+ ::IOError,
43
+ ::SystemCallError, # includes ::Errno::EINVAL, ::Errno::ECONNRESET, ::Errno::ECONNREFUSED, ::Errno::ETIMEDOUT, and more
44
+ ].freeze # Net::HTTP can throw many different errors, this tries to capture most of them
45
+
46
+ class << self
47
+ attr_accessor :exceptions
48
+ attr_reader :semian_configuration
49
+
50
+ def semian_configuration=(configuration)
51
+ raise Semian::NetHTTP::SemianConfigurationChangedError unless @semian_configuration.nil?
52
+ @semian_configuration = configuration
53
+ end
54
+
55
+ def retrieve_semian_configuration(host, port)
56
+ @semian_configuration.call(host, port) if @semian_configuration.respond_to?(:call)
57
+ end
58
+
59
+ def reset_exceptions
60
+ self.exceptions = Semian::NetHTTP::DEFAULT_ERRORS.dup
61
+ end
62
+ end
63
+
64
+ Semian::NetHTTP.reset_exceptions
65
+
66
+ def raw_semian_options
67
+ @raw_semian_options ||= begin
68
+ @raw_semian_options = Semian::NetHTTP.retrieve_semian_configuration(address, port)
69
+ @raw_semian_options = @raw_semian_options.dup unless @raw_semian_options.nil?
70
+ end
71
+ end
72
+
73
+ def resource_exceptions
74
+ Semian::NetHTTP.exceptions
75
+ end
76
+
77
+ def disabled?
78
+ raw_semian_options.nil?
79
+ end
80
+
81
+ def connect
82
+ return super if disabled?
83
+ acquire_semian_resource(adapter: :http, scope: :connection) { super }
84
+ end
85
+
86
+ def transport_request(*)
87
+ return super if disabled?
88
+ acquire_semian_resource(adapter: :http, scope: :query) do
89
+ handle_error_responses(super)
90
+ end
91
+ end
92
+
93
+ def with_resource_timeout(timeout)
94
+ prev_read_timeout = read_timeout
95
+ prev_open_timeout = open_timeout
96
+ begin
97
+ self.read_timeout = timeout
98
+ self.open_timeout = timeout
99
+ yield
100
+ ensure
101
+ self.read_timeout = prev_read_timeout
102
+ self.open_timeout = prev_open_timeout
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def handle_error_responses(result)
109
+ if raw_semian_options.fetch(:open_circuit_server_errors, false)
110
+ semian_resource.mark_failed(result) if result.is_a?(::Net::HTTPServerError)
111
+ end
112
+ result
113
+ end
114
+ end
115
+ end
116
+
117
+ Net::HTTP.prepend(Semian::NetHTTP)
@@ -0,0 +1,16 @@
1
+ module Semian
2
+ extend self
3
+
4
+ # Determines if Semian supported on the current platform.
5
+ def sysv_semaphores_supported?
6
+ /linux/.match(RUBY_PLATFORM)
7
+ end
8
+
9
+ def semaphores_enabled?
10
+ !disabled? && sysv_semaphores_supported?
11
+ end
12
+
13
+ def disabled?
14
+ ENV['SEMIAN_SEMAPHORES_DISABLED'] || ENV['SEMIAN_DISABLED']
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ module Semian
2
+ class ProtectedResource
3
+ extend Forwardable
4
+
5
+ def_delegators :@bulkhead, :destroy, :count, :semid, :tickets, :registered_workers
6
+ def_delegators :@circuit_breaker, :reset, :mark_failed, :mark_success, :request_allowed?,
7
+ :open?, :closed?, :half_open?
8
+
9
+ attr_reader :bulkhead, :circuit_breaker, :name
10
+ attr_accessor :updated_at
11
+
12
+ def initialize(name, bulkhead, circuit_breaker)
13
+ @name = name
14
+ @bulkhead = bulkhead
15
+ @circuit_breaker = circuit_breaker
16
+ @updated_at = Time.now
17
+ end
18
+
19
+ def destroy
20
+ @bulkhead.destroy unless @bulkhead.nil?
21
+ @circuit_breaker.destroy unless @circuit_breaker.nil?
22
+ end
23
+
24
+ def acquire(timeout: nil, scope: nil, adapter: nil, resource: nil)
25
+ acquire_circuit_breaker(scope, adapter, resource) do
26
+ acquire_bulkhead(timeout, scope, adapter) do |_, wait_time|
27
+ Semian.notify(:success, self, scope, adapter, wait_time)
28
+ yield self
29
+ end
30
+ end
31
+ end
32
+
33
+ def in_use?
34
+ circuit_breaker&.in_use? || bulkhead&.in_use?
35
+ end
36
+
37
+ private
38
+
39
+ def acquire_circuit_breaker(scope, adapter, resource)
40
+ if @circuit_breaker.nil?
41
+ yield self
42
+ else
43
+ @circuit_breaker.acquire(resource) do
44
+ yield self
45
+ end
46
+ end
47
+ rescue ::Semian::OpenCircuitError
48
+ Semian.notify(:circuit_open, self, scope, adapter)
49
+ raise
50
+ end
51
+
52
+ def acquire_bulkhead(timeout, scope, adapter)
53
+ if @bulkhead.nil?
54
+ yield self, 0
55
+ else
56
+ @bulkhead.acquire(timeout: timeout) do |wait_time|
57
+ yield self, wait_time
58
+ end
59
+ end
60
+ rescue ::Semian::TimeoutError
61
+ Semian.notify(:busy, self, scope, adapter)
62
+ raise
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+
3
+ class ActiveRecord::ConnectionAdapters::AbstractAdapter
4
+ def semian_resource
5
+ @connection.semian_resource
6
+ end
7
+ end
@@ -0,0 +1,143 @@
1
+ require 'semian/adapter'
2
+ require 'redis'
3
+
4
+ class Redis
5
+ Redis::BaseConnectionError.include(::Semian::AdapterError)
6
+ ::Errno::EINVAL.include(::Semian::AdapterError)
7
+
8
+ class SemianError < Redis::BaseConnectionError
9
+ def initialize(semian_identifier, *args)
10
+ super(*args)
11
+ @semian_identifier = semian_identifier
12
+ end
13
+ end
14
+
15
+ class OutOfMemoryError < Redis::CommandError
16
+ include ::Semian::AdapterError
17
+ end
18
+
19
+ ResourceBusyError = Class.new(SemianError)
20
+ CircuitOpenError = Class.new(SemianError)
21
+ ResolveError = Class.new(SemianError)
22
+
23
+ alias_method :_original_initialize, :initialize
24
+
25
+ def initialize(*args, &block)
26
+ _original_initialize(*args, &block)
27
+
28
+ # This reference is necessary because during a `pipelined` block the client
29
+ # is replaced by an instance of `Redis::Pipeline` and there is no way to
30
+ # access the original client which references the Semian resource.
31
+ @original_client = _client
32
+ end
33
+
34
+ def semian_resource
35
+ @original_client.semian_resource
36
+ end
37
+
38
+ def semian_identifier
39
+ semian_resource.name
40
+ end
41
+
42
+ # Compatibility with old versions of the Redis gem
43
+ unless instance_methods.include?(:_client)
44
+ def _client
45
+ @client
46
+ end
47
+ end
48
+ end
49
+
50
+ module Semian
51
+ module Redis
52
+ include Semian::Adapter
53
+
54
+ ResourceBusyError = ::Redis::ResourceBusyError
55
+ CircuitOpenError = ::Redis::CircuitOpenError
56
+ ResolveError = ::Redis::ResolveError
57
+
58
+ # The naked methods are exposed as `raw_query` and `raw_connect` for instrumentation purpose
59
+ def self.included(base)
60
+ base.send(:alias_method, :raw_io, :io)
61
+ base.send(:remove_method, :io)
62
+
63
+ base.send(:alias_method, :raw_connect, :connect)
64
+ base.send(:remove_method, :connect)
65
+ end
66
+
67
+ def semian_identifier
68
+ @semian_identifier ||= begin
69
+ name = semian_options && semian_options[:name]
70
+ name ||= "#{location}/#{db}"
71
+ :"redis_#{name}"
72
+ end
73
+ end
74
+
75
+ def io(&block)
76
+ acquire_semian_resource(adapter: :redis, scope: :query) do
77
+ reply = raw_io(&block)
78
+ raise_if_out_of_memory(reply)
79
+ reply
80
+ end
81
+ end
82
+
83
+ def connect
84
+ acquire_semian_resource(adapter: :redis, scope: :connection) do
85
+ begin
86
+ raw_connect
87
+ rescue SocketError, RuntimeError => e
88
+ raise ResolveError.new(semian_identifier) if dns_resolve_failure?(e.cause || e)
89
+ raise
90
+ end
91
+ end
92
+ end
93
+
94
+ def with_resource_timeout(temp_timeout)
95
+ timeout = options[:timeout]
96
+ connect_timeout = options[:connect_timeout]
97
+ read_timeout = options[:read_timeout]
98
+ write_timeout = options[:write_timeout]
99
+
100
+ begin
101
+ connection.timeout = temp_timeout if connected?
102
+ options[:timeout] = Float(temp_timeout),
103
+ options[:connect_timeout] = Float(temp_timeout)
104
+ options[:read_timeout] = Float(temp_timeout)
105
+ options[:write_timeout] = Float(temp_timeout)
106
+ yield
107
+ ensure
108
+ options[:timeout] = timeout
109
+ options[:connect_timeout] = connect_timeout
110
+ options[:read_timeout] = read_timeout
111
+ options[:write_timeout] = write_timeout
112
+ connection.timeout = self.timeout if connected?
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def resource_exceptions
119
+ [
120
+ ::Redis::BaseConnectionError,
121
+ ::Errno::EINVAL, # Hiredis bug: https://github.com/redis/hiredis-rb/issues/21
122
+ ::Redis::OutOfMemoryError,
123
+ ]
124
+ end
125
+
126
+ def raw_semian_options
127
+ return options[:semian] if options.key?(:semian)
128
+ return options['semian'.freeze] if options.key?('semian'.freeze)
129
+ end
130
+
131
+ def raise_if_out_of_memory(reply)
132
+ return unless reply.is_a?(::Redis::CommandError)
133
+ return unless reply.message =~ /OOM command not allowed when used memory > 'maxmemory'\.\s*\z/
134
+ raise ::Redis::OutOfMemoryError.new(reply.message)
135
+ end
136
+
137
+ def dns_resolve_failure?(e)
138
+ e.to_s.match?(/(can't resolve)|(name or service not known)|(nodename nor servname provided, or not known)|(failure in name resolution)/i)
139
+ end
140
+ end
141
+ end
142
+
143
+ ::Redis::Client.include(Semian::Redis)
@@ -0,0 +1,65 @@
1
+ module Semian
2
+ class Resource #:nodoc:
3
+ attr_reader :tickets, :name
4
+
5
+ class << Semian::Resource
6
+ # Ensure that there can only be one resource of a given type
7
+ def instance(name, **kwargs)
8
+ Semian.resources[name] ||= ProtectedResource.new(name, new(name, **kwargs), nil)
9
+ end
10
+ end
11
+
12
+ def initialize(name, tickets: nil, quota: nil, permissions: Semian.default_permissions, timeout: 0)
13
+ unless name.is_a?(String) || name.is_a?(Symbol)
14
+ raise TypeError, "name must be a string or symbol, got: #{name.class}"
15
+ end
16
+
17
+ if Semian.semaphores_enabled?
18
+ if respond_to?(:initialize_semaphore)
19
+ initialize_semaphore("#{Semian.namespace}#{name}", tickets, quota, permissions, timeout)
20
+ end
21
+ else
22
+ Semian.issue_disabled_semaphores_warning
23
+ end
24
+ @name = name
25
+ end
26
+
27
+ def reset_registered_workers!
28
+ end
29
+
30
+ def destroy
31
+ end
32
+
33
+ def unregister_worker
34
+ end
35
+
36
+ def acquire(*)
37
+ wait_time = 0
38
+ yield wait_time
39
+ end
40
+
41
+ def count
42
+ 0
43
+ end
44
+
45
+ def tickets
46
+ 0
47
+ end
48
+
49
+ def registered_workers
50
+ 0
51
+ end
52
+
53
+ def semid
54
+ 0
55
+ end
56
+
57
+ def key
58
+ '0x00000000'
59
+ end
60
+
61
+ def in_use?
62
+ false
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,38 @@
1
+ require 'thread'
2
+
3
+ module Semian
4
+ module Simple
5
+ class Integer #:nodoc:
6
+ attr_accessor :value
7
+
8
+ def initialize
9
+ reset
10
+ end
11
+
12
+ def increment(val = 1)
13
+ @value += val
14
+ end
15
+
16
+ def reset
17
+ @value = 0
18
+ end
19
+
20
+ def destroy
21
+ reset
22
+ end
23
+ end
24
+ end
25
+
26
+ module ThreadSafe
27
+ class Integer < Simple::Integer
28
+ def initialize(*)
29
+ super
30
+ @lock = Mutex.new
31
+ end
32
+
33
+ def increment(*)
34
+ @lock.synchronize { super }
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,68 @@
1
+ require 'thread'
2
+
3
+ module Semian
4
+ module Simple
5
+ class SlidingWindow #:nodoc:
6
+ extend Forwardable
7
+
8
+ def_delegators :@window, :size, :last
9
+ attr_reader :max_size
10
+
11
+ # A sliding window is a structure that stores the most @max_size recent timestamps
12
+ # like this: if @max_size = 4, current time is 10, @window =[5,7,9,10].
13
+ # Another push of (11) at 11 sec would make @window [7,9,10,11], shifting off 5.
14
+
15
+ def initialize(max_size:)
16
+ @max_size = max_size
17
+ @window = []
18
+ end
19
+
20
+ def reject!(&block)
21
+ @window.reject!(&block)
22
+ end
23
+
24
+ def push(value)
25
+ resize_to(@max_size - 1) # make room
26
+ @window << value
27
+ self
28
+ end
29
+ alias_method :<<, :push
30
+
31
+ def clear
32
+ @window.clear
33
+ self
34
+ end
35
+ alias_method :destroy, :clear
36
+
37
+ private
38
+
39
+ def resize_to(size)
40
+ @window = @window.last(size) if @window.size >= size
41
+ end
42
+ end
43
+ end
44
+
45
+ module ThreadSafe
46
+ class SlidingWindow < Simple::SlidingWindow
47
+ def initialize(**)
48
+ super
49
+ @lock = Mutex.new
50
+ end
51
+
52
+ # #size, #last, and #clear are not wrapped in a mutex. For the first two,
53
+ # the worst-case is a thread-switch at a timing where they'd receive an
54
+ # out-of-date value--which could happen with a mutex as well.
55
+ #
56
+ # As for clear, it's an all or nothing operation. Doesn't matter if we
57
+ # have the lock or not.
58
+
59
+ def reject!(*)
60
+ @lock.synchronize { super }
61
+ end
62
+
63
+ def push(*)
64
+ @lock.synchronize { super }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,50 @@
1
+ module Semian
2
+ module Simple
3
+ class State #:nodoc:
4
+ def initialize
5
+ reset
6
+ end
7
+
8
+ attr_reader :value
9
+
10
+ def open?
11
+ value == :open
12
+ end
13
+
14
+ def closed?
15
+ value == :closed
16
+ end
17
+
18
+ def half_open?
19
+ value == :half_open
20
+ end
21
+
22
+ def open!
23
+ @value = :open
24
+ end
25
+
26
+ def close!
27
+ @value = :closed
28
+ end
29
+
30
+ def half_open!
31
+ @value = :half_open
32
+ end
33
+
34
+ def reset
35
+ close!
36
+ end
37
+
38
+ def destroy
39
+ reset
40
+ end
41
+ end
42
+ end
43
+
44
+ module ThreadSafe
45
+ class State < Simple::State
46
+ # These operations are already safe for a threaded environment since it's
47
+ # a simple assignment.
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,103 @@
1
+ require 'semian/adapter'
2
+ require 'typhoeus'
3
+ require 'uri'
4
+
5
+ module Typhoeus
6
+ Errors::TyphoeusError.include(::Semian::AdapterError)
7
+
8
+ class SemianError < ::Typhoeus::Errors::TyphoeusError
9
+ def initialize(semian_identifier, *args)
10
+ super(*args)
11
+ @semian_identifier = semian_identifier
12
+ end
13
+ end
14
+
15
+ ResourceBusyError = Class.new(SemianError)
16
+ CircuitOpenError = Class.new(SemianError)
17
+ end
18
+
19
+ module Semian
20
+ module Typhoeus
21
+ include Semian::Adapter
22
+
23
+ ResourceBusyError = ::Typhoeus::ResourceBusyError
24
+ CircuitOpenError = ::Typhoeus::CircuitOpenError
25
+
26
+ class SemianConfigurationChangedError < RuntimeError
27
+ def initialize(msg = "Cannot re-initialize semian_configuration")
28
+ super
29
+ end
30
+ end
31
+
32
+ def semian_identifier
33
+ "typhoeus_#{raw_semian_options[:name]}"
34
+ end
35
+
36
+ DEFAULT_ERRORS = [
37
+ ::Timeout::Error,
38
+ ::SocketError,
39
+ ::Typhoeus::Errors::NoStub,
40
+ ::Typhoeus::Errors::TyphoeusError,
41
+ ::EOFError,
42
+ ::IOError,
43
+ ::SystemCallError, # includes ::Errno::EINVAL, ::Errno::ECONNRESET, ::Errno::ECONNREFUSED, ::Errno::ETIMEDOUT, and more
44
+ ].freeze # Typhoeus can throw many different errors, this tries to capture most of them
45
+
46
+ class << self
47
+ attr_accessor :exceptions
48
+ attr_reader :semian_configuration
49
+
50
+ def semian_configuration=(configuration)
51
+ raise Semian::Typhoeus::SemianConfigurationChangedError unless @semian_configuration.nil?
52
+ @semian_configuration = configuration
53
+ end
54
+
55
+ def retrieve_semian_configuration(host, port)
56
+ @semian_configuration.call(host, port) if @semian_configuration.respond_to?(:call)
57
+ end
58
+
59
+ def reset_exceptions
60
+ self.exceptions = Semian::Typhoeus::DEFAULT_ERRORS.dup
61
+ end
62
+ end
63
+
64
+ Semian::Typhoeus.reset_exceptions
65
+
66
+ def raw_semian_options
67
+ @raw_semian_options ||= begin
68
+ uri = URI.parse(url)
69
+ @raw_semian_options = Semian::Typhoeus.retrieve_semian_configuration(uri.host, uri.port)
70
+ @raw_semian_options = @raw_semian_options.dup unless @raw_semian_options.nil?
71
+ end
72
+ end
73
+
74
+ def resource_exceptions
75
+ Semian::Typhoeus.exceptions
76
+ end
77
+
78
+ def disabled?
79
+ raw_semian_options.nil?
80
+ end
81
+
82
+ def run
83
+ return super if disabled?
84
+
85
+ acquire_semian_resource(adapter: :typhoeus, scope: :query) { handle_error_responses(super) }
86
+ end
87
+
88
+ private
89
+
90
+ def handle_error_responses(result)
91
+ semian_resource.mark_failed(TyphoeusError.new(result.return_message)) if !result.success?
92
+ result
93
+ end
94
+ end
95
+ end
96
+
97
+ class TyphoeusError < StandardError
98
+ def initialize(msg)
99
+ super(msg)
100
+ end
101
+ end
102
+
103
+ Typhoeus::Request.prepend(Semian::Typhoeus)