ione 1.0.0.pre0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +4 -0
- data/lib/ione.rb +8 -0
- data/lib/ione/byte_buffer.rb +279 -0
- data/lib/ione/future.rb +509 -0
- data/lib/ione/io.rb +15 -0
- data/lib/ione/io/connection.rb +215 -0
- data/lib/ione/io/io_reactor.rb +321 -0
- data/lib/ione/version.rb +5 -0
- data/spec/integration/io_spec.rb +283 -0
- data/spec/ione/byte_buffer_spec.rb +342 -0
- data/spec/ione/future_spec.rb +737 -0
- data/spec/ione/io/connection_spec.rb +484 -0
- data/spec/ione/io/io_reactor_spec.rb +360 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/await_helper.rb +20 -0
- data/spec/support/fake_server.rb +106 -0
- metadata +70 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d01e0e0185b664af4fe4770c988a2560388ab05b
|
4
|
+
data.tar.gz: ce1e35186cc20d8732d0cecb1698720d2f3fb73b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a147567529615c86aefe2e2e6cfb5e0e0c57d99efef0bfedd02b4b91dd7acc611d98c98dd694a8ce2c6d657431134b14b7125b1a17bac97598aa560dda8026a
|
7
|
+
data.tar.gz: 92bf693d6bba06dd05e6434484f50802a780819db5b3f1a23c5ed02a30bc183362e1fbe7aeda581adc1223e32084fba11a24a9e7a2decb547e9122e441bd0e2d
|
data/.yardopts
ADDED
data/lib/ione.rb
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ione
|
4
|
+
# A byte buffer is a more efficient way of working with bytes than using
|
5
|
+
# a regular Ruby string. It also has convenient methods for reading integers
|
6
|
+
# shorts and single bytes that are faster than `String#unpack`.
|
7
|
+
#
|
8
|
+
# When you use a string as a buffer, by adding to the end and taking away
|
9
|
+
# from the beginning, Ruby will continue to grow the backing array of
|
10
|
+
# characters. This means that the longer you use the string the worse the
|
11
|
+
# performance will get and the more memory you waste.
|
12
|
+
#
|
13
|
+
# {ByteBuffer} solves the problem by using two strings: one is the read
|
14
|
+
# buffer and one is the write buffer. Writes go to the write buffer only,
|
15
|
+
# and reads read from the read buffer until it is empty, then a new write
|
16
|
+
# buffer is created and the old write buffer becomes the new read buffer.
|
17
|
+
class ByteBuffer
|
18
|
+
def initialize(initial_bytes='')
|
19
|
+
@read_buffer = ''
|
20
|
+
@write_buffer = ''
|
21
|
+
@offset = 0
|
22
|
+
@length = 0
|
23
|
+
append(initial_bytes) unless initial_bytes.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns the number of bytes in the buffer.
|
27
|
+
#
|
28
|
+
# The value is cached so this is a cheap operation.
|
29
|
+
attr_reader :length
|
30
|
+
alias_method :size, :length
|
31
|
+
alias_method :bytesize, :length
|
32
|
+
|
33
|
+
# Returns true when the number of bytes in the buffer is zero.
|
34
|
+
#
|
35
|
+
# The length is cached so this is a cheap operation.
|
36
|
+
def empty?
|
37
|
+
length == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
# Append the bytes from a string or another byte buffer to this buffer.
|
41
|
+
#
|
42
|
+
# @note
|
43
|
+
# When the bytes are not in an ASCII compatible encoding they are copied
|
44
|
+
# and retagged as `Encoding::BINARY` before they are appended to the
|
45
|
+
# buffer – this is required to avoid Ruby retagging the whole buffer with
|
46
|
+
# the encoding of the new bytes. If you can, make sure that the data you
|
47
|
+
# append is ASCII compatible (i.e. responds true to `#ascii_only?`),
|
48
|
+
# otherwise you will pay a small penalty for each append due to the extra
|
49
|
+
# copy that has to be made.
|
50
|
+
#
|
51
|
+
# @param [String, Ione::ByteBuffer] bytes the bytes to append
|
52
|
+
# @return [Ione::ByteBuffer] itself
|
53
|
+
def append(bytes)
|
54
|
+
if bytes.is_a?(self.class)
|
55
|
+
bytes.append_to(self)
|
56
|
+
else
|
57
|
+
bytes = bytes.to_s
|
58
|
+
unless bytes.ascii_only?
|
59
|
+
bytes = bytes.dup.force_encoding(::Encoding::BINARY)
|
60
|
+
end
|
61
|
+
retag = @write_buffer.empty?
|
62
|
+
@write_buffer << bytes
|
63
|
+
@write_buffer.force_encoding(::Encoding::BINARY) if retag
|
64
|
+
@length += bytes.bytesize
|
65
|
+
end
|
66
|
+
self
|
67
|
+
end
|
68
|
+
alias_method :<<, :append
|
69
|
+
|
70
|
+
# Remove the first N bytes from the buffer.
|
71
|
+
#
|
72
|
+
# @param [Integer] n the number of bytes to remove from the buffer
|
73
|
+
# @return [Ione::ByteBuffer] itself
|
74
|
+
# @raise RangeError when there are not enough bytes in the buffer
|
75
|
+
def discard(n)
|
76
|
+
raise RangeError, "#{n} bytes to discard but only #{@length} available" if @length < n
|
77
|
+
@offset += n
|
78
|
+
@length -= n
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Remove and return the first N bytes from the buffer.
|
83
|
+
#
|
84
|
+
# @param [Integer] n the number of bytes to remove and return from the buffer
|
85
|
+
# @return [String] a string with the bytes, the string will be tagged
|
86
|
+
# with `Encoding::BINARY`.
|
87
|
+
# @raise RangeError when there are not enough bytes in the buffer
|
88
|
+
def read(n)
|
89
|
+
raise RangeError, "#{n} bytes required but only #{@length} available" if @length < n
|
90
|
+
if @offset >= @read_buffer.bytesize
|
91
|
+
swap_buffers
|
92
|
+
end
|
93
|
+
if @offset + n > @read_buffer.bytesize
|
94
|
+
s = read(@read_buffer.bytesize - @offset)
|
95
|
+
s << read(n - s.bytesize)
|
96
|
+
s
|
97
|
+
else
|
98
|
+
s = @read_buffer[@offset, n]
|
99
|
+
@offset += n
|
100
|
+
@length -= n
|
101
|
+
s
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Remove and return the first four bytes from the buffer and decode them as an unsigned integer.
|
106
|
+
#
|
107
|
+
# @return [Integer] the big-endian integer interpretation of the four bytes
|
108
|
+
# @raise RangeError when there are not enough bytes in the buffer
|
109
|
+
def read_int
|
110
|
+
raise RangeError, "4 bytes required to read an int, but only #{@length} available" if @length < 4
|
111
|
+
if @offset >= @read_buffer.bytesize
|
112
|
+
swap_buffers
|
113
|
+
end
|
114
|
+
if @read_buffer.bytesize >= @offset + 4
|
115
|
+
i0 = @read_buffer.getbyte(@offset + 0)
|
116
|
+
i1 = @read_buffer.getbyte(@offset + 1)
|
117
|
+
i2 = @read_buffer.getbyte(@offset + 2)
|
118
|
+
i3 = @read_buffer.getbyte(@offset + 3)
|
119
|
+
@offset += 4
|
120
|
+
@length -= 4
|
121
|
+
else
|
122
|
+
i0 = read_byte
|
123
|
+
i1 = read_byte
|
124
|
+
i2 = read_byte
|
125
|
+
i3 = read_byte
|
126
|
+
end
|
127
|
+
(i0 << 24) | (i1 << 16) | (i2 << 8) | i3
|
128
|
+
end
|
129
|
+
|
130
|
+
# Remove and return the first two bytes from the buffer and decode them as an unsigned integer.
|
131
|
+
#
|
132
|
+
# @return [Integer] the big-endian integer interpretation of the two bytes
|
133
|
+
# @raise RangeError when there are not enough bytes in the buffer
|
134
|
+
def read_short
|
135
|
+
raise RangeError, "2 bytes required to read a short, but only #{@length} available" if @length < 2
|
136
|
+
if @offset >= @read_buffer.bytesize
|
137
|
+
swap_buffers
|
138
|
+
end
|
139
|
+
if @read_buffer.bytesize >= @offset + 2
|
140
|
+
i0 = @read_buffer.getbyte(@offset + 0)
|
141
|
+
i1 = @read_buffer.getbyte(@offset + 1)
|
142
|
+
@offset += 2
|
143
|
+
@length -= 2
|
144
|
+
else
|
145
|
+
i0 = read_byte
|
146
|
+
i1 = read_byte
|
147
|
+
end
|
148
|
+
(i0 << 8) | i1
|
149
|
+
end
|
150
|
+
|
151
|
+
# Remove and return the first byte from the buffer and decode it as a signed or unsigned integer.
|
152
|
+
#
|
153
|
+
# @param [Boolean] signed whether or not to interpret the byte as a signed number of not
|
154
|
+
# @return [Integer] the integer interpretation of the byte
|
155
|
+
# @raise RangeError when the buffer is empty
|
156
|
+
def read_byte(signed=false)
|
157
|
+
raise RangeError, "No bytes available to read byte" if empty?
|
158
|
+
if @offset >= @read_buffer.bytesize
|
159
|
+
swap_buffers
|
160
|
+
end
|
161
|
+
b = @read_buffer.getbyte(@offset)
|
162
|
+
b = (b & 0x7f) - (b & 0x80) if signed
|
163
|
+
@offset += 1
|
164
|
+
@length -= 1
|
165
|
+
b
|
166
|
+
end
|
167
|
+
|
168
|
+
# Overwrite a portion of the buffer with new bytes.
|
169
|
+
#
|
170
|
+
# The number of bytes that will be replaced depend on the size of the
|
171
|
+
# replacement string. If you pass a five byte string the five bytes
|
172
|
+
# starting at the location will be replaced.
|
173
|
+
#
|
174
|
+
# When you pass more bytes than the size of the buffer after the location
|
175
|
+
# only as many as needed to replace the remaining bytes of the buffer will
|
176
|
+
# actually be used.
|
177
|
+
#
|
178
|
+
# Make sure that you get your location right, if you have discarded bytes
|
179
|
+
# from the buffer all of the offsets will have changed.
|
180
|
+
#
|
181
|
+
# @example replacing bytes in the middle of a buffer
|
182
|
+
# buffer = ByteBuffer.new("hello world!")
|
183
|
+
# bufferupdate(6, "fnord")
|
184
|
+
# buffer # => "hello fnord!"
|
185
|
+
#
|
186
|
+
# @example replacing bytes at the end of the buffer
|
187
|
+
# buffer = ByteBuffer.new("my name is Jim")
|
188
|
+
# buffer.update(11, "Sammy")
|
189
|
+
# buffer # => "my name is Sam"
|
190
|
+
#
|
191
|
+
# @param [Integer] location the starting location where the new bytes
|
192
|
+
# should be inserted
|
193
|
+
# @param [String] bytes the replacement bytes
|
194
|
+
# @return [Ione::ByteBuffer] itself
|
195
|
+
def update(location, bytes)
|
196
|
+
absolute_offset = @offset + location
|
197
|
+
bytes_length = bytes.bytesize
|
198
|
+
if absolute_offset >= @read_buffer.bytesize
|
199
|
+
@write_buffer[absolute_offset - @read_buffer.bytesize, bytes_length] = bytes
|
200
|
+
else
|
201
|
+
overflow = absolute_offset + bytes_length - @read_buffer.bytesize
|
202
|
+
read_buffer_portion = bytes_length - overflow
|
203
|
+
@read_buffer[absolute_offset, read_buffer_portion] = bytes[0, read_buffer_portion]
|
204
|
+
if overflow > 0
|
205
|
+
@write_buffer[0, overflow] = bytes[read_buffer_portion, bytes_length - 1]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
self
|
209
|
+
end
|
210
|
+
|
211
|
+
# Return as much of the buffer as possible without having to concatenate
|
212
|
+
# or allocate any unnecessary strings.
|
213
|
+
#
|
214
|
+
# If the buffer is not empty this method will return something, but there
|
215
|
+
# are no guarantees as to how much it will return. It's primarily useful
|
216
|
+
# in situations where a loop wants to offer some bytes but can't be sure
|
217
|
+
# how many will be accepted — for example when writing to a socket.
|
218
|
+
#
|
219
|
+
# @example feeding bytes to a socket
|
220
|
+
# while true
|
221
|
+
# _, writables, _ = IO.select(nil, sockets)
|
222
|
+
# if writables
|
223
|
+
# writables.each do |io|
|
224
|
+
# n = io.write_nonblock(buffer.cheap_peek)
|
225
|
+
# buffer.discard(n)
|
226
|
+
# end
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# @return [String] some bytes from the start of the buffer
|
230
|
+
def cheap_peek
|
231
|
+
if @offset >= @read_buffer.bytesize
|
232
|
+
swap_buffers
|
233
|
+
end
|
234
|
+
@read_buffer[@offset, @read_buffer.bytesize - @offset]
|
235
|
+
end
|
236
|
+
|
237
|
+
def eql?(other)
|
238
|
+
self.to_str.eql?(other.to_str)
|
239
|
+
end
|
240
|
+
alias_method :==, :eql?
|
241
|
+
|
242
|
+
def hash
|
243
|
+
to_str.hash
|
244
|
+
end
|
245
|
+
|
246
|
+
def dup
|
247
|
+
self.class.new(to_str)
|
248
|
+
end
|
249
|
+
|
250
|
+
def to_str
|
251
|
+
(@read_buffer + @write_buffer)[@offset, @length]
|
252
|
+
end
|
253
|
+
alias_method :to_s, :to_str
|
254
|
+
|
255
|
+
def inspect
|
256
|
+
%(#<#{self.class.name}: #{to_str.inspect}>)
|
257
|
+
end
|
258
|
+
|
259
|
+
protected
|
260
|
+
|
261
|
+
def append_to(other)
|
262
|
+
other.raw_append(cheap_peek)
|
263
|
+
other.raw_append(@write_buffer) unless @write_buffer.empty?
|
264
|
+
end
|
265
|
+
|
266
|
+
def raw_append(bytes)
|
267
|
+
@write_buffer << bytes
|
268
|
+
@length += bytes.bytesize
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
|
273
|
+
def swap_buffers
|
274
|
+
@offset -= @read_buffer.bytesize
|
275
|
+
@read_buffer = @write_buffer
|
276
|
+
@write_buffer = ''
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
data/lib/ione/future.rb
ADDED
@@ -0,0 +1,509 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
|
6
|
+
module Ione
|
7
|
+
FutureError = Class.new(StandardError)
|
8
|
+
|
9
|
+
# A promise of delivering a value some time in the future.
|
10
|
+
#
|
11
|
+
# A promise is the write end of a Promise/Future pair. It can be fulfilled
|
12
|
+
# with a value or failed with an error. The value can be read through the
|
13
|
+
# future returned by {#future}.
|
14
|
+
class Promise
|
15
|
+
attr_reader :future
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@future = CompletableFuture.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fulfills the promise.
|
22
|
+
#
|
23
|
+
# This will resolve this promise's future, and trigger all listeners of that
|
24
|
+
# future. The value of the future will be the specified value, or nil if
|
25
|
+
# no value is specified.
|
26
|
+
#
|
27
|
+
# @param [Object] value the value of the future
|
28
|
+
def fulfill(value=nil)
|
29
|
+
@future.resolve(value)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Fails the promise.
|
33
|
+
#
|
34
|
+
# This will fail this promise's future, and trigger all listeners of that
|
35
|
+
# future.
|
36
|
+
#
|
37
|
+
# @param [Error] error the error which prevented the promise to be fulfilled
|
38
|
+
def fail(error)
|
39
|
+
@future.fail(error)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Observe a future and fulfill the promise with the future's value when the
|
43
|
+
# future resolves, or fail with the future's error when the future fails.
|
44
|
+
#
|
45
|
+
# @param [Ione::Future] future the future to observe
|
46
|
+
def observe(future)
|
47
|
+
future.on_value { |v| fulfill(v) }
|
48
|
+
future.on_failure { |e| fail(e) }
|
49
|
+
end
|
50
|
+
|
51
|
+
# Run the given block and fulfill this promise with its result. If the block
|
52
|
+
# raises an error, fail this promise with the error.
|
53
|
+
#
|
54
|
+
# All arguments given will be passed onto the block.
|
55
|
+
#
|
56
|
+
# @example
|
57
|
+
# promise.try { 3 + 4 }
|
58
|
+
# promise.future.value # => 7
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# promise.try do
|
62
|
+
# do_something_that_will_raise_an_error
|
63
|
+
# end
|
64
|
+
# promise.future.value # => (raises error)
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# promise.try('foo', 'bar', &proc_taking_two_arguments)
|
68
|
+
#
|
69
|
+
# @yieldparam [Array] ctx the arguments passed to {#try}
|
70
|
+
def try(*ctx)
|
71
|
+
fulfill(yield(*ctx))
|
72
|
+
rescue => e
|
73
|
+
fail(e)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module FutureFactories
|
78
|
+
# Combines multiple futures into a new future which resolves when all
|
79
|
+
# constituent futures complete, or fails when one or more of them fails.
|
80
|
+
#
|
81
|
+
# The value of the combined future is an array of the values of the
|
82
|
+
# constituent futures.
|
83
|
+
#
|
84
|
+
# @param [Array<Ione::Future>] futures the futures to combine
|
85
|
+
# @return [Ione::Future<Array>] an array of the values of the constituent
|
86
|
+
# futures
|
87
|
+
def all(*futures)
|
88
|
+
return resolved([]) if futures.empty?
|
89
|
+
CombinedFuture.new(futures)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a future which will be resolved with the value of the first
|
93
|
+
# (resolved) of the specified futures. If all of the futures fail, the
|
94
|
+
# returned future will also fail (with the error of the last failed future).
|
95
|
+
#
|
96
|
+
# @param [Array<Ione::Future>] futures the futures to monitor
|
97
|
+
# @return [Ione::Future] a future which represents the first completing future
|
98
|
+
def first(*futures)
|
99
|
+
return resolved if futures.empty?
|
100
|
+
FirstFuture.new(futures)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Creates a new pre-resolved future.
|
104
|
+
#
|
105
|
+
# @param [Object, nil] value the value of the created future
|
106
|
+
# @return [Ione::Future] a resolved future
|
107
|
+
def resolved(value=nil)
|
108
|
+
ResolvedFuture.new(value)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Creates a new pre-failed future.
|
112
|
+
#
|
113
|
+
# @param [Error] error the error of the created future
|
114
|
+
# @return [Ione::Future] a failed future
|
115
|
+
def failed(error)
|
116
|
+
FailedFuture.new(error)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
module FutureCombinators
|
121
|
+
# Returns a new future representing a transformation of this future's value.
|
122
|
+
#
|
123
|
+
# @example
|
124
|
+
# future2 = future1.map { |value| value * 2 }
|
125
|
+
#
|
126
|
+
# @param [Object] value the value of this future (when no block is given)
|
127
|
+
# @yieldparam [Object] value the value of this future
|
128
|
+
# @yieldreturn [Object] the transformed value
|
129
|
+
# @return [Ione::Future] a new future representing the transformed value
|
130
|
+
def map(value=nil, &block)
|
131
|
+
CompletableFuture.new.tap do |f|
|
132
|
+
on_failure { |e| f.fail(e) }
|
133
|
+
on_value { |v| run(f, value, block, v) }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns a new future representing a transformation of this future's value,
|
138
|
+
# but where the transformation itself may be asynchronous.
|
139
|
+
#
|
140
|
+
# @example
|
141
|
+
# future2 = future1.flat_map { |value| method_returning_a_future(value) }
|
142
|
+
#
|
143
|
+
# This method is useful when you want to chain asynchronous operations.
|
144
|
+
#
|
145
|
+
# @yieldparam [Object] value the value of this future
|
146
|
+
# @yieldreturn [Ione::Future] a future representing the transformed value
|
147
|
+
# @return [Ione::Future] a new future representing the transformed value
|
148
|
+
def flat_map(&block)
|
149
|
+
CompletableFuture.new.tap do |f|
|
150
|
+
on_failure { |e| f.fail(e) }
|
151
|
+
on_value { |v| chain(f, block, v) }
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns a new future which represents either the value of the original
|
156
|
+
# future, or the result of the given block, if the original future fails.
|
157
|
+
#
|
158
|
+
# This method is similar to{#map}, but is triggered by a failure. You can
|
159
|
+
# also think of it as a `rescue` block for asynchronous operations.
|
160
|
+
#
|
161
|
+
# If the block raises an error a failed future with that error will be
|
162
|
+
# returned (this can be used to transform an error into another error,
|
163
|
+
# instead of tranforming an error into a value).
|
164
|
+
#
|
165
|
+
# @example
|
166
|
+
# future2 = future1.recover { |error| 'foo' }
|
167
|
+
# future1.fail(error)
|
168
|
+
# future2.value # => 'foo'
|
169
|
+
#
|
170
|
+
# @param [Object] value the value when no block is given
|
171
|
+
# @yieldparam [Object] error the error from the original future
|
172
|
+
# @yieldreturn [Object] the value of the new future
|
173
|
+
# @return [Ione::Future] a new future representing a value recovered from the error
|
174
|
+
def recover(value=nil, &block)
|
175
|
+
CompletableFuture.new.tap do |f|
|
176
|
+
on_failure { |e| run(f, value, block, e) }
|
177
|
+
on_value { |v| f.resolve(v) }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns a new future which represents either the value of the original
|
182
|
+
# future, or the value of the future returned by the given block.
|
183
|
+
#
|
184
|
+
# This is like {#recover} but for cases when the handling of an error is
|
185
|
+
# itself asynchronous. In other words, {#fallback} is to {#recover} what
|
186
|
+
# {#flat_map} is to {#map}.
|
187
|
+
#
|
188
|
+
# If the block raises an error a failed future with that error will be
|
189
|
+
# returned (this can be used to transform an error into another error,
|
190
|
+
# instead of tranforming an error into a value).
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# result = http_get('/foo/bar').fallback do |error|
|
194
|
+
# http_get('/baz')
|
195
|
+
# end
|
196
|
+
# result.value # either the response to /foo/bar, or if that failed
|
197
|
+
# # the response to /baz
|
198
|
+
#
|
199
|
+
# @yieldparam [Object] error the error from the original future
|
200
|
+
# @yieldreturn [Object] the value of the new future
|
201
|
+
# @return [Ione::Future] a new future representing a value recovered from the
|
202
|
+
# error
|
203
|
+
def fallback(&block)
|
204
|
+
CompletableFuture.new.tap do |f|
|
205
|
+
on_failure { |e| chain(f, block, e) }
|
206
|
+
on_value { |v| f.resolve(v) }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def run(f, value, producer, arg)
|
213
|
+
value = producer ? producer.call(arg) : value
|
214
|
+
f.resolve(value)
|
215
|
+
rescue => e
|
216
|
+
f.fail(e)
|
217
|
+
end
|
218
|
+
|
219
|
+
def chain(f, constructor, arg)
|
220
|
+
ff = constructor.call(arg)
|
221
|
+
ff.on_failure { |e| f.fail(e) }
|
222
|
+
ff.on_value { |v| f.resolve(v) }
|
223
|
+
rescue => e
|
224
|
+
f.fail(e)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
module FutureCallbacks
|
229
|
+
# Registers a listener that will be called when this future completes,
|
230
|
+
# i.e. resolves or fails. The listener will be called with the future as
|
231
|
+
# solve argument
|
232
|
+
#
|
233
|
+
# @yieldparam [Ione::Future] future the future
|
234
|
+
def on_complete(&listener)
|
235
|
+
run_immediately = false
|
236
|
+
@lock.synchronize do
|
237
|
+
if @resolved || @failed
|
238
|
+
run_immediately = true
|
239
|
+
else
|
240
|
+
@complete_listeners << listener
|
241
|
+
end
|
242
|
+
end
|
243
|
+
if run_immediately
|
244
|
+
listener.call(self) rescue nil
|
245
|
+
end
|
246
|
+
nil
|
247
|
+
end
|
248
|
+
|
249
|
+
# Registers a listener that will be called when this future becomes
|
250
|
+
# resolved. The listener will be called with the value of the future as
|
251
|
+
# sole argument.
|
252
|
+
#
|
253
|
+
# @yieldparam [Object] value the value of the resolved future
|
254
|
+
def on_value(&listener)
|
255
|
+
run_immediately = false
|
256
|
+
@lock.synchronize do
|
257
|
+
if @resolved
|
258
|
+
run_immediately = true
|
259
|
+
elsif !@failed
|
260
|
+
@value_listeners << listener
|
261
|
+
end
|
262
|
+
end
|
263
|
+
if run_immediately
|
264
|
+
listener.call(value) rescue nil
|
265
|
+
end
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
# Registers a listener that will be called when this future fails. The
|
270
|
+
# lisener will be called with the error that failed the future as sole
|
271
|
+
# argument.
|
272
|
+
#
|
273
|
+
# @yieldparam [Error] error the error that failed the future
|
274
|
+
def on_failure(&listener)
|
275
|
+
run_immediately = false
|
276
|
+
@lock.synchronize do
|
277
|
+
if @failed
|
278
|
+
run_immediately = true
|
279
|
+
elsif !@resolved
|
280
|
+
@failure_listeners << listener
|
281
|
+
end
|
282
|
+
end
|
283
|
+
if run_immediately
|
284
|
+
listener.call(@error) rescue nil
|
285
|
+
end
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# A future represents the value of a process that may not yet have completed.
|
291
|
+
#
|
292
|
+
# @see Ione::Promise
|
293
|
+
class Future
|
294
|
+
extend FutureFactories
|
295
|
+
include FutureCombinators
|
296
|
+
include FutureCallbacks
|
297
|
+
|
298
|
+
def initialize
|
299
|
+
@lock = Mutex.new
|
300
|
+
@resolved = false
|
301
|
+
@failed = false
|
302
|
+
@failure_listeners = []
|
303
|
+
@value_listeners = []
|
304
|
+
@complete_listeners = []
|
305
|
+
end
|
306
|
+
|
307
|
+
# Returns the value of this future, blocking until it is available if
|
308
|
+
# necessary.
|
309
|
+
#
|
310
|
+
# If the future fails this method will raise the error that failed the
|
311
|
+
# future.
|
312
|
+
#
|
313
|
+
# @return [Object] the value of this future
|
314
|
+
def value
|
315
|
+
semaphore = nil
|
316
|
+
@lock.synchronize do
|
317
|
+
raise @error if @failed
|
318
|
+
return @value if @resolved
|
319
|
+
semaphore = Queue.new
|
320
|
+
u = proc { semaphore << :unblock }
|
321
|
+
@value_listeners << u
|
322
|
+
@failure_listeners << u
|
323
|
+
end
|
324
|
+
while true
|
325
|
+
@lock.synchronize do
|
326
|
+
raise @error if @failed
|
327
|
+
return @value if @resolved
|
328
|
+
end
|
329
|
+
semaphore.pop
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# Returns true if this future is resolved or failed
|
334
|
+
def completed?
|
335
|
+
resolved? || failed?
|
336
|
+
end
|
337
|
+
|
338
|
+
# Returns true if this future is resolved
|
339
|
+
def resolved?
|
340
|
+
@lock.synchronize { @resolved }
|
341
|
+
end
|
342
|
+
|
343
|
+
# Returns true if this future has failed
|
344
|
+
def failed?
|
345
|
+
@lock.synchronize { @failed }
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
# @private
|
350
|
+
class CompletableFuture < Future
|
351
|
+
def resolve(v=nil)
|
352
|
+
value_listeners = nil
|
353
|
+
complete_listeners = nil
|
354
|
+
@lock.synchronize do
|
355
|
+
raise FutureError, 'Future already completed' if @resolved || @failed
|
356
|
+
@resolved = true
|
357
|
+
@value = v
|
358
|
+
value_listeners = @value_listeners
|
359
|
+
complete_listeners = @complete_listeners
|
360
|
+
@value_listeners = nil
|
361
|
+
@failure_listeners = nil
|
362
|
+
@complete_listeners = nil
|
363
|
+
end
|
364
|
+
value_listeners.each do |listener|
|
365
|
+
listener.call(v) rescue nil
|
366
|
+
end
|
367
|
+
complete_listeners.each do |listener|
|
368
|
+
listener.call(self) rescue nil
|
369
|
+
end
|
370
|
+
nil
|
371
|
+
end
|
372
|
+
|
373
|
+
def fail(error)
|
374
|
+
failure_listeners = nil
|
375
|
+
complete_listeners = nil
|
376
|
+
@lock.synchronize do
|
377
|
+
raise FutureError, 'Future already completed' if @failed || @resolved
|
378
|
+
@failed = true
|
379
|
+
@error = error
|
380
|
+
failure_listeners = @failure_listeners
|
381
|
+
complete_listeners = @complete_listeners
|
382
|
+
@value_listeners = nil
|
383
|
+
@failure_listeners = nil
|
384
|
+
@complete_listeners = nil
|
385
|
+
end
|
386
|
+
failure_listeners.each do |listener|
|
387
|
+
listener.call(error) rescue nil
|
388
|
+
end
|
389
|
+
complete_listeners.each do |listener|
|
390
|
+
listener.call(self) rescue nil
|
391
|
+
end
|
392
|
+
nil
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
# @private
|
397
|
+
class CombinedFuture < CompletableFuture
|
398
|
+
def initialize(futures)
|
399
|
+
super()
|
400
|
+
values = Array.new(futures.size)
|
401
|
+
remaining = futures.size
|
402
|
+
futures.each_with_index do |f, i|
|
403
|
+
f.on_value do |v|
|
404
|
+
@lock.synchronize do
|
405
|
+
values[i] = v
|
406
|
+
remaining -= 1
|
407
|
+
end
|
408
|
+
if remaining == 0
|
409
|
+
resolve(values)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
f.on_failure do |e|
|
413
|
+
unless failed?
|
414
|
+
fail(e)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# @private
|
422
|
+
class FirstFuture < CompletableFuture
|
423
|
+
def initialize(futures)
|
424
|
+
super()
|
425
|
+
futures.each do |f|
|
426
|
+
f.on_value do |value|
|
427
|
+
resolve(value) unless completed?
|
428
|
+
end
|
429
|
+
f.on_failure do |e|
|
430
|
+
fail(e) if futures.all?(&:failed?)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
436
|
+
# @private
|
437
|
+
class ResolvedFuture < Future
|
438
|
+
def initialize(value=nil)
|
439
|
+
@resolved = true
|
440
|
+
@failed = false
|
441
|
+
@value = value
|
442
|
+
@error = nil
|
443
|
+
end
|
444
|
+
|
445
|
+
def value
|
446
|
+
@value
|
447
|
+
end
|
448
|
+
|
449
|
+
def completed?
|
450
|
+
true
|
451
|
+
end
|
452
|
+
|
453
|
+
def resolved?
|
454
|
+
true
|
455
|
+
end
|
456
|
+
|
457
|
+
def failed?
|
458
|
+
false
|
459
|
+
end
|
460
|
+
|
461
|
+
def on_complete(&listener)
|
462
|
+
listener.call(self) rescue nil
|
463
|
+
end
|
464
|
+
|
465
|
+
def on_value(&listener)
|
466
|
+
listener.call(value) rescue nil
|
467
|
+
end
|
468
|
+
|
469
|
+
def on_failure
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# @private
|
474
|
+
class FailedFuture < Future
|
475
|
+
def initialize(error)
|
476
|
+
@resolved = false
|
477
|
+
@failed = true
|
478
|
+
@value = nil
|
479
|
+
@error = error
|
480
|
+
end
|
481
|
+
|
482
|
+
def value
|
483
|
+
raise @error
|
484
|
+
end
|
485
|
+
|
486
|
+
def completed?
|
487
|
+
true
|
488
|
+
end
|
489
|
+
|
490
|
+
def resolved?
|
491
|
+
false
|
492
|
+
end
|
493
|
+
|
494
|
+
def failed?
|
495
|
+
true
|
496
|
+
end
|
497
|
+
|
498
|
+
def on_complete(&listener)
|
499
|
+
listener.call(self) rescue nil
|
500
|
+
end
|
501
|
+
|
502
|
+
def on_value
|
503
|
+
end
|
504
|
+
|
505
|
+
def on_failure(&listener)
|
506
|
+
listener.call(@error) rescue nil
|
507
|
+
end
|
508
|
+
end
|
509
|
+
end
|