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
@@ -0,0 +1,360 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
|
6
|
+
module Ione
|
7
|
+
module Io
|
8
|
+
describe IoReactor do
|
9
|
+
let :reactor do
|
10
|
+
described_class.new(selector: selector, clock: clock)
|
11
|
+
end
|
12
|
+
|
13
|
+
let! :selector do
|
14
|
+
IoReactorSpec::FakeSelector.new
|
15
|
+
end
|
16
|
+
|
17
|
+
let :clock do
|
18
|
+
double(:clock, now: 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
shared_context 'running_reactor' do
|
22
|
+
before do
|
23
|
+
selector.handler do |readables, writables, _, _|
|
24
|
+
writables.each do |writable|
|
25
|
+
fake_connected(writable)
|
26
|
+
end
|
27
|
+
[[], writables, []]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def fake_connected(connection)
|
32
|
+
connection.to_io.stub(:connect_nonblock)
|
33
|
+
end
|
34
|
+
|
35
|
+
after do
|
36
|
+
reactor.stop if reactor.running?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#start' do
|
41
|
+
after do
|
42
|
+
reactor.stop.value if reactor.running?
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns a future that is resolved when the reactor has started' do
|
46
|
+
reactor.start.value
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns a future that resolves to the reactor' do
|
50
|
+
reactor.start.value.should equal(reactor)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'is running after being started' do
|
54
|
+
reactor.start.value
|
55
|
+
reactor.should be_running
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'cannot be started again once stopped' do
|
59
|
+
reactor.start.value
|
60
|
+
reactor.stop.value
|
61
|
+
expect { reactor.start }.to raise_error(ReactorError)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'calls the selector' do
|
65
|
+
called = false
|
66
|
+
selector.handler { called = true; [[], [], []] }
|
67
|
+
reactor.start.value
|
68
|
+
await { called }
|
69
|
+
reactor.stop.value
|
70
|
+
called.should be_true, 'expected the selector to have been called'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#stop' do
|
75
|
+
include_context 'running_reactor'
|
76
|
+
|
77
|
+
it 'returns a future that is resolved when the reactor has stopped' do
|
78
|
+
reactor.start.value
|
79
|
+
reactor.stop.value
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns a future which resolves to the reactor' do
|
83
|
+
reactor.start.value
|
84
|
+
reactor.stop.value.should equal(reactor)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'is not running after being stopped' do
|
88
|
+
reactor.start.value
|
89
|
+
reactor.stop.value
|
90
|
+
reactor.should_not be_running
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'closes all sockets' do
|
94
|
+
reactor.start.value
|
95
|
+
connection = reactor.connect('example.com', 9999, 5).value
|
96
|
+
reactor.stop.value
|
97
|
+
connection.should be_closed
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'cancels all active timers' do
|
101
|
+
reactor.start.value
|
102
|
+
clock.stub(:now).and_return(1)
|
103
|
+
expired_timer = reactor.schedule_timer(1)
|
104
|
+
active_timer1 = reactor.schedule_timer(999)
|
105
|
+
active_timer2 = reactor.schedule_timer(111)
|
106
|
+
expired_timer.should_not_receive(:fail)
|
107
|
+
clock.stub(:now).and_return(2)
|
108
|
+
await { expired_timer.completed? }
|
109
|
+
reactor.stop.value
|
110
|
+
active_timer1.should be_failed
|
111
|
+
active_timer2.should be_failed
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#on_error' do
|
116
|
+
before do
|
117
|
+
selector.handler { raise 'Blurgh' }
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'calls the listeners when the reactor crashes' do
|
121
|
+
error = nil
|
122
|
+
reactor.on_error { |e| error = e }
|
123
|
+
reactor.start
|
124
|
+
await { error }
|
125
|
+
error.message.should == 'Blurgh'
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'calls the listener immediately when the reactor has already crashed' do
|
129
|
+
error = nil
|
130
|
+
reactor.start.value
|
131
|
+
await { !reactor.running? }
|
132
|
+
reactor.on_error { |e| error = e }
|
133
|
+
await { error }
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'ignores errors raised by listeners' do
|
137
|
+
called = false
|
138
|
+
reactor.on_error { raise 'Blurgh' }
|
139
|
+
reactor.on_error { called = true }
|
140
|
+
reactor.start
|
141
|
+
await { called }
|
142
|
+
called.should be_true, 'expected all close listeners to have been called'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe '#connect' do
|
147
|
+
include_context 'running_reactor'
|
148
|
+
|
149
|
+
it 'returns the connected connection when no block is given' do
|
150
|
+
reactor.start.value
|
151
|
+
reactor.connect('example.com', 9999, 5).value.should be_a(Connection)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe '#schedule_timer' do
|
156
|
+
before do
|
157
|
+
reactor.start.value
|
158
|
+
end
|
159
|
+
|
160
|
+
after do
|
161
|
+
reactor.stop.value
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'returns a future that is resolved after the specified duration' do
|
165
|
+
clock.stub(:now).and_return(1)
|
166
|
+
f = reactor.schedule_timer(0.1)
|
167
|
+
clock.stub(:now).and_return(1.1)
|
168
|
+
await { f.resolved? }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe '#to_s' do
|
173
|
+
context 'returns a string that' do
|
174
|
+
it 'includes the class name' do
|
175
|
+
reactor.to_s.should include('Ione::Io::IoReactor')
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'includes a list of its connections' do
|
179
|
+
reactor.to_s.should include('@connections=[')
|
180
|
+
reactor.to_s.should include('#<Ione::Io::Unblocker>')
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe IoLoopBody do
|
187
|
+
let :loop_body do
|
188
|
+
described_class.new(selector: selector, clock: clock)
|
189
|
+
end
|
190
|
+
|
191
|
+
let :selector do
|
192
|
+
double(:selector)
|
193
|
+
end
|
194
|
+
|
195
|
+
let :clock do
|
196
|
+
double(:clock, now: 0)
|
197
|
+
end
|
198
|
+
|
199
|
+
let :socket do
|
200
|
+
double(:socket, connected?: false, connecting?: false, writable?: false, closed?: false)
|
201
|
+
end
|
202
|
+
|
203
|
+
describe '#tick' do
|
204
|
+
before do
|
205
|
+
loop_body.add_socket(socket)
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'passes connected sockets as readables to the selector' do
|
209
|
+
socket.stub(:connected?).and_return(true)
|
210
|
+
selector.should_receive(:select).with([socket], anything, anything, anything).and_return([nil, nil, nil])
|
211
|
+
loop_body.tick
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'passes writable sockets as writable to the selector' do
|
215
|
+
socket.stub(:writable?).and_return(true)
|
216
|
+
selector.should_receive(:select).with(anything, [socket], anything, anything).and_return([nil, nil, nil])
|
217
|
+
loop_body.tick
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'passes connecting sockets as writable to the selector' do
|
221
|
+
socket.stub(:connecting?).and_return(true)
|
222
|
+
socket.stub(:connect)
|
223
|
+
selector.should_receive(:select).with(anything, [socket], anything, anything).and_return([nil, nil, nil])
|
224
|
+
loop_body.tick
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'filters out closed sockets' do
|
228
|
+
socket.stub(:closed?).and_return(true)
|
229
|
+
selector.should_receive(:select).with([], [], anything, anything).and_return([nil, nil, nil])
|
230
|
+
loop_body.tick
|
231
|
+
socket.stub(:connected?).and_return(true)
|
232
|
+
selector.should_receive(:select).with([], [], anything, anything).and_return([nil, nil, nil])
|
233
|
+
loop_body.tick
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'calls #read on all readable sockets returned by the selector' do
|
237
|
+
socket.stub(:connected?).and_return(true)
|
238
|
+
socket.should_receive(:read)
|
239
|
+
selector.stub(:select) do |r, w, _, _|
|
240
|
+
[[socket], nil, nil]
|
241
|
+
end
|
242
|
+
loop_body.tick
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'calls #connect on all connecting sockets' do
|
246
|
+
socket.stub(:connecting?).and_return(true)
|
247
|
+
socket.should_receive(:connect)
|
248
|
+
selector.stub(:select).and_return([nil, nil, nil])
|
249
|
+
loop_body.tick
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'calls #flush on all writable sockets returned by the selector' do
|
253
|
+
socket.stub(:writable?).and_return(true)
|
254
|
+
socket.should_receive(:flush)
|
255
|
+
selector.stub(:select) do |r, w, _, _|
|
256
|
+
[nil, [socket], nil]
|
257
|
+
end
|
258
|
+
loop_body.tick
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'allows the caller to specify a custom timeout' do
|
262
|
+
selector.should_receive(:select).with(anything, anything, anything, 99).and_return([[], [], []])
|
263
|
+
loop_body.tick(99)
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'completes timers that have expired' do
|
267
|
+
selector.stub(:select).and_return([nil, nil, nil])
|
268
|
+
clock.stub(:now).and_return(1)
|
269
|
+
promise = Promise.new
|
270
|
+
loop_body.schedule_timer(1, promise)
|
271
|
+
loop_body.tick
|
272
|
+
promise.future.should_not be_completed
|
273
|
+
clock.stub(:now).and_return(2)
|
274
|
+
loop_body.tick
|
275
|
+
promise.future.should be_completed
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'clears out timers that have expired' do
|
279
|
+
selector.stub(:select).and_return([nil, nil, nil])
|
280
|
+
clock.stub(:now).and_return(1)
|
281
|
+
promise = Promise.new
|
282
|
+
loop_body.schedule_timer(1, promise)
|
283
|
+
clock.stub(:now).and_return(2)
|
284
|
+
loop_body.tick
|
285
|
+
promise.future.should be_completed
|
286
|
+
promise.should_not_receive(:fulfill)
|
287
|
+
loop_body.tick
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
describe '#close_sockets' do
|
292
|
+
it 'closes all sockets' do
|
293
|
+
socket1 = double(:socket1, closed?: false)
|
294
|
+
socket2 = double(:socket2, closed?: false)
|
295
|
+
socket1.should_receive(:close)
|
296
|
+
socket2.should_receive(:close)
|
297
|
+
loop_body.add_socket(socket1)
|
298
|
+
loop_body.add_socket(socket2)
|
299
|
+
loop_body.close_sockets
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'closes all sockets, even when one of them raises an error' do
|
303
|
+
socket1 = double(:socket1, closed?: false)
|
304
|
+
socket2 = double(:socket2, closed?: false)
|
305
|
+
socket1.stub(:close).and_raise('Blurgh')
|
306
|
+
socket2.should_receive(:close)
|
307
|
+
loop_body.add_socket(socket1)
|
308
|
+
loop_body.add_socket(socket2)
|
309
|
+
loop_body.close_sockets
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'does not close already closed sockets' do
|
313
|
+
socket.stub(:closed?).and_return(true)
|
314
|
+
socket.should_not_receive(:close)
|
315
|
+
loop_body.add_socket(socket)
|
316
|
+
loop_body.close_sockets
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
describe '#cancel_timers' do
|
321
|
+
before do
|
322
|
+
selector.stub(:select).and_return([nil, nil, nil])
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'fails all active timers with a CancelledError' do
|
326
|
+
p1 = Promise.new
|
327
|
+
p2 = Promise.new
|
328
|
+
p3 = Promise.new
|
329
|
+
clock.stub(:now).and_return(1)
|
330
|
+
loop_body.schedule_timer(1, p1)
|
331
|
+
loop_body.schedule_timer(3, p2)
|
332
|
+
loop_body.schedule_timer(3, p3)
|
333
|
+
clock.stub(:now).and_return(2)
|
334
|
+
loop_body.tick
|
335
|
+
loop_body.cancel_timers
|
336
|
+
p1.future.should be_completed
|
337
|
+
p2.future.should be_failed
|
338
|
+
p3.future.should be_failed
|
339
|
+
expect { p3.future.value }.to raise_error(CancelledError)
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
module IoReactorSpec
|
347
|
+
class FakeSelector
|
348
|
+
def initialize
|
349
|
+
handler { [[], [], []] }
|
350
|
+
end
|
351
|
+
|
352
|
+
def handler(&body)
|
353
|
+
@body = body
|
354
|
+
end
|
355
|
+
|
356
|
+
def select(*args)
|
357
|
+
@body.call(*args)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module AwaitHelper
|
4
|
+
def await(timeout=5, &test)
|
5
|
+
started_at = Time.now
|
6
|
+
until test.call
|
7
|
+
yield
|
8
|
+
time_taken = Time.now - started_at
|
9
|
+
if time_taken > timeout
|
10
|
+
fail('Test took more than %.1fs' % [time_taken.to_f])
|
11
|
+
else
|
12
|
+
sleep(0.01)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.configure do |c|
|
19
|
+
c.include(AwaitHelper)
|
20
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class FakeServer
|
4
|
+
attr_reader :port, :connects, :disconnects
|
5
|
+
|
6
|
+
def initialize(port=(2**15 + rand(2**15)))
|
7
|
+
@port = port
|
8
|
+
@state = {}
|
9
|
+
@lock = Mutex.new
|
10
|
+
@connects = 0
|
11
|
+
@disconnects = 0
|
12
|
+
@connections = []
|
13
|
+
@received_bytes = ''
|
14
|
+
end
|
15
|
+
|
16
|
+
def start!(options={})
|
17
|
+
@lock.synchronize do
|
18
|
+
return if @running
|
19
|
+
@running = true
|
20
|
+
end
|
21
|
+
@sockets = [TCPServer.new(@port)]
|
22
|
+
@started = Ione::Promise.new
|
23
|
+
@thread = Thread.start do
|
24
|
+
Thread.current.abort_on_exception = true
|
25
|
+
sleep(options[:accept_delay] || 0)
|
26
|
+
@started.fulfill
|
27
|
+
io_loop
|
28
|
+
end
|
29
|
+
@started.future.value
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop!
|
34
|
+
@lock.synchronize do
|
35
|
+
return unless @running
|
36
|
+
@running = false
|
37
|
+
end
|
38
|
+
if defined? @started
|
39
|
+
@thread.join
|
40
|
+
@sockets.each(&:close)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def broadcast!(bytes)
|
45
|
+
@lock.synchronize do
|
46
|
+
@connections.each { |c| c.write_nonblock(bytes) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def await_connects!(n=1)
|
51
|
+
started_at = Time.now
|
52
|
+
until @connects >= n
|
53
|
+
sleep(0.01)
|
54
|
+
raise 'Waited longer than 5s!' if (Time.now - started_at) > 5
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def await_disconnects!(n=1)
|
59
|
+
started_at = Time.now
|
60
|
+
until @disconnects >= n
|
61
|
+
sleep(0.01)
|
62
|
+
raise 'Waited longer than 5s!' if (Time.now - started_at) > 5
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def received_bytes
|
67
|
+
@lock.synchronize do
|
68
|
+
return @received_bytes.dup
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def io_loop
|
75
|
+
while @running
|
76
|
+
acceptables, _ = IO.select(@sockets, @connections, nil, 0)
|
77
|
+
readables, writables, _ = IO.select(@connections, @connections, nil, 0)
|
78
|
+
|
79
|
+
if acceptables
|
80
|
+
acceptables.each do |socket|
|
81
|
+
connection, _ = socket.accept_nonblock
|
82
|
+
@lock.synchronize do
|
83
|
+
@connects += 1
|
84
|
+
@connections << connection
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
if readables
|
90
|
+
readables.each do |readable|
|
91
|
+
begin
|
92
|
+
bytes = readable.read_nonblock(2**16)
|
93
|
+
@lock.synchronize do
|
94
|
+
@received_bytes << bytes
|
95
|
+
end
|
96
|
+
rescue EOFError
|
97
|
+
@lock.synchronize do
|
98
|
+
@connections.delete(readable)
|
99
|
+
@disconnects += 1
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|