expeditor 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +7 -0
- data/Rakefile +6 -1
- data/expeditor.gemspec +3 -2
- data/lib/expeditor.rb +1 -1
- data/lib/expeditor/command.rb +74 -73
- data/lib/expeditor/ring_buffer.rb +76 -0
- data/lib/expeditor/rolling_number.rb +58 -0
- data/lib/expeditor/service.rb +64 -29
- data/lib/expeditor/services/default.rb +2 -2
- data/lib/expeditor/status.rb +25 -13
- data/lib/expeditor/version.rb +1 -1
- data/scripts/command_performance.rb +25 -0
- metadata +32 -16
- data/lib/expeditor/bucket.rb +0 -85
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0295f51ed41290a443f84e29704254cb92773e54'
|
4
|
+
data.tar.gz: d4525b17a023e7aed723ed8b5d40cc7842d14d6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c07b956a48818d56ae0edfc9f282c21ffa7e90ba3432b91d3abba8a095456281cdc6ce22a8a887e72c096b94525a12fa9ddf72929761644df05492f70d02ea7
|
7
|
+
data.tar.gz: 047f91f6875c87e815796801f5a67e49b4d1bcb1821a7c36c8e4835ae6c1cd0a0cfc7992d69a079b1afeb6ec9ad8e6ddf4dcbbfb1b06bbfe97476bc42dcafcf3
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## 0.7.0
|
2
|
+
- Add `gem 'concurrent-ruby-ext'` to your Gemfile if you want to use that gem.
|
3
|
+
- We should not depend on this in a gemspec [#27](https://github.com/cookpad/expeditor/pull/27)
|
4
|
+
- Fix possible race conditions.
|
5
|
+
- Fix bug on cutting passing size [#30](https://github.com/cookpad/expeditor/pull/30)
|
6
|
+
- Implement sleep feature on circuit breaker [#36](https://github.com/cookpad/expeditor/pull/36)
|
7
|
+
|
1
8
|
## 0.6.0
|
2
9
|
- Improve default configuration of circuit breaker [#25](https://github.com/cookpad/expeditor/pull/25)
|
3
10
|
- Default `non_break_count` is reduced from 100 to 20
|
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
|
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.rb
CHANGED
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,36 +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
|
-
if success
|
242
|
-
@ivar.set(value)
|
243
|
-
else
|
244
|
-
@ivar.fail(reason)
|
245
|
-
end
|
246
|
-
end
|
247
|
-
future.safe_execute
|
248
|
-
else
|
249
|
-
@ivar.fail(reason)
|
250
|
-
end
|
251
|
-
else # success
|
252
|
-
@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)
|
253
223
|
end
|
224
|
+
else
|
225
|
+
breakable_block(args, &block)
|
254
226
|
end
|
227
|
+
end
|
255
228
|
|
256
|
-
|
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)
|
257
258
|
end
|
258
259
|
|
259
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
11
|
@threshold = opts.fetch(:threshold, 0.5)
|
11
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
|
@@ -71,25 +97,27 @@ module Expeditor
|
|
71
97
|
end
|
72
98
|
|
73
99
|
def status
|
74
|
-
@
|
100
|
+
@rolling_number.total
|
75
101
|
end
|
76
102
|
|
77
|
-
# @deprecated
|
103
|
+
# @deprecated Use `#status` instead.
|
78
104
|
def current_status
|
79
|
-
warn 'Expeditor::Service#current_status is deprecated. Please use #status instead'
|
80
|
-
@
|
105
|
+
warn 'Expeditor::Service#current_status is deprecated. Please use #status instead.'
|
106
|
+
@rolling_number.current
|
81
107
|
end
|
82
108
|
|
83
109
|
def reset_status!
|
84
|
-
@
|
85
|
-
|
86
|
-
|
110
|
+
@mutex.synchronize do
|
111
|
+
@rolling_number = Expeditor::RollingNumber.new(@rolling_number_opts)
|
112
|
+
@breaking = false
|
113
|
+
@break_start = nil
|
114
|
+
end
|
87
115
|
end
|
88
116
|
|
89
117
|
private
|
90
118
|
|
91
119
|
def calc_open
|
92
|
-
s = @
|
120
|
+
s = @rolling_number.total
|
93
121
|
total_count = s.success + s.failure + s.timeout
|
94
122
|
if total_count >= [@non_break_count, 1].max
|
95
123
|
failure_count = s.failure + s.timeout
|
@@ -98,5 +126,12 @@ module Expeditor
|
|
98
126
|
false
|
99
127
|
end
|
100
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
|
101
136
|
end
|
102
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
@@ -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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- shohei-yasutake
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-05-08 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
|
@@ -115,15 +129,17 @@ files:
|
|
115
129
|
- examples/example.rb
|
116
130
|
- expeditor.gemspec
|
117
131
|
- lib/expeditor.rb
|
118
|
-
- lib/expeditor/bucket.rb
|
119
132
|
- lib/expeditor/command.rb
|
120
133
|
- lib/expeditor/errors.rb
|
121
134
|
- lib/expeditor/rich_future.rb
|
135
|
+
- lib/expeditor/ring_buffer.rb
|
136
|
+
- lib/expeditor/rolling_number.rb
|
122
137
|
- lib/expeditor/service.rb
|
123
138
|
- lib/expeditor/services.rb
|
124
139
|
- lib/expeditor/services/default.rb
|
125
140
|
- lib/expeditor/status.rb
|
126
141
|
- lib/expeditor/version.rb
|
142
|
+
- scripts/command_performance.rb
|
127
143
|
homepage: https://github.com/cookpad/expeditor
|
128
144
|
licenses:
|
129
145
|
- MIT
|
data/lib/expeditor/bucket.rb
DELETED
@@ -1,85 +0,0 @@
|
|
1
|
-
require 'expeditor/status'
|
2
|
-
|
3
|
-
module Expeditor
|
4
|
-
# Bucket is a data structure like circular buffer. It holds some status
|
5
|
-
# objects and it rolls statuses each `per` time (default is 1 second). Once
|
6
|
-
# it reaches the end of statuses array, it backs to start of statuses array
|
7
|
-
# and then reset the status and resumes recording. This is done so that the
|
8
|
-
# statistics are recorded gradually with short time interval rahter than
|
9
|
-
# reset all the record every wide time range (default is 10 seconds).
|
10
|
-
class Bucket
|
11
|
-
def initialize(opts = {})
|
12
|
-
@mutex = Mutex.new
|
13
|
-
@current_index = 0
|
14
|
-
@size = opts.fetch(:size, 10)
|
15
|
-
@per_time = opts.fetch(:per, 1)
|
16
|
-
@current_start = Time.now
|
17
|
-
@statuses = [].fill(0..(@size - 1)) do
|
18
|
-
Expeditor::Status.new
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
def increment(type)
|
23
|
-
@mutex.synchronize do
|
24
|
-
update
|
25
|
-
@statuses[@current_index].increment type
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def total
|
30
|
-
acc = @mutex.synchronize do
|
31
|
-
update
|
32
|
-
@statuses.inject([0, 0, 0, 0, 0, 0]) do |i, s|
|
33
|
-
i[0] += s.success
|
34
|
-
i[1] += s.failure
|
35
|
-
i[2] += s.rejection
|
36
|
-
i[3] += s.timeout
|
37
|
-
i[4] += s.break
|
38
|
-
i[5] += s.dependency
|
39
|
-
i
|
40
|
-
end
|
41
|
-
end
|
42
|
-
status = Expeditor::Status.new
|
43
|
-
status.success = acc[0]
|
44
|
-
status.failure = acc[1]
|
45
|
-
status.rejection = acc[2]
|
46
|
-
status.timeout = acc[3]
|
47
|
-
status.break = acc[4]
|
48
|
-
status.dependency = acc[5]
|
49
|
-
status
|
50
|
-
end
|
51
|
-
|
52
|
-
def current
|
53
|
-
@mutex.synchronize do
|
54
|
-
update
|
55
|
-
@statuses[@current_index]
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
def update
|
62
|
-
passing = last_passing
|
63
|
-
if passing > 0
|
64
|
-
@current_start = @current_start + @per_time * passing
|
65
|
-
passing = passing.div @size + @size if passing > 2 * @size
|
66
|
-
passing.times do
|
67
|
-
@current_index = next_index
|
68
|
-
@statuses[@current_index].reset
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def last_passing
|
74
|
-
(Time.now - @current_start).div @per_time
|
75
|
-
end
|
76
|
-
|
77
|
-
def next_index
|
78
|
-
if @current_index == @size - 1
|
79
|
-
0
|
80
|
-
else
|
81
|
-
@current_index + 1
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|