promise_pool 0.1.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|