expeditor 0.1.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/README.md +37 -0
- data/Rakefile +5 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/examples/example.rb +62 -0
- data/expeditor.gemspec +32 -0
- data/lib/expeditor.rb +7 -0
- data/lib/expeditor/bucket.rb +79 -0
- data/lib/expeditor/command.rb +242 -0
- data/lib/expeditor/errors.rb +13 -0
- data/lib/expeditor/rich_future.rb +61 -0
- data/lib/expeditor/service.rb +82 -0
- data/lib/expeditor/services.rb +14 -0
- data/lib/expeditor/services/default.rb +35 -0
- data/lib/expeditor/status.rb +47 -0
- data/lib/expeditor/version.rb +3 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 28e3d0c9d21f60030c7e591f6fd79c849357b428
|
4
|
+
data.tar.gz: e3fd9e317a4d5fcd3b27bb4f9d4b8c48a03dc436
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 51d080f2f60dde13877de518bdf1ad4893cc98bfa970e3e8ee9891e754445f3564c517beadcec923b62900c44ef97bb0906dcead8dd78d4f7c92f823f186f6c9
|
7
|
+
data.tar.gz: 95753cfcd208ebe0e29ed06a51c69bbf4589d3252e299748a83f13ed17aa326e3a1e68483bbf9b7211a50985309c30b8c8db9de4d7e79b351005495822351fb2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Expeditor
|
2
|
+
|
3
|
+
Expeditor is a Ruby library inspired by Netflix/Hystrix.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'expeditor'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install expeditor
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
|
+
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it ( https://github.com/[my-github-username]/expeditor/fork )
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
37
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "rystrix"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/examples/example.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'expeditor'
|
2
|
+
|
3
|
+
start_time = Time.now
|
4
|
+
|
5
|
+
# Create new service (it is containing a thread pool and circuit breaker function)
|
6
|
+
service = Expeditor::Service.new(
|
7
|
+
executor: Concurrent::ThreadPoolExecutor.new(
|
8
|
+
min_threads: 5, # minimum number of threads
|
9
|
+
max_threads: 5, # maximum number of threads
|
10
|
+
max_queue: 0, # max size of task queue (including executing threads)
|
11
|
+
),
|
12
|
+
non_break_count: 10, # max count of non break
|
13
|
+
threshold: 0.5, # failure rate to break (0.0 - 1.0)
|
14
|
+
)
|
15
|
+
|
16
|
+
# Create commands
|
17
|
+
command1 = Expeditor::Command.new(service: service) do
|
18
|
+
sleep 0.1
|
19
|
+
'command1'
|
20
|
+
end
|
21
|
+
|
22
|
+
command2 = Expeditor::Command.new(service: service, timeout: 0.5) do
|
23
|
+
sleep 1000
|
24
|
+
'command2'
|
25
|
+
end
|
26
|
+
# command2_d is command2 with fallback
|
27
|
+
command2_d = command2.with_fallback do |e|
|
28
|
+
'command2 fallback'
|
29
|
+
end
|
30
|
+
|
31
|
+
command3 = Expeditor::Command.new(
|
32
|
+
service: service,
|
33
|
+
dependencies: [command1, command2_d]
|
34
|
+
) do |v1, v2|
|
35
|
+
sleep 0.2
|
36
|
+
v1 + ', ' + v2
|
37
|
+
end
|
38
|
+
|
39
|
+
command4 = Expeditor::Command.new(
|
40
|
+
service: service,
|
41
|
+
dependencies: [command2, command3],
|
42
|
+
timeout: 1
|
43
|
+
) do |v2, v3|
|
44
|
+
sleep 0.3
|
45
|
+
v2 + ', ' + v3
|
46
|
+
end
|
47
|
+
command4_d = command4.with_fallback do
|
48
|
+
'command4 fallback'
|
49
|
+
end
|
50
|
+
|
51
|
+
# Start command (all dependencies of command4_d are executed. this is non blocking)
|
52
|
+
command4_d.start
|
53
|
+
|
54
|
+
puts Time.now - start_time #=> 0.00...
|
55
|
+
puts command1.get #=> command1
|
56
|
+
puts Time.now - start_time #=> 0.10...
|
57
|
+
puts command2_d.get #=> command2 fallback
|
58
|
+
puts Time.now - start_time #=> 0.50...
|
59
|
+
puts command4_d.get #=> command4 fallback
|
60
|
+
puts Time.now - start_time #=> 0.50...
|
61
|
+
puts command3.get #=> command3
|
62
|
+
puts Time.now - start_time #=> 0.70...
|
data/expeditor.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'expeditor/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "expeditor"
|
8
|
+
spec.version = Expeditor::VERSION
|
9
|
+
spec.authors = ["shohei-yasutake"]
|
10
|
+
spec.email = ["shohei-yasutake@cookpad.com"]
|
11
|
+
|
12
|
+
spec.summary = "Expeditor provides asynchronous execution and fault tolerance for microservices"
|
13
|
+
spec.description = "Expeditor provides asynchronous execution and fault tolerance for microservices"
|
14
|
+
spec.homepage = "https://github.com/cookpad/expeditor"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
if spec.respond_to?(:metadata)
|
22
|
+
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com' to prevent pushes to rubygems.org, or delete to allow pushes to any server."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.add_runtime_dependency "concurrent-ruby", "~> 0.8"
|
26
|
+
spec.add_runtime_dependency "concurrent-ruby-ext", "~> 0.8"
|
27
|
+
spec.add_runtime_dependency "retryable", "> 1.0"
|
28
|
+
|
29
|
+
spec.add_development_dependency "bundler", "~> 1.9"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
|
+
spec.add_development_dependency "rspec", "3.2.0"
|
32
|
+
end
|
data/lib/expeditor.rb
ADDED
@@ -0,0 +1,79 @@
|
|
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
|
@@ -0,0 +1,242 @@
|
|
1
|
+
require 'concurrent/ivar'
|
2
|
+
require 'concurrent/executor/safe_task_executor'
|
3
|
+
require 'concurrent/configuration'
|
4
|
+
require 'expeditor/errors'
|
5
|
+
require 'expeditor/rich_future'
|
6
|
+
require 'expeditor/service'
|
7
|
+
require 'expeditor/services'
|
8
|
+
require 'retryable'
|
9
|
+
require 'timeout'
|
10
|
+
|
11
|
+
module Expeditor
|
12
|
+
class Command
|
13
|
+
def initialize(opts = {}, &block)
|
14
|
+
@service = opts.fetch(:service, Expeditor::Services.default)
|
15
|
+
@timeout = opts[:timeout]
|
16
|
+
@dependencies = opts.fetch(:dependencies, [])
|
17
|
+
@normal_future = initial_normal(&block)
|
18
|
+
@fallback_var = nil
|
19
|
+
@retryable_options = Concurrent::IVar.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
if not started?
|
24
|
+
@dependencies.each(&:start)
|
25
|
+
@normal_future.safe_execute
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Equivalent to retryable gem options
|
31
|
+
def start_with_retry(retryable_options = {})
|
32
|
+
if not started?
|
33
|
+
@retryable_options.set(retryable_options)
|
34
|
+
start
|
35
|
+
end
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
def started?
|
40
|
+
@normal_future.executed?
|
41
|
+
end
|
42
|
+
|
43
|
+
def get
|
44
|
+
raise NotStartedError if not started?
|
45
|
+
@normal_future.get_or_else do
|
46
|
+
if @fallback_var
|
47
|
+
@fallback_var.wait
|
48
|
+
if @fallback_var.rejected?
|
49
|
+
raise @fallback_var.reason
|
50
|
+
else
|
51
|
+
@fallback_var.value
|
52
|
+
end
|
53
|
+
else
|
54
|
+
raise @normal_future.reason
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_fallback(&block)
|
60
|
+
command = self.clone
|
61
|
+
command.reset_fallback(&block)
|
62
|
+
command
|
63
|
+
end
|
64
|
+
|
65
|
+
def wait
|
66
|
+
raise NotStartedError if not started?
|
67
|
+
@normal_future.wait
|
68
|
+
@fallback_var.wait if @fallback_var
|
69
|
+
end
|
70
|
+
|
71
|
+
# command.on_complete do |success, value, reason|
|
72
|
+
# ...
|
73
|
+
# end
|
74
|
+
def on_complete(&block)
|
75
|
+
on do |_, value, reason|
|
76
|
+
block.call(reason == nil, value, reason)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# command.on_success do |value|
|
81
|
+
# ...
|
82
|
+
# end
|
83
|
+
def on_success(&block)
|
84
|
+
on do |_, value, reason|
|
85
|
+
block.call(value) unless reason
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# command.on_failure do |e|
|
90
|
+
# ...
|
91
|
+
# end
|
92
|
+
def on_failure(&block)
|
93
|
+
on do |_, _, reason|
|
94
|
+
block.call(reason) if reason
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# `chain` returns new command that has self as dependencies
|
99
|
+
def chain(opts = {}, &block)
|
100
|
+
opts[:dependencies] = [self]
|
101
|
+
Command.new(opts, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.const(value)
|
105
|
+
ConstCommand.new(value)
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.start(opts = {}, &block)
|
109
|
+
Command.new(opts, &block).start
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def reset_fallback(&block)
|
115
|
+
@fallback_var = Concurrent::IVar.new
|
116
|
+
@normal_future.add_observer do |_, value, reason|
|
117
|
+
if reason != nil
|
118
|
+
future = RichFuture.new(executor: Concurrent.configuration.global_task_pool) do
|
119
|
+
success, val, reason = Concurrent::SafeTaskExecutor.new(block, rescue_exception: true).execute(reason)
|
120
|
+
@fallback_var.complete(success, val, reason)
|
121
|
+
end
|
122
|
+
future.safe_execute
|
123
|
+
else
|
124
|
+
@fallback_var.complete(true, value, nil)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def breakable_block(args, &block)
|
132
|
+
if @service.open?
|
133
|
+
raise CircuitBreakError
|
134
|
+
else
|
135
|
+
block.call(*args)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def retryable_block(args, &block)
|
140
|
+
if @retryable_options.fulfilled?
|
141
|
+
Retryable.retryable(@retryable_options.value) do |retries, exception|
|
142
|
+
metrics(exception) if retries > 0
|
143
|
+
breakable_block(args, &block)
|
144
|
+
end
|
145
|
+
else
|
146
|
+
breakable_block(args, &block)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def timeout_block(args, &block)
|
151
|
+
if @timeout
|
152
|
+
Timeout::timeout(@timeout) do
|
153
|
+
retryable_block(args, &block)
|
154
|
+
end
|
155
|
+
else
|
156
|
+
retryable_block(args, &block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def metrics(reason)
|
161
|
+
case reason
|
162
|
+
when nil
|
163
|
+
@service.success
|
164
|
+
when Timeout::Error
|
165
|
+
@service.timeout
|
166
|
+
when RejectedExecutionError
|
167
|
+
@service.rejection
|
168
|
+
when CircuitBreakError
|
169
|
+
@service.break
|
170
|
+
when DependencyError
|
171
|
+
@service.dependency
|
172
|
+
else
|
173
|
+
@service.failure
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# timeout do
|
178
|
+
# retryable do
|
179
|
+
# circuit break do
|
180
|
+
# block.call
|
181
|
+
# end
|
182
|
+
# end
|
183
|
+
# end
|
184
|
+
def initial_normal(&block)
|
185
|
+
future = RichFuture.new(executor: @service.executor) do
|
186
|
+
args = wait_dependencies
|
187
|
+
timeout_block(args, &block)
|
188
|
+
end
|
189
|
+
future.add_observer do |_, _, reason|
|
190
|
+
metrics(reason)
|
191
|
+
end
|
192
|
+
future
|
193
|
+
end
|
194
|
+
|
195
|
+
def wait_dependencies
|
196
|
+
if @dependencies.count > 0
|
197
|
+
current = Thread.current
|
198
|
+
executor = Concurrent::ThreadPoolExecutor.new(
|
199
|
+
min_threads: 0,
|
200
|
+
max_threads: 5,
|
201
|
+
max_queue: 0,
|
202
|
+
)
|
203
|
+
error = Concurrent::IVar.new
|
204
|
+
error.add_observer do |_, e, _|
|
205
|
+
executor.shutdown
|
206
|
+
current.raise(DependencyError.new(e))
|
207
|
+
end
|
208
|
+
args = []
|
209
|
+
@dependencies.each_with_index do |c, i|
|
210
|
+
executor.post do
|
211
|
+
begin
|
212
|
+
args[i] = c.get
|
213
|
+
rescue => e
|
214
|
+
error.set(e)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
executor.shutdown
|
219
|
+
executor.wait_for_termination
|
220
|
+
args
|
221
|
+
else
|
222
|
+
[]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def on(&callback)
|
227
|
+
if @fallback_var
|
228
|
+
@fallback_var.add_observer(&callback)
|
229
|
+
else
|
230
|
+
@normal_future.add_observer(&callback)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
class ConstCommand < Command
|
235
|
+
def initialize(value)
|
236
|
+
@service = Expeditor::Services.default
|
237
|
+
@dependencies = []
|
238
|
+
@normal_future = RichFuture.new {}.set(value)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'concurrent/errors'
|
2
|
+
|
3
|
+
module Expeditor
|
4
|
+
NotStartedError = Class.new(StandardError)
|
5
|
+
RejectedExecutionError = Concurrent::RejectedExecutionError
|
6
|
+
CircuitBreakError = Class.new(StandardError)
|
7
|
+
class DependencyError < StandardError
|
8
|
+
attr :error
|
9
|
+
def initialize(e)
|
10
|
+
@error = e
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'concurrent/configuration'
|
2
|
+
require 'concurrent/future'
|
3
|
+
|
4
|
+
module Expeditor
|
5
|
+
class RichFuture < Concurrent::Future
|
6
|
+
def get
|
7
|
+
wait
|
8
|
+
if rejected?
|
9
|
+
raise reason
|
10
|
+
else
|
11
|
+
value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_or_else(&block)
|
16
|
+
wait
|
17
|
+
if rejected?
|
18
|
+
block.call
|
19
|
+
else
|
20
|
+
value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def set(v)
|
25
|
+
super(v)
|
26
|
+
end
|
27
|
+
|
28
|
+
def safe_set(v)
|
29
|
+
set(v) unless completed?
|
30
|
+
end
|
31
|
+
|
32
|
+
def fail(e)
|
33
|
+
super(e)
|
34
|
+
end
|
35
|
+
|
36
|
+
def safe_fail(e)
|
37
|
+
fail(e) unless completed?
|
38
|
+
end
|
39
|
+
|
40
|
+
def executed?
|
41
|
+
not unscheduled?
|
42
|
+
end
|
43
|
+
|
44
|
+
def safe_execute
|
45
|
+
begin
|
46
|
+
execute
|
47
|
+
rescue Exception => e
|
48
|
+
fail(e)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# This is workaround for concurrent-ruby's deadlock bug
|
55
|
+
# see: ruby-concurrency/concurrent-ruby#275
|
56
|
+
def work
|
57
|
+
success, val, reason = Concurrent::SafeTaskExecutor.new(@task, rescue_exception: true).execute(*@args)
|
58
|
+
complete(success, val, reason)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'concurrent/executor/thread_pool_executor'
|
2
|
+
|
3
|
+
module Expeditor
|
4
|
+
class Service
|
5
|
+
attr_reader :executor
|
6
|
+
|
7
|
+
def initialize(opts = {})
|
8
|
+
@executor = opts.fetch(:executor) { Concurrent::ThreadPoolExecutor.new }
|
9
|
+
@threshold = opts.fetch(:threshold, 0.5) # is 0.5 ok?
|
10
|
+
@non_break_count = opts.fetch(:non_break_count, 100) # is 100 ok?
|
11
|
+
@sleep = opts.fetch(:sleep, 1)
|
12
|
+
bucket_opts = {
|
13
|
+
size: 10,
|
14
|
+
per: opts.fetch(:period, 10).to_f / 10
|
15
|
+
}
|
16
|
+
@bucket = Expeditor::Bucket.new(bucket_opts)
|
17
|
+
@breaking = false
|
18
|
+
@break_start = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def success
|
22
|
+
@bucket.increment :success
|
23
|
+
end
|
24
|
+
|
25
|
+
def failure
|
26
|
+
@bucket.increment :failure
|
27
|
+
end
|
28
|
+
|
29
|
+
def rejection
|
30
|
+
@bucket.increment :rejection
|
31
|
+
end
|
32
|
+
|
33
|
+
def timeout
|
34
|
+
@bucket.increment :timeout
|
35
|
+
end
|
36
|
+
|
37
|
+
def break
|
38
|
+
@bucket.increment :break
|
39
|
+
end
|
40
|
+
|
41
|
+
def dependency
|
42
|
+
@bucket.increment :dependency
|
43
|
+
end
|
44
|
+
|
45
|
+
# break circuit?
|
46
|
+
def open?
|
47
|
+
if @breaking
|
48
|
+
if Time.now - @break_start > @sleep
|
49
|
+
@breaking = false
|
50
|
+
@break_start = nil
|
51
|
+
else
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
open = calc_open
|
56
|
+
if open
|
57
|
+
@breaking = true
|
58
|
+
@break_start = Time.now
|
59
|
+
end
|
60
|
+
open
|
61
|
+
end
|
62
|
+
|
63
|
+
# shutdown thread pool
|
64
|
+
# after shutdown, if you create thread, RejectedExecutionError is raised.
|
65
|
+
def shutdown
|
66
|
+
@executor.shutdown
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def calc_open
|
72
|
+
s = @bucket.total
|
73
|
+
total_count = s.success + s.failure + s.timeout
|
74
|
+
if total_count >= [@non_break_count, 1].max
|
75
|
+
failure_count = s.failure + s.timeout
|
76
|
+
failure_count.to_f / total_count.to_f >= @threshold
|
77
|
+
else
|
78
|
+
false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'concurrent/configuration'
|
2
|
+
require 'expeditor/services/default'
|
3
|
+
|
4
|
+
module Expeditor
|
5
|
+
module Services
|
6
|
+
DEFAULT_SERVICE = Expeditor::Services::Default.new
|
7
|
+
private_constant :DEFAULT_SERVICE
|
8
|
+
|
9
|
+
def default
|
10
|
+
DEFAULT_SERVICE
|
11
|
+
end
|
12
|
+
module_function :default
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'concurrent/configuration'
|
2
|
+
require 'expeditor/service'
|
3
|
+
|
4
|
+
module Expeditor
|
5
|
+
module Services
|
6
|
+
class Default < Expeditor::Service
|
7
|
+
def initialize
|
8
|
+
@executor = Concurrent.configuration.global_task_pool
|
9
|
+
@bucket = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def success
|
13
|
+
end
|
14
|
+
|
15
|
+
def failure
|
16
|
+
end
|
17
|
+
|
18
|
+
def rejection
|
19
|
+
end
|
20
|
+
|
21
|
+
def timeout
|
22
|
+
end
|
23
|
+
|
24
|
+
def break
|
25
|
+
end
|
26
|
+
|
27
|
+
def dependency
|
28
|
+
end
|
29
|
+
|
30
|
+
def open?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Expeditor
|
2
|
+
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
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
set(0, 0, 0, 0, 0, 0)
|
12
|
+
end
|
13
|
+
|
14
|
+
def increment(type)
|
15
|
+
case type
|
16
|
+
when :success
|
17
|
+
@success += 1
|
18
|
+
when :failure
|
19
|
+
@failure += 1
|
20
|
+
when :rejection
|
21
|
+
@rejection += 1
|
22
|
+
when :timeout
|
23
|
+
@timeout += 1
|
24
|
+
when :break
|
25
|
+
@break += 1
|
26
|
+
when :dependency
|
27
|
+
@dependency += 1
|
28
|
+
else
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def reset
|
33
|
+
set(0, 0, 0, 0, 0, 0)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def set(s, f, r, t, b, d)
|
39
|
+
@success = s
|
40
|
+
@failure = f
|
41
|
+
@rejection = r
|
42
|
+
@timeout = t
|
43
|
+
@break = b
|
44
|
+
@dependency = d
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: expeditor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- shohei-yasutake
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-04-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: concurrent-ruby
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: concurrent-ruby-ext
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.8'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.8'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: retryable
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>'
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>'
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 3.2.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.2.0
|
97
|
+
description: Expeditor provides asynchronous execution and fault tolerance for microservices
|
98
|
+
email:
|
99
|
+
- shohei-yasutake@cookpad.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- .gitignore
|
105
|
+
- .rspec
|
106
|
+
- .travis.yml
|
107
|
+
- Gemfile
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- bin/console
|
111
|
+
- bin/setup
|
112
|
+
- examples/example.rb
|
113
|
+
- expeditor.gemspec
|
114
|
+
- lib/expeditor.rb
|
115
|
+
- lib/expeditor/bucket.rb
|
116
|
+
- lib/expeditor/command.rb
|
117
|
+
- lib/expeditor/errors.rb
|
118
|
+
- lib/expeditor/rich_future.rb
|
119
|
+
- lib/expeditor/service.rb
|
120
|
+
- lib/expeditor/services.rb
|
121
|
+
- lib/expeditor/services/default.rb
|
122
|
+
- lib/expeditor/status.rb
|
123
|
+
- lib/expeditor/version.rb
|
124
|
+
homepage: https://github.com/cookpad/expeditor
|
125
|
+
licenses: []
|
126
|
+
metadata:
|
127
|
+
allowed_push_host: 'TODO: Set to ''http://mygemserver.com'' to prevent pushes to
|
128
|
+
rubygems.org, or delete to allow pushes to any server.'
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - '>='
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
+
requirements:
|
140
|
+
- - '>='
|
141
|
+
- !ruby/object:Gem::Version
|
142
|
+
version: '0'
|
143
|
+
requirements: []
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 2.0.14
|
146
|
+
signing_key:
|
147
|
+
specification_version: 4
|
148
|
+
summary: Expeditor provides asynchronous execution and fault tolerance for microservices
|
149
|
+
test_files: []
|