expeditor 0.6.0 → 0.7.0
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 +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
|