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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5ee9877b9a720af63ec3a4163d680e43d734c4b7
4
- data.tar.gz: 8707da47bb651151d0c9a2306dcbe237e165cfe5
2
+ SHA256:
3
+ metadata.gz: a260bbe17a9db35fafb969bd979d87cfc74c7f9b23932567b7f6dd3f378c2a98
4
+ data.tar.gz: 044615f3a206c7948f60e6fbaeb9f9c055ac1320e016c165123ddf58ec4ff5fc
5
5
  SHA512:
6
- metadata.gz: 7889e85b9081df0c1aecc1039e0b2193224effe67d5169c6c100027714909b4616cb5c913a02d808bdf0e21669cdfd36ea22829dea1eb4bdc09df8dc0a8e95b5
7
- data.tar.gz: c36f0c9b174b2043de7da8628afa6379f655d38a57538d99669e5124e24eff5e067ed96ad209e0a25ddc70cdcdaf7171c3de21c9ffbcb99e85ef78c6015b3864
6
+ metadata.gz: 76dbc470fe709056315dda8d7cdbf63bd8b0ada4a62f735cbb9a7ff87c70b80aff42cd6775036a0b82a23652730d06bb878335444ce69db31f6544dd5516f109
7
+ data.tar.gz: 39e8389e033b6a2e7d2c4af73105380e878d6426002d8644253e63a3c2315de1d9730b69f9d17c306aa2d31b883e585b0968ba151b52e46ec3c12421ddffbc29
data/.gitignore CHANGED
@@ -8,3 +8,4 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /vendor/
11
+ .ruby-version
data/.travis.yml CHANGED
@@ -1,10 +1,12 @@
1
1
  language: ruby
2
- sudo: false
3
2
  cache: bundler
4
3
  rvm:
5
- - 2.1
6
- - 2.2
7
- - 2.3.0
4
+ - 2.1.10
5
+ - 2.2.10
6
+ - 2.3.8
7
+ - 2.4.6
8
+ - 2.5.5
9
+ - 2.6.2
8
10
  - ruby-head
9
11
  matrix:
10
12
  allow_failures:
data/CHANGELOG.md CHANGED
@@ -1,7 +1,20 @@
1
- <!-- ## Master (unreleased) -->
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.5.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.current_status
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
- period: 10, # retention period of the service metrics (success, failure, timeout, ...)
140
- sleep: 1, # if once the circuit is opened, the circuit is still open until sleep time is passed even though failure rate is less than threshold
141
- threshold: 0.5, # if the failure rate is more than or equal to threshold, the circuit is opened
142
- non_break_count: 100 # 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
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", "~> 1.0.0"
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
@@ -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
- def reset_fallback(&block)
129
- @fallback_block = block
130
- end
131
-
132
- def breakable_block(args, &block)
133
- if @service.open?
134
- raise CircuitBreakError
135
- else
136
- block.call(*args)
137
- end
138
- end
139
-
140
- def retryable_block(args, &block)
141
- if @retryable_options.fulfilled?
142
- Retryable.retryable(@retryable_options.value) do |retries, exception|
143
- metrics(exception) if retries > 0
144
- breakable_block(args, &block)
145
- end
146
- else
147
- breakable_block(args, &block)
148
- end
149
- end
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
- def metrics(reason)
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
- # timeout do
179
- # retryable do
180
- # circuit break do
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 on(&callback)
228
- @ivar.add_observer(&callback)
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
- # set future
232
- # set fallback future as an observer
233
- # start dependencies
234
- def prepare(executor = @service.executor)
235
- @normal_future = initial_normal(executor, &@normal_block)
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
- @dependencies.each(&:start)
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
@@ -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) # is 0.5 ok?
11
- @non_break_count = opts.fetch(:non_break_count, 100) # is 100 ok?
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
- @bucket_opts = {
14
- size: 10,
15
- per: opts.fetch(:period, 10).to_f / 10
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
- @bucket.increment :success
24
+ @rolling_number.increment :success
23
25
  end
24
26
 
25
27
  def failure
26
- @bucket.increment :failure
28
+ @rolling_number.increment :failure
27
29
  end
28
30
 
29
31
  def rejection
30
- @bucket.increment :rejection
32
+ @rolling_number.increment :rejection
31
33
  end
32
34
 
33
35
  def timeout
34
- @bucket.increment :timeout
36
+ @rolling_number.increment :timeout
35
37
  end
36
38
 
37
39
  def break
38
- @bucket.increment :break
40
+ @rolling_number.increment :break
39
41
  end
40
42
 
41
43
  def dependency
42
- @bucket.increment :dependency
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
- # break circuit?
50
- def open?
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
- if Time.now - @break_start > @sleep
53
- @breaking = false
54
- @break_start = nil
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
- return true
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
- @bucket.current
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
- @bucket = Expeditor::Bucket.new(@bucket_opts)
79
- @breaking = false
80
- @break_start = nil
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 = @bucket.total
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
@@ -28,8 +28,8 @@ module Expeditor
28
28
  def dependency
29
29
  end
30
30
 
31
- def open?
32
- false
31
+ def run_if_allowed
32
+ yield
33
33
  end
34
34
  end
35
35
  end
@@ -1,34 +1,46 @@
1
1
  module Expeditor
2
+ # Thread unsafe.
2
3
  class Status
3
- attr_accessor :success
4
- attr_accessor :failure
5
- attr_accessor :rejection
6
- attr_accessor :timeout
7
- attr_accessor :break
8
- attr_accessor :dependency
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 += 1
18
+ @success += i
18
19
  when :failure
19
- @failure += 1
20
+ @failure += i
20
21
  when :rejection
21
- @rejection += 1
22
+ @rejection += i
22
23
  when :timeout
23
- @timeout += 1
24
+ @timeout += i
24
25
  when :break
25
- @break += 1
26
+ @break += i
26
27
  when :dependency
27
- @dependency += 1
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
@@ -1,3 +1,3 @@
1
1
  module Expeditor
2
- VERSION = "0.5.0"
2
+ VERSION = "0.7.1"
3
3
  end
data/lib/expeditor.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'expeditor/bucket'
1
+ require 'expeditor/rolling_number'
2
2
  require 'expeditor/command'
3
3
  require 'expeditor/errors'
4
4
  require 'expeditor/rich_future'
@@ -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.5.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: 2016-08-29 00:00:00.000000000 Z
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: concurrent-ruby-ext
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.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.0
40
+ version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: retryable
42
+ name: benchmark-ips
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.0'
48
- type: :runtime
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: '1.0'
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
- rubyforge_project:
146
- rubygems_version: 2.5.1
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: []
@@ -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