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