expeditor 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|