promise_pool 0.1.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +9 -0
- data/README.md +138 -3
- data/Rakefile +1 -1
- data/lib/promise_pool/future.rb +4 -0
- data/lib/promise_pool/promise.rb +31 -21
- data/lib/promise_pool/thread_pool.rb +1 -1
- data/lib/promise_pool/timer.rb +4 -4
- data/lib/promise_pool/version.rb +1 -1
- data/lib/promise_pool.rb +0 -1
- data/promise_pool.gemspec +12 -10
- data/test/test_future.rb +26 -0
- data/test/test_promise.rb +130 -22
- data/test/test_readme.rb +30 -0
- data/test/test_thread_pool.rb +50 -0
- data/test/test_timer.rb +60 -0
- metadata +12 -9
- data/lib/promise_pool/promise_eager.rb +0 -36
- data/test/test_pool.rb +0 -74
- data/test/test_promise_eager.rb +0 -41
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8cdae0e0cfd7f377206d08c5898bc2637a4eca77
|
4
|
+
data.tar.gz: a7a4f91c93b6b81fa4c1472772d445b43ff2764e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d0c873467132e5132e83ecb6f000baed9acc44c282fe02b39455f56a3b6db6222803f65e82e12501e5c7f717c683166bbcfb917a4101736610ceb43bbc98e9d
|
7
|
+
data.tar.gz: 1143bb7a1a4438909b3efe14471012638168cb920a18d00d6644aeafe0ad4652d6e904f33023b97adcc90b66a5c6652d56bb13f5c0714ea10cb88fc25d49e1f7
|
data/CHANGES.md
ADDED
data/README.md
CHANGED
@@ -10,15 +10,21 @@ by Lin Jen-Shin ([godfat](http://godfat.org))
|
|
10
10
|
|
11
11
|
## DESCRIPTION:
|
12
12
|
|
13
|
-
promise_pool
|
13
|
+
promise_pool is a promise implementation backed by threads or threads pool.
|
14
14
|
|
15
15
|
## FEATURES:
|
16
16
|
|
17
|
-
*
|
17
|
+
* PromisePool::Promise
|
18
|
+
* PromisePool::ThreadPool
|
19
|
+
* PromisePool::Timer
|
18
20
|
|
19
21
|
## WHY?
|
20
22
|
|
21
|
-
|
23
|
+
This was extracted from [rest-core][] because rest-core itself is getting
|
24
|
+
too complex, and extracting promises from it could greatly reduce complexity
|
25
|
+
and improve modularity for both rest-core and promise_pool.
|
26
|
+
|
27
|
+
* [rest-core]: https://github.com/godfat/rest-core
|
22
28
|
|
23
29
|
## REQUIREMENTS:
|
24
30
|
|
@@ -40,6 +46,135 @@ gem 'promise_pool', :git => 'git://github.com/godfat/promise_pool.git',
|
|
40
46
|
:submodules => true
|
41
47
|
```
|
42
48
|
|
49
|
+
## SYNOPSIS:
|
50
|
+
|
51
|
+
### Basic Usage
|
52
|
+
|
53
|
+
``` ruby
|
54
|
+
require 'promise_pool/promise'
|
55
|
+
promise = PromisePool::Promise.new
|
56
|
+
promise.defer do
|
57
|
+
sleep 1
|
58
|
+
puts "Doing works..."
|
59
|
+
sleep 1
|
60
|
+
"Done!"
|
61
|
+
end
|
62
|
+
puts "It's not blocking!"
|
63
|
+
puts promise.yield
|
64
|
+
```
|
65
|
+
|
66
|
+
Prints:
|
67
|
+
|
68
|
+
```
|
69
|
+
It's not blocking!
|
70
|
+
Doing works...
|
71
|
+
Done!
|
72
|
+
```
|
73
|
+
|
74
|
+
### Multiple Concurrent Promises
|
75
|
+
|
76
|
+
Doing multiple things at the same time, and wait for all of them at once.
|
77
|
+
|
78
|
+
``` ruby
|
79
|
+
require 'promise_pool/promise'
|
80
|
+
|
81
|
+
futures = 3.times.map do |i|
|
82
|
+
PromisePool::Promise.new.defer do
|
83
|
+
sleep i
|
84
|
+
i
|
85
|
+
end.future
|
86
|
+
end
|
87
|
+
|
88
|
+
futures.each(&method(:puts))
|
89
|
+
```
|
90
|
+
|
91
|
+
Prints:
|
92
|
+
|
93
|
+
```
|
94
|
+
0
|
95
|
+
1
|
96
|
+
2
|
97
|
+
```
|
98
|
+
|
99
|
+
### Error Handling
|
100
|
+
|
101
|
+
If an exception was raised in the `defer` block, it would propagate whenever
|
102
|
+
`yield` is called. Note that futures would implicitly call `yield` for you.
|
103
|
+
|
104
|
+
``` ruby
|
105
|
+
require 'promise_pool/promise'
|
106
|
+
|
107
|
+
future = PromisePool::Promise.new.defer do
|
108
|
+
raise 'nnf'
|
109
|
+
end.future
|
110
|
+
|
111
|
+
begin
|
112
|
+
future.to_s
|
113
|
+
never reached
|
114
|
+
rescue RuntimeError => e
|
115
|
+
puts e
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
Prints:
|
120
|
+
|
121
|
+
```
|
122
|
+
nnf
|
123
|
+
```
|
124
|
+
|
125
|
+
### PromisePool::ThreadPool
|
126
|
+
|
127
|
+
With a thread pool, we could throttle the process and avoid exhausting
|
128
|
+
resources whenever needed.
|
129
|
+
|
130
|
+
``` ruby
|
131
|
+
require 'promise_pool/promise'
|
132
|
+
require 'promise_pool/thread_pool'
|
133
|
+
|
134
|
+
pool = PromisePool::ThreadPool.new(10, 60) # max_size=10, idle_time=60
|
135
|
+
future = PromisePool::Promise.new.defer(pool) do
|
136
|
+
'Only process this whenever a worker is available.'
|
137
|
+
end.future
|
138
|
+
|
139
|
+
puts future
|
140
|
+
|
141
|
+
pool.shutdown # Make sure all the tasks are done in the pool before exit.
|
142
|
+
# You'll surely need this for shutting down gracefully.
|
143
|
+
```
|
144
|
+
|
145
|
+
Prints:
|
146
|
+
|
147
|
+
```
|
148
|
+
Only process this whenever a worker is available.
|
149
|
+
```
|
150
|
+
|
151
|
+
### PromisePool::Timer
|
152
|
+
|
153
|
+
If a task is taking too much time, we could time it out.
|
154
|
+
|
155
|
+
``` ruby
|
156
|
+
require 'promise_pool/promise'
|
157
|
+
require 'promise_pool/timer'
|
158
|
+
|
159
|
+
timer = PromisePool::Timer.new(1)
|
160
|
+
future = PromisePool::Promise.new(timer).defer do
|
161
|
+
sleep
|
162
|
+
never reached
|
163
|
+
end.future
|
164
|
+
|
165
|
+
begin
|
166
|
+
future.to_s
|
167
|
+
rescue PromisePool::Timer::Error => e
|
168
|
+
puts e.message
|
169
|
+
end
|
170
|
+
```
|
171
|
+
|
172
|
+
Prints:
|
173
|
+
|
174
|
+
```
|
175
|
+
execution expired
|
176
|
+
```
|
177
|
+
|
43
178
|
## CHANGES:
|
44
179
|
|
45
180
|
* [CHANGES](CHANGES.md)
|
data/Rakefile
CHANGED
data/lib/promise_pool/future.rb
CHANGED
data/lib/promise_pool/promise.rb
CHANGED
@@ -4,8 +4,9 @@ require 'promise_pool/future'
|
|
4
4
|
|
5
5
|
module PromisePool
|
6
6
|
class Promise
|
7
|
-
def self.claim value
|
7
|
+
def self.claim value, &callback
|
8
8
|
promise = new
|
9
|
+
promise.then(&callback) if block_given?
|
9
10
|
promise.fulfill(value)
|
10
11
|
promise
|
11
12
|
end
|
@@ -21,9 +22,9 @@ module PromisePool
|
|
21
22
|
|
22
23
|
def initialize timer=nil
|
23
24
|
self.value = self.error = self.result = nil
|
24
|
-
self.resolved =
|
25
|
+
self.resolved = false
|
26
|
+
self.callbacks = []
|
25
27
|
|
26
|
-
self.k = []
|
27
28
|
self.timer = timer
|
28
29
|
self.condv = ConditionVariable.new
|
29
30
|
self.mutex = Mutex.new
|
@@ -54,6 +55,7 @@ module PromisePool
|
|
54
55
|
def call
|
55
56
|
self.thread = Thread.current # set working thread
|
56
57
|
protected_yield{ yield } # avoid any exception and do the job
|
58
|
+
self
|
57
59
|
end
|
58
60
|
|
59
61
|
def future
|
@@ -69,7 +71,12 @@ module PromisePool
|
|
69
71
|
# called in client thread (from the future (e.g. body))
|
70
72
|
def yield
|
71
73
|
wait
|
72
|
-
|
74
|
+
case result
|
75
|
+
when Exception
|
76
|
+
raise result
|
77
|
+
else
|
78
|
+
result
|
79
|
+
end
|
73
80
|
end
|
74
81
|
|
75
82
|
# called in requesting thread after the request is done
|
@@ -84,7 +91,7 @@ module PromisePool
|
|
84
91
|
|
85
92
|
# append your actions, which would be called when we're calling back
|
86
93
|
def then &action
|
87
|
-
|
94
|
+
callbacks << action
|
88
95
|
self
|
89
96
|
end
|
90
97
|
|
@@ -93,24 +100,28 @@ module PromisePool
|
|
93
100
|
end
|
94
101
|
|
95
102
|
protected
|
96
|
-
attr_accessor :value, :error, :result, :resolved, :
|
97
|
-
:
|
103
|
+
attr_accessor :value, :error, :result, :resolved, :callbacks,
|
104
|
+
:timer, :condv, :mutex, :task, :thread
|
98
105
|
|
99
106
|
private
|
100
|
-
def fulfilling value
|
107
|
+
def fulfilling value # should be synchronized
|
101
108
|
self.value = value
|
102
109
|
resolve
|
103
110
|
end
|
104
111
|
|
105
|
-
def rejecting error
|
112
|
+
def rejecting error # should be synchronized
|
106
113
|
self.error = error
|
107
114
|
resolve
|
108
115
|
end
|
109
116
|
|
110
|
-
def resolve
|
111
|
-
self.
|
112
|
-
|
117
|
+
def resolve # should be synchronized
|
118
|
+
self.result = callbacks.inject(error || value){ |r, k| k.call(r) }
|
119
|
+
rescue Exception => err
|
120
|
+
self.class.set_backtrace(err)
|
121
|
+
self.result = err
|
122
|
+
log_callback_error(err)
|
113
123
|
ensure
|
124
|
+
self.resolved = true
|
114
125
|
condv.broadcast # client or response might be waiting
|
115
126
|
end
|
116
127
|
|
@@ -130,20 +141,12 @@ module PromisePool
|
|
130
141
|
|
131
142
|
def timeout_protected_yield
|
132
143
|
# timeout might already be set for thread_pool (pool_size > 0)
|
133
|
-
timer.on_timeout{ cancel_task } unless timer
|
144
|
+
timer.on_timeout{ cancel_task } unless timer.timer
|
134
145
|
yield
|
135
146
|
ensure
|
136
147
|
timer.cancel
|
137
148
|
end
|
138
149
|
|
139
|
-
# called in client thread, when yield is called
|
140
|
-
def callback
|
141
|
-
return result if called
|
142
|
-
self.result = k.inject(error || value){ |r, i| i.call(r) }
|
143
|
-
ensure
|
144
|
-
self.called = true
|
145
|
-
end
|
146
|
-
|
147
150
|
# timeout!
|
148
151
|
def cancel_task
|
149
152
|
mutex.synchronize do
|
@@ -159,5 +162,12 @@ module PromisePool
|
|
159
162
|
end
|
160
163
|
end
|
161
164
|
end
|
165
|
+
|
166
|
+
# log user callback error, should never raise
|
167
|
+
def log_callback_error err
|
168
|
+
warn "#{self.class}: ERROR: #{err}\n from #{err.backtrace.inspect}"
|
169
|
+
rescue Exception => e
|
170
|
+
Thread.main.raise(e) if !!$DEBUG
|
171
|
+
end
|
162
172
|
end
|
163
173
|
end
|
data/lib/promise_pool/timer.rb
CHANGED
@@ -4,6 +4,8 @@ require 'timers'
|
|
4
4
|
|
5
5
|
module PromisePool
|
6
6
|
class Timer
|
7
|
+
Error = Class.new(RuntimeError)
|
8
|
+
|
7
9
|
@mutex = Mutex.new
|
8
10
|
@interval = 1
|
9
11
|
|
@@ -31,16 +33,14 @@ module PromisePool
|
|
31
33
|
end
|
32
34
|
|
33
35
|
attr_accessor :timeout, :error, :timer
|
34
|
-
def initialize timeout, error
|
36
|
+
def initialize timeout, error=Error.new('execution expired')
|
35
37
|
self.timeout = timeout
|
36
38
|
self.error = error
|
37
|
-
self.block = block
|
38
|
-
start if block_given?
|
39
39
|
end
|
40
40
|
|
41
41
|
def on_timeout &block
|
42
42
|
self.block = block
|
43
|
-
start
|
43
|
+
start
|
44
44
|
end
|
45
45
|
|
46
46
|
# should never raise!
|
data/lib/promise_pool/version.rb
CHANGED
data/lib/promise_pool.rb
CHANGED
data/promise_pool.gemspec
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: promise_pool 0.
|
2
|
+
# stub: promise_pool 0.9.0 ruby lib
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "promise_pool"
|
6
|
-
s.version = "0.
|
6
|
+
s.version = "0.9.0"
|
7
7
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
9
9
|
s.require_paths = ["lib"]
|
10
10
|
s.authors = ["Lin Jen-Shin (godfat)"]
|
11
|
-
s.date = "2016-01-
|
12
|
-
s.description = "promise_pool"
|
11
|
+
s.date = "2016-01-29"
|
12
|
+
s.description = "promise_pool is a promise implementation backed by threads or threads pool."
|
13
13
|
s.email = ["godfat (XD) godfat.org"]
|
14
14
|
s.files = [
|
15
15
|
".gitignore",
|
16
16
|
".gitmodules",
|
17
17
|
".travis.yml",
|
18
|
+
"CHANGES.md",
|
18
19
|
"Gemfile",
|
19
20
|
"README.md",
|
20
21
|
"Rakefile",
|
21
22
|
"lib/promise_pool.rb",
|
22
23
|
"lib/promise_pool/future.rb",
|
23
24
|
"lib/promise_pool/promise.rb",
|
24
|
-
"lib/promise_pool/promise_eager.rb",
|
25
25
|
"lib/promise_pool/queue.rb",
|
26
26
|
"lib/promise_pool/task.rb",
|
27
27
|
"lib/promise_pool/test.rb",
|
@@ -31,18 +31,20 @@ Gem::Specification.new do |s|
|
|
31
31
|
"promise_pool.gemspec",
|
32
32
|
"task/README.md",
|
33
33
|
"task/gemgem.rb",
|
34
|
-
"test/
|
34
|
+
"test/test_future.rb",
|
35
35
|
"test/test_promise.rb",
|
36
|
-
"test/
|
36
|
+
"test/test_readme.rb",
|
37
|
+
"test/test_thread_pool.rb",
|
37
38
|
"test/test_timer.rb"]
|
38
39
|
s.homepage = "https://github.com/godfat/promise_pool"
|
39
40
|
s.licenses = ["Apache License 2.0"]
|
40
41
|
s.rubygems_version = "2.5.1"
|
41
|
-
s.summary = "promise_pool"
|
42
|
+
s.summary = "promise_pool is a promise implementation backed by threads or threads pool."
|
42
43
|
s.test_files = [
|
43
|
-
"test/
|
44
|
+
"test/test_future.rb",
|
44
45
|
"test/test_promise.rb",
|
45
|
-
"test/
|
46
|
+
"test/test_readme.rb",
|
47
|
+
"test/test_thread_pool.rb",
|
46
48
|
"test/test_timer.rb"]
|
47
49
|
|
48
50
|
if s.respond_to? :specification_version then
|
data/test/test_future.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
require 'promise_pool/test'
|
3
|
+
|
4
|
+
describe PromisePool::Future do
|
5
|
+
would 'return the value' do
|
6
|
+
Promise.new.defer{ 'value' }.future.should.eq 'value'
|
7
|
+
end
|
8
|
+
|
9
|
+
would 'raise an exception' do
|
10
|
+
expect.raise(RuntimeError) do
|
11
|
+
Promise.new.defer do
|
12
|
+
raise 'nnf'
|
13
|
+
end.future.oops
|
14
|
+
end.message.should.eq 'nnf'
|
15
|
+
end
|
16
|
+
|
17
|
+
would 'raise an exception if it is returning an exception' do
|
18
|
+
expect.raise(RuntimeError) do
|
19
|
+
Promise.new.defer{ RuntimeError.new('nnf') }.future.oops
|
20
|
+
end.message.should.eq 'nnf'
|
21
|
+
end
|
22
|
+
|
23
|
+
would 'respond_to_missing? properly' do
|
24
|
+
[Promise.new.defer{0}.future].flatten.first.should.eq 0
|
25
|
+
end
|
26
|
+
end
|
data/test/test_promise.rb
CHANGED
@@ -2,33 +2,141 @@
|
|
2
2
|
require 'promise_pool/test'
|
3
3
|
|
4
4
|
describe PromisePool::Promise do
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
describe 'claim' do
|
6
|
+
would 'without block' do
|
7
|
+
value = 'body'
|
8
|
+
Promise.claim(value).yield.should.eq value
|
9
|
+
end
|
10
|
+
|
11
|
+
would 'with block' do
|
12
|
+
value = 'body'
|
13
|
+
Promise.claim(value, &:reverse).yield.should.eq 'ydob'
|
14
|
+
end
|
8
15
|
end
|
9
16
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
17
|
+
describe 'then' do
|
18
|
+
describe 'with mock' do
|
19
|
+
after do
|
20
|
+
Muack.verify
|
21
|
+
end
|
22
|
+
|
23
|
+
would 'handle exceptions in callbacks' do
|
24
|
+
errors = []
|
25
|
+
promise = Promise.new.then(&errors.method(:<<))
|
26
|
+
|
27
|
+
mock(promise).warn(is_a(String)) do |msg|
|
28
|
+
msg.should.start_with?("PromisePool::Promise: ERROR: nnf\n")
|
29
|
+
end
|
30
|
+
|
31
|
+
promise.then do |es|
|
32
|
+
es.first.message.should.eq 'boom'
|
33
|
+
raise 'nnf'
|
34
|
+
end
|
35
|
+
|
36
|
+
promise.defer do
|
37
|
+
raise 'boom'
|
38
|
+
end.wait
|
39
|
+
|
40
|
+
errors.map(&:message).should.eq ['boom']
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
would 'then then then' do
|
45
|
+
plusone = lambda{ |r| r + 1 }
|
46
|
+
promise = Promise.new
|
47
|
+
2.times{ promise.then(&plusone).then(&plusone).then(&plusone) }
|
48
|
+
promise.fulfill(0)
|
49
|
+
promise.yield.should.eq 6
|
50
|
+
end
|
51
|
+
|
52
|
+
would 'transform to an exception and raise it' do
|
53
|
+
promise = Promise.new
|
54
|
+
promise.then(&RuntimeError.method(:new))
|
55
|
+
promise.defer{ 'nnf' }
|
56
|
+
expect.raise(RuntimeError) do
|
57
|
+
promise.yield
|
58
|
+
end.message.should.eq 'nnf'
|
59
|
+
end
|
16
60
|
end
|
17
61
|
|
18
|
-
|
19
|
-
|
62
|
+
describe 'call' do
|
63
|
+
would 'call in the current thread' do
|
64
|
+
promise = Promise.new
|
65
|
+
promise.call{ raise 'nnf' }
|
66
|
+
promise.send(:thread).should.eq Thread.current
|
67
|
+
promise.send(:task) .should.eq nil
|
68
|
+
promise.send(:error).message.should.eq 'nnf'
|
69
|
+
expect.raise(RuntimeError) do
|
70
|
+
promise.yield
|
71
|
+
end.message.should.eq 'nnf'
|
72
|
+
end
|
20
73
|
end
|
21
74
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
75
|
+
describe 'defer' do
|
76
|
+
describe 'with mock' do
|
77
|
+
after do
|
78
|
+
Muack.verify
|
79
|
+
end
|
80
|
+
|
81
|
+
would 'call in a new thread if no pool' do
|
82
|
+
thread = nil
|
83
|
+
rd, wr = IO.pipe
|
84
|
+
mock(Thread).new.with_any_args.peek_return do |t|
|
85
|
+
thread = t
|
86
|
+
wr.puts
|
87
|
+
end
|
88
|
+
Promise.new.defer do
|
89
|
+
rd.gets
|
90
|
+
Thread.current.should.eq thread
|
91
|
+
end.yield
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
would 'work, fulfill, yield' do
|
96
|
+
value = 'body'
|
97
|
+
flag = 0
|
98
|
+
promise = Promise.new.defer do
|
99
|
+
flag.should.eq 0
|
100
|
+
flag += 1
|
101
|
+
value
|
102
|
+
end
|
103
|
+
promise.yield.should.eq value
|
104
|
+
promise.send(:value).should.eq value
|
105
|
+
promise.send(:result).should.eq value
|
106
|
+
promise.should.resolved?
|
107
|
+
flag.should.eq 1
|
108
|
+
end
|
109
|
+
|
110
|
+
would 'work, reject, wait' do
|
111
|
+
flag = 0
|
112
|
+
promise = Promise.new.defer do
|
113
|
+
flag.should.eq 0
|
114
|
+
flag += 1
|
115
|
+
raise 'boom'
|
116
|
+
end
|
117
|
+
promise.wait
|
118
|
+
flag.should.eq 1
|
119
|
+
promise.send(:error).message.should.eq 'boom'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe 'wait' do
|
124
|
+
would 'broadcast to different threads' do
|
125
|
+
flag = 0
|
126
|
+
mutex = Mutex.new
|
127
|
+
promise = Promise.new
|
128
|
+
threads = 3.times.map do
|
129
|
+
Thread.new do
|
130
|
+
mutex.synchronize{ flag += 1 }
|
131
|
+
promise.wait
|
132
|
+
mutex.synchronize{ flag += 1 }
|
133
|
+
promise.yield
|
134
|
+
end
|
135
|
+
end
|
136
|
+
Thread.pass until flag == 3
|
137
|
+
promise.fulfill('ok')
|
138
|
+
Thread.pass until flag == 6
|
139
|
+
threads.map(&:value).should.eq %w[ok ok ok]
|
140
|
+
end
|
33
141
|
end
|
34
142
|
end
|
data/test/test_readme.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
require 'promise_pool/test'
|
3
|
+
|
4
|
+
describe 'README.md' do
|
5
|
+
readme = File.read("#{__dir__}/../README.md")
|
6
|
+
codes = readme.scan(
|
7
|
+
/### ([^\n]+).+?``` ruby\n(.+?)\n```\n\nPrints:\n\n```\n(.+?)```/m)
|
8
|
+
|
9
|
+
context = Class.new(Struct.new(:result)) do
|
10
|
+
def sleep sec=nil
|
11
|
+
if sec
|
12
|
+
Kernel.sleep(sec / 100.0)
|
13
|
+
else
|
14
|
+
Kernel.sleep
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def puts str
|
19
|
+
result << "#{str}\n"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
codes.each.with_index do |(title, code, test), index|
|
24
|
+
would "pass README.md #%02d #{title}" % index do
|
25
|
+
ctx = context.new([])
|
26
|
+
ctx.instance_eval(code, 'README.md', 0)
|
27
|
+
ctx.result.should.eq test.lines
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
require 'promise_pool/test'
|
3
|
+
|
4
|
+
describe PromisePool::ThreadPool do
|
5
|
+
before do
|
6
|
+
@pool = ThreadPool.new(1)
|
7
|
+
@promise = Promise.new
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
@pool.shutdown
|
12
|
+
@pool.size.should.eq 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def defer &block
|
16
|
+
@promise.defer(@pool, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
would 'use the pool if passed' do
|
20
|
+
@pool.size.should.eq 0
|
21
|
+
defer do
|
22
|
+
@pool.size
|
23
|
+
end.yield.should.eq 1
|
24
|
+
end
|
25
|
+
|
26
|
+
would 'call in thread pool if pool_size > 0' do
|
27
|
+
flag = 0
|
28
|
+
rd, wr = IO.pipe
|
29
|
+
defer do
|
30
|
+
rd.gets
|
31
|
+
flag.should.eq 0
|
32
|
+
flag += 1
|
33
|
+
raise 'nnf'
|
34
|
+
end
|
35
|
+
p1 = Promise.new
|
36
|
+
p1.defer(@pool) do # block until promise #0 is done because max_size == 1
|
37
|
+
flag.should.eq 1
|
38
|
+
flag += 1
|
39
|
+
raise 'boom'
|
40
|
+
end
|
41
|
+
wr.puts # start promise #0
|
42
|
+
|
43
|
+
# even if we're not yielding, the block should still be resolved,
|
44
|
+
# so there should not have any deadlock here.
|
45
|
+
expect.raise(RuntimeError){ p1.yield }.message.should.eq 'boom'
|
46
|
+
expect.raise(RuntimeError){ @promise.yield }.message.should.eq 'nnf'
|
47
|
+
|
48
|
+
flag.should.eq 2
|
49
|
+
end
|
50
|
+
end
|
data/test/test_timer.rb
CHANGED
@@ -2,4 +2,64 @@
|
|
2
2
|
require 'promise_pool/test'
|
3
3
|
|
4
4
|
describe PromisePool::Timer do
|
5
|
+
before do
|
6
|
+
@timer = Timer.new(0.01)
|
7
|
+
end
|
8
|
+
|
9
|
+
def expect_raise
|
10
|
+
expect.raise(@timer.error.class) do
|
11
|
+
yield
|
12
|
+
end.should.eq @timer.error
|
13
|
+
end
|
14
|
+
|
15
|
+
would 'raise timeout if task has not started' do
|
16
|
+
pool = ThreadPool.new(0)
|
17
|
+
expect_raise do
|
18
|
+
Promise.new(@timer).defer(pool) do
|
19
|
+
never called
|
20
|
+
end.yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'with flag' do
|
25
|
+
before do
|
26
|
+
@flag = false
|
27
|
+
end
|
28
|
+
|
29
|
+
after do
|
30
|
+
@flag.should.eq true
|
31
|
+
end
|
32
|
+
|
33
|
+
would 'raise timeout if the task started' do
|
34
|
+
pool = ThreadPool.new(1)
|
35
|
+
expect_raise do
|
36
|
+
Promise.new(@timer).defer(pool) do
|
37
|
+
@flag = true
|
38
|
+
sleep
|
39
|
+
never called
|
40
|
+
end.yield
|
41
|
+
end
|
42
|
+
pool.shutdown
|
43
|
+
end
|
44
|
+
|
45
|
+
would 'raise timeout in the thread' do
|
46
|
+
expect_raise do
|
47
|
+
Promise.new(@timer).defer do
|
48
|
+
@flag = true
|
49
|
+
sleep
|
50
|
+
never called
|
51
|
+
end.yield
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
would 'raise timeout even with inline call' do
|
56
|
+
expect_raise do
|
57
|
+
Promise.new(@timer).call do
|
58
|
+
@flag = true
|
59
|
+
sleep
|
60
|
+
never called
|
61
|
+
end.yield
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
5
65
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: promise_pool
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lin Jen-Shin (godfat)
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: timers
|
@@ -24,7 +24,8 @@ dependencies:
|
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 4.0.1
|
27
|
-
description: promise_pool
|
27
|
+
description: promise_pool is a promise implementation backed by threads or threads
|
28
|
+
pool.
|
28
29
|
email:
|
29
30
|
- godfat (XD) godfat.org
|
30
31
|
executables: []
|
@@ -34,13 +35,13 @@ files:
|
|
34
35
|
- ".gitignore"
|
35
36
|
- ".gitmodules"
|
36
37
|
- ".travis.yml"
|
38
|
+
- CHANGES.md
|
37
39
|
- Gemfile
|
38
40
|
- README.md
|
39
41
|
- Rakefile
|
40
42
|
- lib/promise_pool.rb
|
41
43
|
- lib/promise_pool/future.rb
|
42
44
|
- lib/promise_pool/promise.rb
|
43
|
-
- lib/promise_pool/promise_eager.rb
|
44
45
|
- lib/promise_pool/queue.rb
|
45
46
|
- lib/promise_pool/task.rb
|
46
47
|
- lib/promise_pool/test.rb
|
@@ -50,9 +51,10 @@ files:
|
|
50
51
|
- promise_pool.gemspec
|
51
52
|
- task/README.md
|
52
53
|
- task/gemgem.rb
|
53
|
-
- test/
|
54
|
+
- test/test_future.rb
|
54
55
|
- test/test_promise.rb
|
55
|
-
- test/
|
56
|
+
- test/test_readme.rb
|
57
|
+
- test/test_thread_pool.rb
|
56
58
|
- test/test_timer.rb
|
57
59
|
homepage: https://github.com/godfat/promise_pool
|
58
60
|
licenses:
|
@@ -77,9 +79,10 @@ rubyforge_project:
|
|
77
79
|
rubygems_version: 2.5.1
|
78
80
|
signing_key:
|
79
81
|
specification_version: 4
|
80
|
-
summary: promise_pool
|
82
|
+
summary: promise_pool is a promise implementation backed by threads or threads pool.
|
81
83
|
test_files:
|
82
|
-
- test/
|
84
|
+
- test/test_future.rb
|
83
85
|
- test/test_promise.rb
|
84
|
-
- test/
|
86
|
+
- test/test_readme.rb
|
87
|
+
- test/test_thread_pool.rb
|
85
88
|
- test/test_timer.rb
|
@@ -1,36 +0,0 @@
|
|
1
|
-
|
2
|
-
require 'promise_pool/promise'
|
3
|
-
|
4
|
-
module PromisePool
|
5
|
-
class PromiseEager < Promise
|
6
|
-
attr_accessor :error_callback
|
7
|
-
|
8
|
-
def initialize timer=nil, &error_callback
|
9
|
-
super(timer)
|
10
|
-
self.error_callback = error_callback
|
11
|
-
end
|
12
|
-
|
13
|
-
def resolved?
|
14
|
-
super && called
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
-
def resolve
|
19
|
-
super{ callback } # under ASYNC callback, should call immediately
|
20
|
-
rescue Exception => err
|
21
|
-
self.class.set_backtrace(err)
|
22
|
-
call_error_callback(err)
|
23
|
-
end
|
24
|
-
|
25
|
-
# log user callback error, should never raise
|
26
|
-
def call_error_callback err
|
27
|
-
if error_callback
|
28
|
-
error_callback.call(err)
|
29
|
-
else
|
30
|
-
warn "#{self.class}: ERROR: #{err}\n from #{err.backtrace.inspect}"
|
31
|
-
end
|
32
|
-
rescue Exception => e
|
33
|
-
Thread.main.raise(e) if !!$DEBUG
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
data/test/test_pool.rb
DELETED
@@ -1,74 +0,0 @@
|
|
1
|
-
|
2
|
-
require 'promise_pool/test'
|
3
|
-
|
4
|
-
describe PromisePool::ThreadPool do
|
5
|
-
before do
|
6
|
-
@pool = ThreadPool.new(3)
|
7
|
-
@promise = Promise.new
|
8
|
-
end
|
9
|
-
|
10
|
-
after do
|
11
|
-
@pool.shutdown
|
12
|
-
@pool.size.should.eq 0
|
13
|
-
end
|
14
|
-
|
15
|
-
would 'work, reject, yield' do
|
16
|
-
@pool.max_size = 1
|
17
|
-
flag = 0
|
18
|
-
@promise.defer(@pool) do
|
19
|
-
flag.should.eq 0
|
20
|
-
flag += 1
|
21
|
-
raise 'boom'
|
22
|
-
end.yield
|
23
|
-
flag.should.eq 1
|
24
|
-
@promise.send(:error).message.should.eq 'boom'
|
25
|
-
end
|
26
|
-
|
27
|
-
would 'work, fulfill, yield' do
|
28
|
-
value = 'body'
|
29
|
-
@pool.max_size = 2
|
30
|
-
flag = 0
|
31
|
-
@promise.defer(@pool) do
|
32
|
-
flag.should.eq 0
|
33
|
-
flag += 1
|
34
|
-
value
|
35
|
-
end
|
36
|
-
@promise.future.should.eq value
|
37
|
-
@promise.send(:value).should.eq value
|
38
|
-
@promise.send(:result).should.eq value
|
39
|
-
@promise.should.resolved?
|
40
|
-
flag.should.eq 1
|
41
|
-
end
|
42
|
-
|
43
|
-
would 'work, check body', :groups => [:only] do
|
44
|
-
flag = 0
|
45
|
-
result = @promise.defer(@pool) do
|
46
|
-
flag.should.eq 0
|
47
|
-
flag += 1
|
48
|
-
end.future
|
49
|
-
result.should.eq 1
|
50
|
-
flag.should.eq 1
|
51
|
-
end
|
52
|
-
|
53
|
-
would 'call in thread pool if pool_size > 0' do
|
54
|
-
@pool.max_size = 1
|
55
|
-
flag = 0
|
56
|
-
rd, wr = IO.pipe
|
57
|
-
@promise.defer(@pool) do
|
58
|
-
rd.gets
|
59
|
-
flag.should.eq 0
|
60
|
-
flag += 1
|
61
|
-
raise 'nnf'
|
62
|
-
end
|
63
|
-
p1 = Promise.new
|
64
|
-
p1.defer(@pool) do # block until promise #0 is done because max_size == 1
|
65
|
-
flag.should.eq 1
|
66
|
-
flag += 1
|
67
|
-
raise 'boom'
|
68
|
-
end
|
69
|
-
wr.puts # start promise #0
|
70
|
-
@promise.yield
|
71
|
-
p1.yield # block until promise #1 is done
|
72
|
-
flag.should.eq 2
|
73
|
-
end
|
74
|
-
end
|
data/test/test_promise_eager.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
|
2
|
-
require 'promise_pool/test'
|
3
|
-
|
4
|
-
describe PromisePool::PromiseEager do
|
5
|
-
would 'call error_callback on errors' do
|
6
|
-
errors = []
|
7
|
-
promise = PromiseEager.new(&errors.method(:<<))
|
8
|
-
|
9
|
-
promise.then do |err|
|
10
|
-
err.message.should.eq 'boom'
|
11
|
-
raise 'nnf'
|
12
|
-
end
|
13
|
-
|
14
|
-
promise.defer do
|
15
|
-
raise 'boom'
|
16
|
-
end.wait
|
17
|
-
|
18
|
-
errors.map(&:message).should.eq ['nnf']
|
19
|
-
end
|
20
|
-
|
21
|
-
after do
|
22
|
-
Muack.verify
|
23
|
-
end
|
24
|
-
|
25
|
-
would 'warn if there is no error_callback' do
|
26
|
-
promise = PromiseEager.new
|
27
|
-
|
28
|
-
mock(promise).warn(is_a(String)) do |msg|
|
29
|
-
msg.should.start_with?("PromisePool::PromiseEager: ERROR: nnf\n")
|
30
|
-
end
|
31
|
-
|
32
|
-
promise.then do |value|
|
33
|
-
value.should.eq 'value'
|
34
|
-
raise 'nnf'
|
35
|
-
end
|
36
|
-
|
37
|
-
promise.defer do
|
38
|
-
'value'
|
39
|
-
end.wait
|
40
|
-
end
|
41
|
-
end
|