semian 0.8.9 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ext/semian/resource.c +6 -0
- data/ext/semian/resource.h +4 -0
- data/ext/semian/semian.c +1 -0
- data/lib/semian.rb +33 -4
- data/lib/semian/circuit_breaker.rb +5 -0
- data/lib/semian/instrumentable.rb +6 -0
- data/lib/semian/lru_hash.rb +160 -0
- data/lib/semian/protected_resource.rb +6 -0
- data/lib/semian/resource.rb +5 -1
- data/lib/semian/unprotected_resource.rb +6 -0
- data/lib/semian/version.rb +1 -1
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 426aa6c4392e70e79cbf478827dcdccbc1a468f0f19b2a5e42b885584a389b00
|
4
|
+
data.tar.gz: df5c7c0ca8e80879c1d1d1a5cc46400c2de841e5809ea2396cc91941f222fa05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da18cc34e1c36d18904cbb584a9c7a03fd32afa976f27a6ed227b62e4ebb64065ea38a36ac951d0cade1c72b115143c3b24d6299b28de0927c78e52379205f17
|
7
|
+
data.tar.gz: 1e4dd82c16f7717022e65d4c722a28b4bd06490cbf347dfbe0f9fafd5652b615162f884a6dea04ad5ec5314986f7b0aad3abda8205cf2cc34b434416b001d0ed
|
data/ext/semian/resource.c
CHANGED
data/ext/semian/resource.h
CHANGED
data/ext/semian/semian.c
CHANGED
@@ -52,6 +52,7 @@ void Init_semian()
|
|
52
52
|
rb_define_method(cResource, "destroy", semian_resource_destroy, 0);
|
53
53
|
rb_define_method(cResource, "reset_registered_workers!", semian_resource_reset_workers, 0);
|
54
54
|
rb_define_method(cResource, "unregister_worker", semian_resource_unregister_worker, 0);
|
55
|
+
rb_define_method(cResource, "in_use?", semian_resource_in_use, 0);
|
55
56
|
|
56
57
|
id_wait_time = rb_intern("wait_time");
|
57
58
|
id_timeout = rb_intern("timeout");
|
data/lib/semian.rb
CHANGED
@@ -13,6 +13,7 @@ require 'semian/unprotected_resource'
|
|
13
13
|
require 'semian/simple_sliding_window'
|
14
14
|
require 'semian/simple_integer'
|
15
15
|
require 'semian/simple_state'
|
16
|
+
require 'semian/lru_hash'
|
16
17
|
|
17
18
|
#
|
18
19
|
# === Overview
|
@@ -89,6 +90,10 @@ module Semian
|
|
89
90
|
InternalError = Class.new(BaseError)
|
90
91
|
OpenCircuitError = Class.new(BaseError)
|
91
92
|
|
93
|
+
attr_accessor :maximum_lru_size, :minimum_lru_time
|
94
|
+
self.maximum_lru_size = 500
|
95
|
+
self.minimum_lru_time = 300
|
96
|
+
|
92
97
|
def issue_disabled_semaphores_warning
|
93
98
|
return if defined?(@warning_issued)
|
94
99
|
@warning_issued = true
|
@@ -216,7 +221,7 @@ module Semian
|
|
216
221
|
|
217
222
|
# Retrieves a hash of all registered resources.
|
218
223
|
def resources
|
219
|
-
@resources ||=
|
224
|
+
@resources ||= LRUHash.new
|
220
225
|
end
|
221
226
|
|
222
227
|
# Retrieves a hash of all registered resource consumers.
|
@@ -226,7 +231,16 @@ module Semian
|
|
226
231
|
|
227
232
|
def reset!
|
228
233
|
@consumers = {}
|
229
|
-
@resources =
|
234
|
+
@resources = LRUHash.new
|
235
|
+
end
|
236
|
+
|
237
|
+
def thread_safe?
|
238
|
+
return @thread_safe if defined?(@thread_safe)
|
239
|
+
@thread_safe = true
|
240
|
+
end
|
241
|
+
|
242
|
+
def thread_safe=(thread_safe)
|
243
|
+
@thread_safe = thread_safe
|
230
244
|
end
|
231
245
|
|
232
246
|
private
|
@@ -235,7 +249,6 @@ module Semian
|
|
235
249
|
circuit_breaker = options.fetch(:circuit_breaker, true)
|
236
250
|
return unless circuit_breaker
|
237
251
|
require_keys!([:success_threshold, :error_threshold, :error_timeout], options)
|
238
|
-
implementation = options[:thread_safety_disabled] ? ::Semian::Simple : ::Semian::ThreadSafe
|
239
252
|
|
240
253
|
exceptions = options[:exceptions] || []
|
241
254
|
CircuitBreaker.new(
|
@@ -245,10 +258,26 @@ module Semian
|
|
245
258
|
error_timeout: options[:error_timeout],
|
246
259
|
exceptions: Array(exceptions) + [::Semian::BaseError],
|
247
260
|
half_open_resource_timeout: options[:half_open_resource_timeout],
|
248
|
-
implementation: implementation,
|
261
|
+
implementation: implementation(**options),
|
249
262
|
)
|
250
263
|
end
|
251
264
|
|
265
|
+
def implementation(**options)
|
266
|
+
# thread_safety_disabled will be replaced by a global setting
|
267
|
+
# Semian is thread safe by default. It is possible
|
268
|
+
# to modify the value by using Semian.thread_safe=
|
269
|
+
unless options[:thread_safety_disabled].nil?
|
270
|
+
logger.info(
|
271
|
+
"NOTE: thread_safety_disabled will be replaced by a global setting" \
|
272
|
+
"Semian is thread safe by default. It is possible" \
|
273
|
+
"to modify the value by using Semian.thread_safe=",
|
274
|
+
)
|
275
|
+
end
|
276
|
+
|
277
|
+
thread_safe = options[:thread_safety_disabled].nil? ? Semian.thread_safe? : !options[:thread_safety_disabled]
|
278
|
+
thread_safe ? ::Semian::ThreadSafe : ::Semian::Simple
|
279
|
+
end
|
280
|
+
|
252
281
|
def create_bulkhead(name, **options)
|
253
282
|
bulkhead = options.fetch(:bulkhead, true)
|
254
283
|
return unless bulkhead
|
@@ -9,6 +9,12 @@ module Semian
|
|
9
9
|
subscribers.delete(name)
|
10
10
|
end
|
11
11
|
|
12
|
+
# Args:
|
13
|
+
# event (string)
|
14
|
+
# resource (Object)
|
15
|
+
# scope (string)
|
16
|
+
# adapter (string)
|
17
|
+
# payload (optional)
|
12
18
|
def notify(*args)
|
13
19
|
subscribers.values.each { |subscriber| subscriber.call(*args) }
|
14
20
|
end
|
@@ -0,0 +1,160 @@
|
|
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
|
+
extend Forwardable
|
9
|
+
def_delegators :@table, :size, :count, :empty?, :values
|
10
|
+
attr_reader :table
|
11
|
+
|
12
|
+
class NoopMutex
|
13
|
+
def synchronize(*)
|
14
|
+
yield
|
15
|
+
end
|
16
|
+
|
17
|
+
def try_lock
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def unlock
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def locked?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def owned?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def keys
|
35
|
+
@lock.synchronize { @table.keys }
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear
|
39
|
+
@lock.synchronize { @table.clear }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create an LRU hash
|
43
|
+
#
|
44
|
+
# Arguments:
|
45
|
+
# +max_size+ The maximum size of the table
|
46
|
+
# +min_time+ The minimum time a resource can live in the cache
|
47
|
+
#
|
48
|
+
# Note:
|
49
|
+
# The +min_time+ is a stronger guarantee than +max_size+. That is, if there are
|
50
|
+
# more than +max_size+ entries in the cache, but they've all been updated more
|
51
|
+
# recently than +min_time+, the garbage collection will not remove them and the
|
52
|
+
# cache can grow without bound. This usually means that you have many active
|
53
|
+
# circuits to disparate endpoints (or your circuit names are bad).
|
54
|
+
# If the max_size is 0, the garbage collection will be very aggressive and
|
55
|
+
# potentially computationally expensive.
|
56
|
+
def initialize(max_size: Semian.maximum_lru_size, min_time: Semian.minimum_lru_time)
|
57
|
+
@max_size = max_size
|
58
|
+
@min_time = min_time
|
59
|
+
@table = {}
|
60
|
+
@lock =
|
61
|
+
if Semian.thread_safe?
|
62
|
+
Mutex.new
|
63
|
+
else
|
64
|
+
NoopMutex.new
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def set(key, resource)
|
69
|
+
@lock.synchronize do
|
70
|
+
@table.delete(key)
|
71
|
+
@table[key] = resource
|
72
|
+
resource.updated_at = Time.now
|
73
|
+
end
|
74
|
+
clear_unused_resources if @table.length > @max_size
|
75
|
+
end
|
76
|
+
|
77
|
+
# This method uses the property that "Hashes enumerate their values in the
|
78
|
+
# order that the corresponding keys were inserted." Deleting a key and
|
79
|
+
# re-inserting it effectively moves it to the front of the cache.
|
80
|
+
# Update the `updated_at` field so we can use it later do decide if the
|
81
|
+
# resource is "in use".
|
82
|
+
def get(key)
|
83
|
+
@lock.synchronize do
|
84
|
+
found = @table.delete(key)
|
85
|
+
if found
|
86
|
+
@table[key] = found
|
87
|
+
found.updated_at = Time.now
|
88
|
+
end
|
89
|
+
found
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete(key)
|
94
|
+
@lock.synchronize do
|
95
|
+
@table.delete(key)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def []=(key, resource)
|
100
|
+
set(key, resource)
|
101
|
+
end
|
102
|
+
|
103
|
+
def [](key)
|
104
|
+
get(key)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def clear_unused_resources
|
110
|
+
payload = {
|
111
|
+
size: @table.size,
|
112
|
+
examined: 0,
|
113
|
+
cleared: 0,
|
114
|
+
elapsed: nil,
|
115
|
+
}
|
116
|
+
timer_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
117
|
+
|
118
|
+
ran = try_synchronize do
|
119
|
+
# Clears resources that have not been used in the last 5 minutes.
|
120
|
+
|
121
|
+
stop_time = Time.now - @min_time # Don't process resources updated after this time
|
122
|
+
@table.each do |_, resource|
|
123
|
+
payload[:examined] += 1
|
124
|
+
|
125
|
+
# The update times of the resources in the LRU are monotonically increasing,
|
126
|
+
# time, so we can stop looking once we find the first resource with an
|
127
|
+
# update time after the stop_time.
|
128
|
+
break if resource.updated_at > stop_time
|
129
|
+
|
130
|
+
next if resource.in_use?
|
131
|
+
|
132
|
+
resource = @table.delete(resource.name)
|
133
|
+
if resource
|
134
|
+
payload[:cleared] += 1
|
135
|
+
resource.destroy
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
if ran
|
141
|
+
payload[:elapsed] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - timer_start
|
142
|
+
Semian.notify(:lru_hash_gc, self, nil, nil, payload)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
EXCEPTION_NEVER = {Exception => :never}.freeze
|
147
|
+
EXCEPTION_IMMEDIATE = {Exception => :immediate}.freeze
|
148
|
+
private_constant :EXCEPTION_NEVER
|
149
|
+
private_constant :EXCEPTION_IMMEDIATE
|
150
|
+
|
151
|
+
def try_synchronize
|
152
|
+
Thread.handle_interrupt(EXCEPTION_NEVER) do
|
153
|
+
return false unless @lock.try_lock
|
154
|
+
Thread.handle_interrupt(EXCEPTION_IMMEDIATE) { yield }
|
155
|
+
true
|
156
|
+
ensure
|
157
|
+
@lock.unlock if @lock.owned?
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -7,11 +7,13 @@ module Semian
|
|
7
7
|
:open?, :closed?, :half_open?
|
8
8
|
|
9
9
|
attr_reader :bulkhead, :circuit_breaker, :name
|
10
|
+
attr_accessor :updated_at
|
10
11
|
|
11
12
|
def initialize(name, bulkhead, circuit_breaker)
|
12
13
|
@name = name
|
13
14
|
@bulkhead = bulkhead
|
14
15
|
@circuit_breaker = circuit_breaker
|
16
|
+
@updated_at = Time.now
|
15
17
|
end
|
16
18
|
|
17
19
|
def destroy
|
@@ -28,6 +30,10 @@ module Semian
|
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
33
|
+
def in_use?
|
34
|
+
circuit_breaker&.in_use? || bulkhead&.in_use?
|
35
|
+
end
|
36
|
+
|
31
37
|
private
|
32
38
|
|
33
39
|
def acquire_circuit_breaker(scope, adapter, resource)
|
data/lib/semian/resource.rb
CHANGED
@@ -5,7 +5,7 @@ module Semian
|
|
5
5
|
class << Semian::Resource
|
6
6
|
# Ensure that there can only be one resource of a given type
|
7
7
|
def instance(*args)
|
8
|
-
Semian.resources[args.first] ||= new(*args)
|
8
|
+
Semian.resources[args.first] ||= ProtectedResource.new(args.first, new(*args), nil)
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -51,5 +51,9 @@ module Semian
|
|
51
51
|
def key
|
52
52
|
'0x00000000'
|
53
53
|
end
|
54
|
+
|
55
|
+
def in_use?
|
56
|
+
false
|
57
|
+
end
|
54
58
|
end
|
55
59
|
end
|
@@ -3,9 +3,11 @@ module Semian
|
|
3
3
|
# the semian configuration of an `Adapter` is missing or explicitly disabled
|
4
4
|
class UnprotectedResource
|
5
5
|
attr_reader :name
|
6
|
+
attr_accessor :updated_at
|
6
7
|
|
7
8
|
def initialize(name)
|
8
9
|
@name = name
|
10
|
+
@updated_at = Time.now
|
9
11
|
end
|
10
12
|
|
11
13
|
def registered_workers
|
@@ -63,5 +65,9 @@ module Semian
|
|
63
65
|
def circuit_breaker
|
64
66
|
nil
|
65
67
|
end
|
68
|
+
|
69
|
+
def in_use?
|
70
|
+
true
|
71
|
+
end
|
66
72
|
end
|
67
73
|
end
|
data/lib/semian/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2019-09-
|
13
|
+
date: 2019-09-04 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rake-compiler
|
@@ -166,6 +166,34 @@ dependencies:
|
|
166
166
|
- - ">="
|
167
167
|
- !ruby/object:Gem::Version
|
168
168
|
version: '0'
|
169
|
+
- !ruby/object:Gem::Dependency
|
170
|
+
name: memory_profiler
|
171
|
+
requirement: !ruby/object:Gem::Requirement
|
172
|
+
requirements:
|
173
|
+
- - ">="
|
174
|
+
- !ruby/object:Gem::Version
|
175
|
+
version: '0'
|
176
|
+
type: :development
|
177
|
+
prerelease: false
|
178
|
+
version_requirements: !ruby/object:Gem::Requirement
|
179
|
+
requirements:
|
180
|
+
- - ">="
|
181
|
+
- !ruby/object:Gem::Version
|
182
|
+
version: '0'
|
183
|
+
- !ruby/object:Gem::Dependency
|
184
|
+
name: benchmark-memory
|
185
|
+
requirement: !ruby/object:Gem::Requirement
|
186
|
+
requirements:
|
187
|
+
- - ">="
|
188
|
+
- !ruby/object:Gem::Version
|
189
|
+
version: '0'
|
190
|
+
type: :development
|
191
|
+
prerelease: false
|
192
|
+
version_requirements: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
169
197
|
description: |2
|
170
198
|
A Ruby C extention that is used to control access to shared resources
|
171
199
|
across process boundaries with SysV semaphores.
|
@@ -190,6 +218,7 @@ files:
|
|
190
218
|
- lib/semian/circuit_breaker.rb
|
191
219
|
- lib/semian/grpc.rb
|
192
220
|
- lib/semian/instrumentable.rb
|
221
|
+
- lib/semian/lru_hash.rb
|
193
222
|
- lib/semian/mysql2.rb
|
194
223
|
- lib/semian/net_http.rb
|
195
224
|
- lib/semian/platform.rb
|