expeditor 0.5.0 → 0.7.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 +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
|