expeditor 0.5.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +6 -4
- data/CHANGELOG.md +15 -2
- data/README.md +21 -5
- data/Rakefile +6 -1
- data/examples/circuit_breaker.rb +27 -0
- data/expeditor.gemspec +3 -2
- data/lib/expeditor/command.rb +74 -69
- data/lib/expeditor/ring_buffer.rb +76 -0
- data/lib/expeditor/rolling_number.rb +58 -0
- data/lib/expeditor/service.rb +69 -28
- data/lib/expeditor/services/default.rb +2 -2
- data/lib/expeditor/status.rb +25 -13
- data/lib/expeditor/version.rb +1 -1
- data/lib/expeditor.rb +1 -1
- data/scripts/command_performance.rb +25 -0
- metadata +37 -21
- data/lib/expeditor/bucket.rb +0 -79
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a260bbe17a9db35fafb969bd979d87cfc74c7f9b23932567b7f6dd3f378c2a98
|
4
|
+
data.tar.gz: 044615f3a206c7948f60e6fbaeb9f9c055ac1320e016c165123ddf58ec4ff5fc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 76dbc470fe709056315dda8d7cdbf63bd8b0ada4a62f735cbb9a7ff87c70b80aff42cd6775036a0b82a23652730d06bb878335444ce69db31f6544dd5516f109
|
7
|
+
data.tar.gz: 39e8389e033b6a2e7d2c4af73105380e878d6426002d8644253e63a3c2315de1d9730b69f9d17c306aa2d31b883e585b0968ba151b52e46ec3c12421ddffbc29
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,20 @@
|
|
1
|
-
|
1
|
+
## 0.7.1
|
2
|
+
- Fix Ruby 2 style keyword arguments to support Ruby 3 [#45](https://github.com/cookpad/expeditor/pull/27)
|
2
3
|
|
3
|
-
## 0.
|
4
|
+
## 0.7.0
|
5
|
+
- Add `gem 'concurrent-ruby-ext'` to your Gemfile if you want to use that gem.
|
6
|
+
- We should not depend on this in a gemspec [#27](https://github.com/cookpad/expeditor/pull/27)
|
7
|
+
- Fix possible race conditions.
|
8
|
+
- Fix bug on cutting passing size [#30](https://github.com/cookpad/expeditor/pull/30)
|
9
|
+
- Implement sleep feature on circuit breaker [#36](https://github.com/cookpad/expeditor/pull/36)
|
10
|
+
|
11
|
+
## 0.6.0
|
12
|
+
- Improve default configuration of circuit breaker [#25](https://github.com/cookpad/expeditor/pull/25)
|
13
|
+
- Default `non_break_count` is reduced from 100 to 20
|
14
|
+
- Return proper status of service [#26](https://github.com/cookpad/expeditor/pull/26)
|
15
|
+
- Use `Expeditor::Service#status` instead of `#current_status`
|
4
16
|
|
17
|
+
## 0.5.0
|
5
18
|
- Add a `current_thread` option of `Expeditor::Command#start` method to execute a task on current thread [#13](https://github.com/cookpad/expeditor/pull/13)
|
6
19
|
- Drop support for MRI 2.0.x [#15](https://github.com/cookpad/expeditor/pull/15)
|
7
20
|
- Deprecate Expeditor::Command#with_fallback. Use `set_fallback` instead [#14](https://github.com/cookpad/expeditor/pull/14)
|
data/README.md
CHANGED
@@ -126,20 +126,25 @@ command = Expeditor::Command.new(service: service) do
|
|
126
126
|
...
|
127
127
|
end
|
128
128
|
|
129
|
-
service.
|
129
|
+
service.status
|
130
130
|
# => #<Expeditor::Status:0x007fdeeeb18468 @break=0, @dependency=0, @failure=0, @rejection=0, @success=0, @timeout=0>
|
131
131
|
|
132
132
|
service.reset_status! # reset status in the service
|
133
133
|
```
|
134
134
|
|
135
135
|
### circuit breaker
|
136
|
+
The circuit breaker needs a service metrics (success, failure, timeout, ...) to decide open the circuit or not.
|
137
|
+
Expeditor's circuit breaker has a few configuration for how it collects service metrics and how it opens the circuit.
|
138
|
+
|
139
|
+
For service metrics, Expeditor collects them with the given time window.
|
140
|
+
The metrics is gradually collected by breaking given time window into some peice of short time windows and resetting previous metrics when passing each short time window.
|
136
141
|
|
137
142
|
```ruby
|
138
143
|
service = Expeditor::Service.new(
|
139
|
-
|
140
|
-
sleep: 1, #
|
141
|
-
|
142
|
-
|
144
|
+
threshold: 0.5, # If the failure rate is more than or equal to threshold, the circuit will be opened.
|
145
|
+
sleep: 1, # If once the circuit is opened, the circuit is still open until sleep time seconds is passed even though failure rate is less than threshold.
|
146
|
+
non_break_count: 20, # If the total count of metrics is not more than non_break_count, the circuit is not opened even though failure rate is more than threshold.
|
147
|
+
period: 10, # Time window of collecting metrics (in seconds).
|
143
148
|
)
|
144
149
|
|
145
150
|
command = Expeditor::Command.new(service: service) do
|
@@ -147,6 +152,17 @@ command = Expeditor::Command.new(service: service) do
|
|
147
152
|
end
|
148
153
|
```
|
149
154
|
|
155
|
+
`non_break_count` is used to ignore requests to the service which is not frequentlly requested. Configure this value considering your estimated "requests per period to the service".
|
156
|
+
For example, when `period = 10` and `non_break_count = 20` and the requests do not occur more than 20 per 10 seconds, the circuit never opens because Expeditor ignores that "small number of requests".
|
157
|
+
If you don't ignore the failures in that case, set `non_break_count` to smaller value than `20`.
|
158
|
+
|
159
|
+
The default values are:
|
160
|
+
|
161
|
+
- threshold: 0.5
|
162
|
+
- sleep: 1
|
163
|
+
- non_break_count: 20
|
164
|
+
- period: 10
|
165
|
+
|
150
166
|
### synchronous execution
|
151
167
|
|
152
168
|
Use `current_thread` option of `#start`, command executes synchronous on current thread.
|
data/Rakefile
CHANGED
@@ -2,4 +2,9 @@ require "bundler/gem_tasks"
|
|
2
2
|
require "rspec/core/rake_task"
|
3
3
|
|
4
4
|
RSpec::Core::RakeTask.new(:spec)
|
5
|
-
task default: :spec
|
5
|
+
task default: [:spec, :performance_test]
|
6
|
+
|
7
|
+
desc 'Check performance'
|
8
|
+
task :performance_test do
|
9
|
+
ruby 'scripts/command_performance.rb'
|
10
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'expeditor'
|
2
|
+
|
3
|
+
service = Expeditor::Service.new
|
4
|
+
|
5
|
+
i = 1
|
6
|
+
loop do
|
7
|
+
puts '=' * 100
|
8
|
+
p i
|
9
|
+
|
10
|
+
command = Expeditor::Command.new(service: service, timeout: 1) {
|
11
|
+
sleep 0.001 # simulate remote resource access
|
12
|
+
if File.exist?('foo')
|
13
|
+
'result'
|
14
|
+
else
|
15
|
+
raise 'Demo error'
|
16
|
+
end
|
17
|
+
}.set_fallback { |e|
|
18
|
+
p e
|
19
|
+
'default value'
|
20
|
+
}.start
|
21
|
+
|
22
|
+
p command.get
|
23
|
+
p service.status
|
24
|
+
puts
|
25
|
+
|
26
|
+
i += 1
|
27
|
+
end
|
data/expeditor.gemspec
CHANGED
@@ -21,11 +21,12 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.required_ruby_version = '>= 2.1.0'
|
23
23
|
|
24
|
-
spec.add_runtime_dependency "concurrent-ruby", "
|
25
|
-
spec.add_runtime_dependency "concurrent-ruby-ext", "~> 1.0.0"
|
24
|
+
spec.add_runtime_dependency "concurrent-ruby", ">= 1.0.0"
|
26
25
|
spec.add_runtime_dependency "retryable", "> 1.0"
|
27
26
|
|
27
|
+
spec.add_development_dependency "benchmark-ips"
|
28
28
|
spec.add_development_dependency "bundler"
|
29
|
+
spec.add_development_dependency "concurrent-ruby-ext", ">= 1.0.0"
|
29
30
|
spec.add_development_dependency "rake"
|
30
31
|
spec.add_development_dependency "rspec", ">= 3.0.0"
|
31
32
|
end
|
data/lib/expeditor/command.rb
CHANGED
@@ -109,6 +109,9 @@ module Expeditor
|
|
109
109
|
end
|
110
110
|
end
|
111
111
|
|
112
|
+
# XXX: Raise ArgumentError when given `opts` has :dependencies
|
113
|
+
# because this forcefully change given :dependencies.
|
114
|
+
#
|
112
115
|
# `chain` returns new command that has self as dependencies
|
113
116
|
def chain(opts = {}, &block)
|
114
117
|
opts[:dependencies] = [self]
|
@@ -125,59 +128,37 @@ module Expeditor
|
|
125
128
|
|
126
129
|
private
|
127
130
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
def timeout_block(args, &block)
|
152
|
-
if @timeout
|
153
|
-
Timeout::timeout(@timeout) do
|
154
|
-
retryable_block(args, &block)
|
131
|
+
# set future
|
132
|
+
# set fallback future as an observer
|
133
|
+
# start dependencies
|
134
|
+
def prepare(executor = @service.executor)
|
135
|
+
@normal_future = initial_normal(executor, &@normal_block)
|
136
|
+
@normal_future.add_observer do |_, value, reason|
|
137
|
+
if reason # failure
|
138
|
+
if @fallback_block
|
139
|
+
future = RichFuture.new(executor: executor) do
|
140
|
+
success, value, reason = Concurrent::SafeTaskExecutor.new(@fallback_block, rescue_exception: true).execute(reason)
|
141
|
+
if success
|
142
|
+
@ivar.set(value)
|
143
|
+
else
|
144
|
+
@ivar.fail(reason)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
future.safe_execute
|
148
|
+
else
|
149
|
+
@ivar.fail(reason)
|
150
|
+
end
|
151
|
+
else # success
|
152
|
+
@ivar.set(value)
|
155
153
|
end
|
156
|
-
else
|
157
|
-
retryable_block(args, &block)
|
158
154
|
end
|
159
|
-
end
|
160
155
|
|
161
|
-
|
162
|
-
case reason
|
163
|
-
when nil
|
164
|
-
@service.success
|
165
|
-
when Timeout::Error
|
166
|
-
@service.timeout
|
167
|
-
when RejectedExecutionError
|
168
|
-
@service.rejection
|
169
|
-
when CircuitBreakError
|
170
|
-
@service.break
|
171
|
-
when DependencyError
|
172
|
-
@service.dependency
|
173
|
-
else
|
174
|
-
@service.failure
|
175
|
-
end
|
156
|
+
@dependencies.each(&:start)
|
176
157
|
end
|
177
158
|
|
178
|
-
#
|
179
|
-
#
|
180
|
-
#
|
159
|
+
# timeout_block do
|
160
|
+
# retryable_block do
|
161
|
+
# breakable_block do
|
181
162
|
# block.call
|
182
163
|
# end
|
183
164
|
# end
|
@@ -224,32 +205,56 @@ module Expeditor
|
|
224
205
|
end
|
225
206
|
end
|
226
207
|
|
227
|
-
def
|
228
|
-
@
|
208
|
+
def timeout_block(args, &block)
|
209
|
+
if @timeout
|
210
|
+
Timeout::timeout(@timeout) do
|
211
|
+
retryable_block(args, &block)
|
212
|
+
end
|
213
|
+
else
|
214
|
+
retryable_block(args, &block)
|
215
|
+
end
|
229
216
|
end
|
230
217
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
@normal_future.add_observer do |_, value, reason|
|
237
|
-
if reason # failure
|
238
|
-
if @fallback_block
|
239
|
-
future = RichFuture.new(executor: executor) do
|
240
|
-
success, value, reason = Concurrent::SafeTaskExecutor.new(@fallback_block, rescue_exception: true).execute(reason)
|
241
|
-
@ivar.send(:complete, success, value, reason)
|
242
|
-
end
|
243
|
-
future.safe_execute
|
244
|
-
else
|
245
|
-
@ivar.fail(reason)
|
246
|
-
end
|
247
|
-
else # success
|
248
|
-
@ivar.set(value)
|
218
|
+
def retryable_block(args, &block)
|
219
|
+
if @retryable_options.fulfilled?
|
220
|
+
Retryable.retryable(@retryable_options.value) do |retries, exception|
|
221
|
+
metrics(exception) if retries > 0
|
222
|
+
breakable_block(args, &block)
|
249
223
|
end
|
224
|
+
else
|
225
|
+
breakable_block(args, &block)
|
250
226
|
end
|
227
|
+
end
|
251
228
|
|
252
|
-
|
229
|
+
def breakable_block(args, &block)
|
230
|
+
@service.run_if_allowed do
|
231
|
+
block.call(*args)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def metrics(reason)
|
236
|
+
case reason
|
237
|
+
when nil
|
238
|
+
@service.success
|
239
|
+
when Timeout::Error
|
240
|
+
@service.timeout
|
241
|
+
when RejectedExecutionError
|
242
|
+
@service.rejection
|
243
|
+
when CircuitBreakError
|
244
|
+
@service.break
|
245
|
+
when DependencyError
|
246
|
+
@service.dependency
|
247
|
+
else
|
248
|
+
@service.failure
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def reset_fallback(&block)
|
253
|
+
@fallback_block = block
|
254
|
+
end
|
255
|
+
|
256
|
+
def on(&callback)
|
257
|
+
@ivar.add_observer(&callback)
|
253
258
|
end
|
254
259
|
|
255
260
|
class ConstCommand < Command
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Expeditor
|
2
|
+
# Circular buffer with user-defined initialization and optimized `move`
|
3
|
+
# implementation. The next element will be always initialized with
|
4
|
+
# user-defined initialization proc.
|
5
|
+
#
|
6
|
+
# Thread unsafe.
|
7
|
+
class RingBuffer
|
8
|
+
# @params [Integer] size
|
9
|
+
def initialize(size, &initialize_proc)
|
10
|
+
raise ArgumentError.new('initialize_proc is not given') unless initialize_proc
|
11
|
+
@size = size
|
12
|
+
@initialize_proc = initialize_proc
|
13
|
+
@elements = Array.new(@size, &initialize_proc)
|
14
|
+
@current_index = 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Object] user created object with given initialization proc.
|
18
|
+
def current
|
19
|
+
@elements[@current_index]
|
20
|
+
end
|
21
|
+
|
22
|
+
# @params [Integer] times How many elements will we pass.
|
23
|
+
# @return [Object] current element after moving.
|
24
|
+
def move(times)
|
25
|
+
cut_moving_time_if_possible(times).times do
|
26
|
+
next_element
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array<Object>] Array of elements.
|
31
|
+
def all
|
32
|
+
@elements
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# This logic is used for cutting moving times. When moving times is greater
|
38
|
+
# than statuses size, we can cut moving times to less than statuses size
|
39
|
+
# because the statuses are circulated.
|
40
|
+
#
|
41
|
+
# `*` is current index.
|
42
|
+
# When the statuses size is 3:
|
43
|
+
#
|
44
|
+
# [*, , ]
|
45
|
+
#
|
46
|
+
# Then when the moving times = 3, current index will be 0 (0-origin):
|
47
|
+
#
|
48
|
+
# [*, , ] -3> [ ,*, ] -2> [ , ,*] -1> [*, , ]
|
49
|
+
#
|
50
|
+
# Then moving times = 6, current index will be 0 again:
|
51
|
+
#
|
52
|
+
# [*, , ] -6> [ ,*, ] -5> [ , ,*] -4> [*, , ] -3> [ ,*, ] -2> [ , ,*] -1> [*, , ]
|
53
|
+
#
|
54
|
+
# In that case we can cut the moving times from 6 to 3.
|
55
|
+
# That is "cut moving times" here.
|
56
|
+
#
|
57
|
+
# TODO: We can write more optimized code which resets all elements with
|
58
|
+
# Array.new if given moving times is greater than `@size`.
|
59
|
+
def cut_moving_time_if_possible(times)
|
60
|
+
if times >= @size * 2
|
61
|
+
(times % @size) + @size
|
62
|
+
else
|
63
|
+
times
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def next_element
|
68
|
+
if @current_index == @size - 1
|
69
|
+
@current_index = 0
|
70
|
+
else
|
71
|
+
@current_index += 1
|
72
|
+
end
|
73
|
+
@elements[@current_index] = @initialize_proc.call
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'expeditor/status'
|
2
|
+
require 'expeditor/ring_buffer'
|
3
|
+
|
4
|
+
module Expeditor
|
5
|
+
# A RollingNumber holds some Status objects and it rolls statuses each
|
6
|
+
# `per_time` (default is 1 second). This is done so that the statistics are
|
7
|
+
# recorded gradually with short time interval rahter than reset all the
|
8
|
+
# record every wide time range (default is 10 seconds).
|
9
|
+
class RollingNumber
|
10
|
+
def initialize(size:, per_time:)
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@ring = RingBuffer.new(size) do
|
13
|
+
Expeditor::Status.new
|
14
|
+
end
|
15
|
+
@per_time = per_time
|
16
|
+
@current_start = Time.now
|
17
|
+
end
|
18
|
+
|
19
|
+
# @params [Symbol] type
|
20
|
+
def increment(type)
|
21
|
+
@mutex.synchronize do
|
22
|
+
update
|
23
|
+
@ring.current.increment(type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Expeditor::Status] Newly created status
|
28
|
+
def total
|
29
|
+
@mutex.synchronize do
|
30
|
+
update
|
31
|
+
@ring.all.inject(Expeditor::Status.new) {|i, s| i.merge!(s) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @deprecated Don't use, use `#total` instead.
|
36
|
+
def current
|
37
|
+
warn 'Expeditor::RollingNumber#current is deprecated. Please use #total instead to fetch correct status object.'
|
38
|
+
@mutex.synchronize do
|
39
|
+
update
|
40
|
+
@ring.current
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def update
|
47
|
+
passing = last_passing
|
48
|
+
if passing > 0
|
49
|
+
@current_start = @current_start + @per_time * passing
|
50
|
+
@ring.move(passing)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def last_passing
|
55
|
+
(Time.now - @current_start).div(@per_time)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/expeditor/service.rb
CHANGED
@@ -6,62 +6,88 @@ module Expeditor
|
|
6
6
|
attr_accessor :fallback_enabled
|
7
7
|
|
8
8
|
def initialize(opts = {})
|
9
|
+
@mutex = Mutex.new
|
9
10
|
@executor = opts.fetch(:executor) { Concurrent::ThreadPoolExecutor.new }
|
10
|
-
@threshold = opts.fetch(:threshold, 0.5)
|
11
|
-
@non_break_count = opts.fetch(:non_break_count,
|
11
|
+
@threshold = opts.fetch(:threshold, 0.5)
|
12
|
+
@non_break_count = opts.fetch(:non_break_count, 20)
|
12
13
|
@sleep = opts.fetch(:sleep, 1)
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
granularity = 10
|
15
|
+
@rolling_number_opts = {
|
16
|
+
size: granularity,
|
17
|
+
per_time: opts.fetch(:period, 10).to_f / granularity
|
16
18
|
}
|
17
19
|
reset_status!
|
18
20
|
@fallback_enabled = true
|
19
21
|
end
|
20
22
|
|
21
23
|
def success
|
22
|
-
@
|
24
|
+
@rolling_number.increment :success
|
23
25
|
end
|
24
26
|
|
25
27
|
def failure
|
26
|
-
@
|
28
|
+
@rolling_number.increment :failure
|
27
29
|
end
|
28
30
|
|
29
31
|
def rejection
|
30
|
-
@
|
32
|
+
@rolling_number.increment :rejection
|
31
33
|
end
|
32
34
|
|
33
35
|
def timeout
|
34
|
-
@
|
36
|
+
@rolling_number.increment :timeout
|
35
37
|
end
|
36
38
|
|
37
39
|
def break
|
38
|
-
@
|
40
|
+
@rolling_number.increment :break
|
39
41
|
end
|
40
42
|
|
41
43
|
def dependency
|
42
|
-
@
|
44
|
+
@rolling_number.increment :dependency
|
43
45
|
end
|
44
46
|
|
45
47
|
def fallback_enabled?
|
46
48
|
!!fallback_enabled
|
47
49
|
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
+
def breaking?
|
52
|
+
@breaking
|
53
|
+
end
|
54
|
+
|
55
|
+
# Run given block when the request is allowed, otherwise raise
|
56
|
+
# Expeditor::CircuitBreakError. When breaking and sleep time was passed,
|
57
|
+
# the circuit breaker tries to close the circuit. So subsequent single
|
58
|
+
# command execution is allowed (will not be breaked) to check the service
|
59
|
+
# is healthy or not. The circuit breaker only allows one request so other
|
60
|
+
# subsequent requests will be aborted with CircuitBreakError. When the test
|
61
|
+
# request succeeds, the circuit breaker resets the service status and
|
62
|
+
# closes the circuit.
|
63
|
+
def run_if_allowed
|
51
64
|
if @breaking
|
52
|
-
|
53
|
-
|
54
|
-
|
65
|
+
now = Time.now
|
66
|
+
|
67
|
+
# Only one thread can be allowed to execute single request when half-opened.
|
68
|
+
allow_single_request = false
|
69
|
+
@mutex.synchronize do
|
70
|
+
allow_single_request = now - @break_start > @sleep
|
71
|
+
@break_start = now if allow_single_request
|
72
|
+
end
|
73
|
+
|
74
|
+
if allow_single_request
|
75
|
+
result = yield # This can be raise exception.
|
76
|
+
# The execution succeed, then
|
77
|
+
reset_status!
|
78
|
+
result
|
55
79
|
else
|
56
|
-
|
80
|
+
raise CircuitBreakError
|
81
|
+
end
|
82
|
+
else
|
83
|
+
open = calc_open
|
84
|
+
if open
|
85
|
+
change_state(true, Time.now)
|
86
|
+
raise CircuitBreakError
|
87
|
+
else
|
88
|
+
yield
|
57
89
|
end
|
58
90
|
end
|
59
|
-
open = calc_open
|
60
|
-
if open
|
61
|
-
@breaking = true
|
62
|
-
@break_start = Time.now
|
63
|
-
end
|
64
|
-
open
|
65
91
|
end
|
66
92
|
|
67
93
|
# shutdown thread pool
|
@@ -70,20 +96,28 @@ module Expeditor
|
|
70
96
|
@executor.shutdown
|
71
97
|
end
|
72
98
|
|
99
|
+
def status
|
100
|
+
@rolling_number.total
|
101
|
+
end
|
102
|
+
|
103
|
+
# @deprecated Use `#status` instead.
|
73
104
|
def current_status
|
74
|
-
|
105
|
+
warn 'Expeditor::Service#current_status is deprecated. Please use #status instead.'
|
106
|
+
@rolling_number.current
|
75
107
|
end
|
76
108
|
|
77
109
|
def reset_status!
|
78
|
-
@
|
79
|
-
|
80
|
-
|
110
|
+
@mutex.synchronize do
|
111
|
+
@rolling_number = Expeditor::RollingNumber.new(**@rolling_number_opts)
|
112
|
+
@breaking = false
|
113
|
+
@break_start = nil
|
114
|
+
end
|
81
115
|
end
|
82
116
|
|
83
117
|
private
|
84
118
|
|
85
119
|
def calc_open
|
86
|
-
s = @
|
120
|
+
s = @rolling_number.total
|
87
121
|
total_count = s.success + s.failure + s.timeout
|
88
122
|
if total_count >= [@non_break_count, 1].max
|
89
123
|
failure_count = s.failure + s.timeout
|
@@ -92,5 +126,12 @@ module Expeditor
|
|
92
126
|
false
|
93
127
|
end
|
94
128
|
end
|
129
|
+
|
130
|
+
def change_state(breaking, break_start)
|
131
|
+
@mutex.synchronize do
|
132
|
+
@breaking = breaking
|
133
|
+
@break_start = break_start
|
134
|
+
end
|
135
|
+
end
|
95
136
|
end
|
96
137
|
end
|
data/lib/expeditor/status.rb
CHANGED
@@ -1,34 +1,46 @@
|
|
1
1
|
module Expeditor
|
2
|
+
# Thread unsafe.
|
2
3
|
class Status
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
attr_reader :success
|
5
|
+
attr_reader :failure
|
6
|
+
attr_reader :rejection
|
7
|
+
attr_reader :timeout
|
8
|
+
attr_reader :break
|
9
|
+
attr_reader :dependency
|
9
10
|
|
10
11
|
def initialize
|
11
12
|
set(0, 0, 0, 0, 0, 0)
|
12
13
|
end
|
13
14
|
|
14
|
-
def increment(type)
|
15
|
+
def increment(type, i = 1)
|
15
16
|
case type
|
16
17
|
when :success
|
17
|
-
@success +=
|
18
|
+
@success += i
|
18
19
|
when :failure
|
19
|
-
@failure +=
|
20
|
+
@failure += i
|
20
21
|
when :rejection
|
21
|
-
@rejection +=
|
22
|
+
@rejection += i
|
22
23
|
when :timeout
|
23
|
-
@timeout +=
|
24
|
+
@timeout += i
|
24
25
|
when :break
|
25
|
-
@break +=
|
26
|
+
@break += i
|
26
27
|
when :dependency
|
27
|
-
@dependency +=
|
28
|
+
@dependency += i
|
28
29
|
else
|
30
|
+
raise ArgumentError.new("Unknown type: #{type}")
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
34
|
+
def merge!(other)
|
35
|
+
increment(:success, other.success)
|
36
|
+
increment(:failure, other.failure)
|
37
|
+
increment(:rejection, other.rejection)
|
38
|
+
increment(:timeout, other.timeout)
|
39
|
+
increment(:break, other.break)
|
40
|
+
increment(:dependency, other.dependency)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
32
44
|
def reset
|
33
45
|
set(0, 0, 0, 0, 0, 0)
|
34
46
|
end
|
data/lib/expeditor/version.rb
CHANGED
data/lib/expeditor.rb
CHANGED
@@ -0,0 +1,25 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
2
|
+
require 'expeditor'
|
3
|
+
|
4
|
+
require 'benchmark/ips'
|
5
|
+
|
6
|
+
Benchmark.ips do |x|
|
7
|
+
x.report("simple command") do |i|
|
8
|
+
executor = Concurrent::ThreadPoolExecutor.new(min_threads: 100, max_threads: 100, max_queue: 100)
|
9
|
+
service = Expeditor::Service.new(period: 10, non_break_count: 0, threshold: 0.5, sleep: 1, executor: executor)
|
10
|
+
|
11
|
+
i.times do
|
12
|
+
commands = 10000.times.map do
|
13
|
+
Expeditor::Command.new { 1 }.start
|
14
|
+
end
|
15
|
+
command = Expeditor::Command.new(service: service, dependencies: commands) do |*vs|
|
16
|
+
vs.inject(0, &:+)
|
17
|
+
end.start
|
18
|
+
command.get
|
19
|
+
|
20
|
+
service.reset_status!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
x.compare!
|
25
|
+
end
|
metadata
CHANGED
@@ -1,57 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: expeditor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- shohei-yasutake
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: 1.0.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 1.0.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: retryable
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 1.0
|
33
|
+
version: '1.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 1.0
|
40
|
+
version: '1.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: benchmark-ips
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
-
type: :
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: bundler
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: concurrent-ruby-ext
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.0.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.0.0
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,23 +125,26 @@ files:
|
|
111
125
|
- Rakefile
|
112
126
|
- bin/console
|
113
127
|
- bin/setup
|
128
|
+
- examples/circuit_breaker.rb
|
114
129
|
- examples/example.rb
|
115
130
|
- expeditor.gemspec
|
116
131
|
- lib/expeditor.rb
|
117
|
-
- lib/expeditor/bucket.rb
|
118
132
|
- lib/expeditor/command.rb
|
119
133
|
- lib/expeditor/errors.rb
|
120
134
|
- lib/expeditor/rich_future.rb
|
135
|
+
- lib/expeditor/ring_buffer.rb
|
136
|
+
- lib/expeditor/rolling_number.rb
|
121
137
|
- lib/expeditor/service.rb
|
122
138
|
- lib/expeditor/services.rb
|
123
139
|
- lib/expeditor/services/default.rb
|
124
140
|
- lib/expeditor/status.rb
|
125
141
|
- lib/expeditor/version.rb
|
142
|
+
- scripts/command_performance.rb
|
126
143
|
homepage: https://github.com/cookpad/expeditor
|
127
144
|
licenses:
|
128
145
|
- MIT
|
129
146
|
metadata: {}
|
130
|
-
post_install_message:
|
147
|
+
post_install_message:
|
131
148
|
rdoc_options: []
|
132
149
|
require_paths:
|
133
150
|
- lib
|
@@ -142,9 +159,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
142
159
|
- !ruby/object:Gem::Version
|
143
160
|
version: '0'
|
144
161
|
requirements: []
|
145
|
-
|
146
|
-
|
147
|
-
signing_key:
|
162
|
+
rubygems_version: 3.3.8
|
163
|
+
signing_key:
|
148
164
|
specification_version: 4
|
149
165
|
summary: Expeditor provides asynchronous execution and fault tolerance for microservices
|
150
166
|
test_files: []
|
data/lib/expeditor/bucket.rb
DELETED
@@ -1,79 +0,0 @@
|
|
1
|
-
require 'expeditor/status'
|
2
|
-
|
3
|
-
module Expeditor
|
4
|
-
class Bucket
|
5
|
-
def initialize(opts = {})
|
6
|
-
@mutex = Mutex.new
|
7
|
-
@current_index = 0
|
8
|
-
@size = opts.fetch(:size, 10)
|
9
|
-
@per_time = opts.fetch(:per, 1)
|
10
|
-
@current_start = Time.now
|
11
|
-
@statuses = [].fill(0..(@size - 1)) do
|
12
|
-
Expeditor::Status.new
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def increment(type)
|
17
|
-
@mutex.synchronize do
|
18
|
-
update
|
19
|
-
@statuses[@current_index].increment type
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def total
|
24
|
-
acc = @mutex.synchronize do
|
25
|
-
update
|
26
|
-
@statuses.inject([0, 0, 0, 0, 0, 0]) do |acc, s|
|
27
|
-
acc[0] += s.success
|
28
|
-
acc[1] += s.failure
|
29
|
-
acc[2] += s.rejection
|
30
|
-
acc[3] += s.timeout
|
31
|
-
acc[4] += s.break
|
32
|
-
acc[5] += s.dependency
|
33
|
-
acc
|
34
|
-
end
|
35
|
-
end
|
36
|
-
status = Expeditor::Status.new
|
37
|
-
status.success = acc[0]
|
38
|
-
status.failure = acc[1]
|
39
|
-
status.rejection = acc[2]
|
40
|
-
status.timeout = acc[3]
|
41
|
-
status.break = acc[4]
|
42
|
-
status.dependency = acc[5]
|
43
|
-
status
|
44
|
-
end
|
45
|
-
|
46
|
-
def current
|
47
|
-
@mutex.synchronize do
|
48
|
-
update
|
49
|
-
@statuses[@current_index]
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
private
|
54
|
-
|
55
|
-
def update
|
56
|
-
passing = last_passing
|
57
|
-
if passing > 0
|
58
|
-
@current_start = @current_start + @per_time * passing
|
59
|
-
passing = passing.div @size + @size if passing > 2 * @size
|
60
|
-
passing.times do
|
61
|
-
@current_index = next_index
|
62
|
-
@statuses[@current_index].reset
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def last_passing
|
68
|
-
(Time.now - @current_start).div @per_time
|
69
|
-
end
|
70
|
-
|
71
|
-
def next_index
|
72
|
-
if @current_index == @size - 1
|
73
|
-
0
|
74
|
-
else
|
75
|
-
@current_index + 1
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|