ione 1.0.0.pre1 → 1.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +23 -0
- data/lib/ione/byte_buffer.rb +24 -3
- data/lib/ione/version.rb +1 -1
- data/spec/integration/io_spec.rb +0 -211
- data/spec/ione/byte_buffer_spec.rb +34 -0
- data/spec/ione/future_spec.rb +31 -3
- data/spec/spec_helper.rb +20 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72e65e6b94938777a27d3baf72acda5bb56dc206
|
4
|
+
data.tar.gz: ecfaf4523b57f11061d0548be4c4649d54ab1e8d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8576c5a7b533d37597d874bbca5dd92ff9385afbdfdb730328307d4dd8eac8be17efddf1445655367e422ada2215aeec5814f4c11505a5db5ea9d1deb061ff39
|
7
|
+
data.tar.gz: 8de121c515e8a25ec2208d47da67eeb5b7771036c8358f92c3e087e4862b5667408d5c1256f8777765ba0ec03d3eebf0cac868e62bc09e8cea92661cd3606104
|
data/README.md
CHANGED
@@ -1,12 +1,35 @@
|
|
1
1
|
# Ione
|
2
2
|
|
3
3
|
[![Build Status](https://travis-ci.org/iconara/ione.png?branch=master)](https://travis-ci.org/iconara/ione)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/iconara/ione/badge.png)](https://coveralls.io/r/iconara/ione)
|
4
5
|
[![Blog](http://b.repl.ca/v1/blog-ione-ff69b4.png)](http://architecturalatrocities.com/tagged/ione)
|
5
6
|
|
6
7
|
_If you're reading this on GitHub, please note that this is the readme for the development version and that some features described here might not yet have been released. You can find the readme for a specific version either through [rubydoc.info](http://rubydoc.info/find/gems?q=ione) or via the release tags ([here is an example](https://github.com/iconara/ione/tree/v1.0.0.pre0))._
|
7
8
|
|
8
9
|
Ione is a framework for reactive programming in Ruby. It is based on the reactive core of [cql-rb](http://github.com/iconara/cql-rb), the Ruby driver for Cassandra.
|
9
10
|
|
11
|
+
# Features
|
12
|
+
|
13
|
+
## Futures & promises
|
14
|
+
|
15
|
+
At the core of Ione is a futures API. Futures make it easy to compose asynchronous operations.
|
16
|
+
|
17
|
+
## Evented IO
|
18
|
+
|
19
|
+
A key piece of the framework is an IO reactor with which you can easily build network clients and servers.
|
20
|
+
|
21
|
+
### Byte buffer
|
22
|
+
|
23
|
+
Networking usually means pushing lots of bytes around and in Ruby it's easy to make the mistake of using strings as buffers. Ione provides an efficient byte buffer implementation as an alternative.
|
24
|
+
|
25
|
+
# Examples
|
26
|
+
|
27
|
+
The [examples](https://github.com/iconara/ione/tree/master/examples) directory has some examples of what you can do with Ione, for example:
|
28
|
+
|
29
|
+
* [redis_client](https://github.com/iconara/ione/tree/master/examples/redis_client) is a more or less full featured Redis client that uses most of Ione's features.
|
30
|
+
* [http_client](https://github.com/iconara/ione/tree/master/examples/http_client) is a simplistic HTTP client that uses Ione and [http_parser.rb](http://rubygems.org/gems/http_parser.rb) to make HTTP GET request.
|
31
|
+
* [cql-rb](https://github.com/iconara/cql-rb) is a high performance Cassandra driver and where Ione was originally developed.
|
32
|
+
|
10
33
|
# How to contribute
|
11
34
|
|
12
35
|
[See CONTRIBUTING.md](CONTRIBUTING.md)
|
data/lib/ione/byte_buffer.rb
CHANGED
@@ -15,12 +15,17 @@ module Ione
|
|
15
15
|
# and reads read from the read buffer until it is empty, then a new write
|
16
16
|
# buffer is created and the old write buffer becomes the new read buffer.
|
17
17
|
class ByteBuffer
|
18
|
-
def initialize(initial_bytes=
|
18
|
+
def initialize(initial_bytes=nil)
|
19
19
|
@read_buffer = ''
|
20
|
-
@write_buffer = ''
|
21
20
|
@offset = 0
|
22
21
|
@length = 0
|
23
|
-
|
22
|
+
if initial_bytes && !initial_bytes.empty?
|
23
|
+
@write_buffer = initial_bytes.dup
|
24
|
+
@write_buffer.force_encoding(::Encoding::BINARY)
|
25
|
+
@length = @write_buffer.bytesize
|
26
|
+
else
|
27
|
+
@write_buffer = ''
|
28
|
+
end
|
24
29
|
end
|
25
30
|
|
26
31
|
# Returns the number of bytes in the buffer.
|
@@ -73,6 +78,7 @@ module Ione
|
|
73
78
|
# @return [Ione::ByteBuffer] itself
|
74
79
|
# @raise RangeError when there are not enough bytes in the buffer
|
75
80
|
def discard(n)
|
81
|
+
raise RangeError, 'Cannot discard a negative number of bytes' if n < 0
|
76
82
|
raise RangeError, "#{n} bytes to discard but only #{@length} available" if @length < n
|
77
83
|
@offset += n
|
78
84
|
@length -= n
|
@@ -86,6 +92,7 @@ module Ione
|
|
86
92
|
# with `Encoding::BINARY`.
|
87
93
|
# @raise RangeError when there are not enough bytes in the buffer
|
88
94
|
def read(n)
|
95
|
+
raise RangeError, 'Cannot read a negative number of bytes' if n < 0
|
89
96
|
raise RangeError, "#{n} bytes required but only #{@length} available" if @length < n
|
90
97
|
if @offset >= @read_buffer.bytesize
|
91
98
|
swap_buffers
|
@@ -165,6 +172,20 @@ module Ione
|
|
165
172
|
b
|
166
173
|
end
|
167
174
|
|
175
|
+
def index(substring, start_index=0)
|
176
|
+
if @offset >= @read_buffer.bytesize
|
177
|
+
swap_buffers
|
178
|
+
end
|
179
|
+
read_buffer_length = @read_buffer.bytesize
|
180
|
+
if start_index < read_buffer_length - @offset && (index = @read_buffer.index(substring, @offset + start_index))
|
181
|
+
index - @offset
|
182
|
+
elsif (index = @write_buffer.index(substring, start_index - read_buffer_length + @offset))
|
183
|
+
index + read_buffer_length - @offset
|
184
|
+
else
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
168
189
|
# Overwrite a portion of the buffer with new bytes.
|
169
190
|
#
|
170
191
|
# The number of bytes that will be replaced depend on the size of the
|
data/lib/ione/version.rb
CHANGED
data/spec/integration/io_spec.rb
CHANGED
@@ -48,78 +48,6 @@ describe 'An IO reactor' do
|
|
48
48
|
protocol_handlers.sample.data.should == 'hello world'
|
49
49
|
end
|
50
50
|
end
|
51
|
-
|
52
|
-
context 'when talking to Redis' do
|
53
|
-
let :protocol_handler do
|
54
|
-
begin
|
55
|
-
io_reactor.connect(ENV['SERVER_HOST'], 6379, 1).map { |c| IoSpec::RedisProtocolHandler.new(c) }.value
|
56
|
-
rescue Ione::Io::ConnectionError
|
57
|
-
nil
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
before do
|
62
|
-
io_reactor.start.value
|
63
|
-
end
|
64
|
-
|
65
|
-
after do
|
66
|
-
io_reactor.stop.value
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'can set a value' do
|
70
|
-
pending('Redis not running', unless: protocol_handler)
|
71
|
-
response = protocol_handler.send_request('SET', 'foo', 'bar').value
|
72
|
-
response.should == 'OK'
|
73
|
-
end
|
74
|
-
|
75
|
-
it 'can get a value' do
|
76
|
-
pending('Redis not running', unless: protocol_handler)
|
77
|
-
f = protocol_handler.send_request('SET', 'foo', 'bar').flat_map do
|
78
|
-
protocol_handler.send_request('GET', 'foo')
|
79
|
-
end
|
80
|
-
f.value.should == 'bar'
|
81
|
-
end
|
82
|
-
|
83
|
-
it 'can delete values' do
|
84
|
-
pending('Redis not running', unless: protocol_handler)
|
85
|
-
f = protocol_handler.send_request('SET', 'hello', 'world').flat_map do
|
86
|
-
protocol_handler.send_request('DEL', 'hello')
|
87
|
-
end
|
88
|
-
f.value.should == 1
|
89
|
-
end
|
90
|
-
|
91
|
-
it 'handles nil values' do
|
92
|
-
pending('Redis not running', unless: protocol_handler)
|
93
|
-
f = protocol_handler.send_request('DEL', 'hello').flat_map do
|
94
|
-
protocol_handler.send_request('GET', 'hello')
|
95
|
-
end
|
96
|
-
f.value.should be_nil
|
97
|
-
end
|
98
|
-
|
99
|
-
it 'handles errors' do
|
100
|
-
pending('Redis not running', unless: protocol_handler)
|
101
|
-
f = protocol_handler.send_request('SET', 'foo')
|
102
|
-
expect { f.value }.to raise_error("ERR wrong number of arguments for 'set' command")
|
103
|
-
end
|
104
|
-
|
105
|
-
it 'handles replies with multiple elements' do
|
106
|
-
pending('Redis not running', unless: protocol_handler)
|
107
|
-
f = protocol_handler.send_request('DEL', 'stuff')
|
108
|
-
f.value
|
109
|
-
f = protocol_handler.send_request('RPUSH', 'stuff', 'hello', 'world')
|
110
|
-
f.value.should == 2
|
111
|
-
f = protocol_handler.send_request('LRANGE', 'stuff', 0, 2)
|
112
|
-
f.value.should == ['hello', 'world']
|
113
|
-
end
|
114
|
-
|
115
|
-
it 'handles nil values when reading multiple elements' do
|
116
|
-
pending('Redis not running', unless: protocol_handler)
|
117
|
-
protocol_handler.send_request('DEL', 'things')
|
118
|
-
protocol_handler.send_request('HSET', 'things', 'hello', 'world')
|
119
|
-
f = protocol_handler.send_request('HMGET', 'things', 'hello', 'foo')
|
120
|
-
f.value.should == ['world', nil]
|
121
|
-
end
|
122
|
-
end
|
123
51
|
end
|
124
52
|
|
125
53
|
module IoSpec
|
@@ -141,143 +69,4 @@ module IoSpec
|
|
141
69
|
@lock.synchronize { @data << new_data }
|
142
70
|
end
|
143
71
|
end
|
144
|
-
|
145
|
-
class LineProtocolHandler
|
146
|
-
def initialize(connection)
|
147
|
-
@connection = connection
|
148
|
-
@connection.on_data(&method(:process_data))
|
149
|
-
@lock = Mutex.new
|
150
|
-
@buffer = ''
|
151
|
-
@requests = []
|
152
|
-
end
|
153
|
-
|
154
|
-
def on_line(&listener)
|
155
|
-
@line_listener = listener
|
156
|
-
end
|
157
|
-
|
158
|
-
def write(command_string)
|
159
|
-
@connection.write(command_string)
|
160
|
-
end
|
161
|
-
|
162
|
-
def process_data(new_data)
|
163
|
-
lines = []
|
164
|
-
@lock.synchronize do
|
165
|
-
@buffer << new_data
|
166
|
-
while newline_index = @buffer.index("\r\n")
|
167
|
-
line = @buffer.slice!(0, newline_index + 2)
|
168
|
-
line.chomp!
|
169
|
-
lines << line
|
170
|
-
end
|
171
|
-
end
|
172
|
-
lines.each do |line|
|
173
|
-
@line_listener.call(line) if @line_listener
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
class RedisProtocolHandler
|
179
|
-
def initialize(connection)
|
180
|
-
@line_protocol = LineProtocolHandler.new(connection)
|
181
|
-
@line_protocol.on_line(&method(:handle_line))
|
182
|
-
@lock = Mutex.new
|
183
|
-
@responses = []
|
184
|
-
@state = BaseState.new(method(:handle_response))
|
185
|
-
end
|
186
|
-
|
187
|
-
def send_request(*args)
|
188
|
-
promise = Ione::Promise.new
|
189
|
-
@lock.synchronize do
|
190
|
-
@responses << promise
|
191
|
-
end
|
192
|
-
request = "*#{args.size}\r\n"
|
193
|
-
args.each do |arg|
|
194
|
-
arg_str = arg.to_s
|
195
|
-
request << "$#{arg_str.bytesize}\r\n#{arg_str}\r\n"
|
196
|
-
end
|
197
|
-
@line_protocol.write(request)
|
198
|
-
promise.future
|
199
|
-
end
|
200
|
-
|
201
|
-
def handle_response(result, error=false)
|
202
|
-
promise = @lock.synchronize do
|
203
|
-
@responses.shift
|
204
|
-
end
|
205
|
-
if error
|
206
|
-
promise.fail(StandardError.new(result))
|
207
|
-
else
|
208
|
-
promise.fulfill(result)
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
def handle_line(line)
|
213
|
-
@state = @state.handle_line(line)
|
214
|
-
end
|
215
|
-
|
216
|
-
class State
|
217
|
-
def initialize(result_handler)
|
218
|
-
@result_handler = result_handler
|
219
|
-
end
|
220
|
-
|
221
|
-
def complete!(result)
|
222
|
-
@result_handler.call(result)
|
223
|
-
end
|
224
|
-
|
225
|
-
def fail!(message)
|
226
|
-
@result_handler.call(message, true)
|
227
|
-
end
|
228
|
-
end
|
229
|
-
|
230
|
-
class BulkState < State
|
231
|
-
def handle_line(line)
|
232
|
-
complete!(line)
|
233
|
-
BaseState.new(@result_handler)
|
234
|
-
end
|
235
|
-
end
|
236
|
-
|
237
|
-
class MultiBulkState < State
|
238
|
-
def initialize(result_handler, expected_elements)
|
239
|
-
super(result_handler)
|
240
|
-
@expected_elements = expected_elements
|
241
|
-
@elements = []
|
242
|
-
end
|
243
|
-
|
244
|
-
def handle_line(line)
|
245
|
-
if line.start_with?('$')
|
246
|
-
line.slice!(0, 1)
|
247
|
-
if line.to_i == -1
|
248
|
-
@elements << nil
|
249
|
-
end
|
250
|
-
else
|
251
|
-
@elements << line
|
252
|
-
end
|
253
|
-
if @elements.size == @expected_elements
|
254
|
-
complete!(@elements)
|
255
|
-
BaseState.new(@result_handler)
|
256
|
-
else
|
257
|
-
self
|
258
|
-
end
|
259
|
-
end
|
260
|
-
end
|
261
|
-
|
262
|
-
class BaseState < State
|
263
|
-
def handle_line(line)
|
264
|
-
next_state = self
|
265
|
-
first_char = line.slice!(0, 1)
|
266
|
-
case first_char
|
267
|
-
when '+' then complete!(line)
|
268
|
-
when ':' then complete!(line.to_i)
|
269
|
-
when '-' then fail!(line)
|
270
|
-
when '$'
|
271
|
-
if line.to_i == -1
|
272
|
-
complete!(nil)
|
273
|
-
else
|
274
|
-
next_state = BulkState.new(@result_handler)
|
275
|
-
end
|
276
|
-
when '*'
|
277
|
-
next_state = MultiBulkState.new(@result_handler, line.to_i)
|
278
|
-
end
|
279
|
-
next_state
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
72
|
end
|
@@ -175,6 +175,11 @@ module Ione
|
|
175
175
|
buffer.append('hello')
|
176
176
|
expect { buffer.discard(7) }.to raise_error(RangeError)
|
177
177
|
end
|
178
|
+
|
179
|
+
it 'raises an error when the specified number of bytes is negative' do
|
180
|
+
buffer.append('hello')
|
181
|
+
expect { buffer.discard(-7) }.to raise_error(RangeError)
|
182
|
+
end
|
178
183
|
end
|
179
184
|
|
180
185
|
describe '#read' do
|
@@ -196,6 +201,11 @@ module Ione
|
|
196
201
|
expect { buffer.discard(5).read(1) }.to raise_error(RangeError)
|
197
202
|
end
|
198
203
|
|
204
|
+
it 'raises an error when the specified number of bytes is negative' do
|
205
|
+
buffer.append('hello')
|
206
|
+
expect { buffer.read(-4) }.to raise_error(RangeError)
|
207
|
+
end
|
208
|
+
|
199
209
|
it 'returns a string with binary encoding' do
|
200
210
|
buffer.append('hello')
|
201
211
|
buffer.read(4).encoding.should == ::Encoding::BINARY
|
@@ -322,6 +332,30 @@ module Ione
|
|
322
332
|
end
|
323
333
|
end
|
324
334
|
|
335
|
+
describe '#index' do
|
336
|
+
it 'returns the first index of the specified substring' do
|
337
|
+
buffer.append('fizz buzz')
|
338
|
+
buffer.index('zz').should == 2
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'returns the first index of the specified substring, after the specified index' do
|
342
|
+
buffer.append('fizz buzz')
|
343
|
+
buffer.index('zz', 3).should == 7
|
344
|
+
end
|
345
|
+
|
346
|
+
it 'returns nil when the substring is not found' do
|
347
|
+
buffer.append('hello world')
|
348
|
+
buffer.index('zz').should be_nil
|
349
|
+
end
|
350
|
+
|
351
|
+
it 'returns the first index of the specified substring after the buffer has been modified' do
|
352
|
+
buffer.append('foo bar')
|
353
|
+
buffer.read(1)
|
354
|
+
buffer.append(' baz baz')
|
355
|
+
buffer.index('baz', 8).should == 11
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
325
359
|
context 'when reading and appending' do
|
326
360
|
it 'handles heavy churn' do
|
327
361
|
1000.times do
|
data/spec/ione/future_spec.rb
CHANGED
@@ -696,12 +696,26 @@ module Ione
|
|
696
696
|
future.should be_resolved
|
697
697
|
end
|
698
698
|
|
699
|
-
it '
|
699
|
+
it 'is completed' do
|
700
|
+
future.should be_completed
|
701
|
+
end
|
702
|
+
|
703
|
+
it 'is not failed' do
|
704
|
+
future.should_not be_failed
|
705
|
+
end
|
706
|
+
|
707
|
+
it 'calls its value callbacks immediately' do
|
700
708
|
value = nil
|
701
709
|
future.on_value { |v| value = v }
|
702
710
|
value.should == 'hello world'
|
703
711
|
end
|
704
712
|
|
713
|
+
it 'calls its complete callbacks immediately' do
|
714
|
+
f = nil
|
715
|
+
future.on_complete { |ff| f = ff }
|
716
|
+
f.should_not be_nil
|
717
|
+
end
|
718
|
+
|
705
719
|
it 'does not block on #value' do
|
706
720
|
future.value.should == 'hello world'
|
707
721
|
end
|
@@ -718,16 +732,30 @@ module Ione
|
|
718
732
|
end
|
719
733
|
|
720
734
|
context 'returns a future which' do
|
721
|
-
it 'is failed
|
735
|
+
it 'is failed' do
|
722
736
|
future.should be_failed
|
723
737
|
end
|
724
738
|
|
725
|
-
it '
|
739
|
+
it 'is completed' do
|
740
|
+
future.should be_completed
|
741
|
+
end
|
742
|
+
|
743
|
+
it 'is not resolved' do
|
744
|
+
future.should_not be_resolved
|
745
|
+
end
|
746
|
+
|
747
|
+
it 'call its failure callbacks immediately' do
|
726
748
|
error = nil
|
727
749
|
future.on_failure { |e| error = e }
|
728
750
|
error.message.should == 'bork'
|
729
751
|
end
|
730
752
|
|
753
|
+
it 'calls its complete callbacks immediately' do
|
754
|
+
f = nil
|
755
|
+
future.on_complete { |ff| f = ff }
|
756
|
+
f.should_not be_nil
|
757
|
+
end
|
758
|
+
|
731
759
|
it 'does not block on #value' do
|
732
760
|
expect { future.value }.to raise_error('bork')
|
733
761
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,8 +1,26 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
|
3
|
+
ENV['SERVER_HOST'] ||= '127.0.0.1'.freeze
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
4
6
|
|
5
7
|
require 'support/fake_server'
|
6
8
|
require 'support/await_helper'
|
7
9
|
|
8
|
-
ENV['
|
10
|
+
unless ENV['COVERAGE'] == 'no' || RUBY_ENGINE == 'rbx'
|
11
|
+
require 'coveralls'
|
12
|
+
require 'simplecov'
|
13
|
+
|
14
|
+
if ENV.include?('TRAVIS')
|
15
|
+
Coveralls.wear!
|
16
|
+
SimpleCov.formatter = Coveralls::SimpleCov::Formatter
|
17
|
+
end
|
18
|
+
|
19
|
+
SimpleCov.start do
|
20
|
+
add_group 'Source', 'lib'
|
21
|
+
add_group 'Unit tests', 'spec/cql'
|
22
|
+
add_group 'Integration tests', 'spec/integration'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'ione'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ione
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.pre2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Theo Hultberg
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-03-
|
11
|
+
date: 2014-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Reactive programming framework for Ruby, painless evented IO, futures
|
14
14
|
and an efficient byte buffer
|