ione-rpc 1.0.0.pre2 → 1.0.0.pre3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c7643bab20779d92db259349ee30689f27f00ca
4
- data.tar.gz: ac4e876e133758cc94c5e52682685865225d7cdc
3
+ metadata.gz: 98f7be7547ebdcd263f54d286682869bd629dfdc
4
+ data.tar.gz: 6d331deff1f746ce7c69f3914f9527f115701d08
5
5
  SHA512:
6
- metadata.gz: 54903ddee404d09c58056af6e1766d8d4c2323037cbfdfac42c66c82ad11f152425fc542eab87adf05ea4ef67afb32e351e011ff906b4329edc2f1ba0af4b49e
7
- data.tar.gz: cfc8c07ab978079837a309b736505031ca422441c6c1083d81a23e55a82a0729528fb3c9874819ce5a9755bc4a6711d0946bdfcc64a1298db78fac5e167ae86a
6
+ metadata.gz: 4ecab7afd0cc50ecfcd9475d8f2b04da77aac64b063e022b29498556241e0bddd64329291fb2c64299cc670d22c9368203c4cb66c5ab5113d4aeb2bac035bc2f
7
+ data.tar.gz: 33a712ea45a402f0f5fd501fc3b4eb14af603c56c1496424512252e2c387f9801a0fc8a6b2d91e45dc25e45f6c70a50939bf60222ad16211b1e4f44c59695854
@@ -48,7 +48,36 @@ module Ione
48
48
 
49
49
  # A client is connected when it has at least one open connection.
50
50
  def connected?
51
- @lock.synchronize { @connections.any? }
51
+ @lock.lock
52
+ @connections.any?
53
+ ensure
54
+ @lock.unlock
55
+ end
56
+
57
+ # Returns an array of info and statistics about the currently open connections.
58
+ #
59
+ # Each open connection is represented by a hash which includes the keys
60
+ #
61
+ # * `:host` and `:port`
62
+ # * `:max_channels`: the maximum number of messages to send concurrently
63
+ # * `:active_channels`: the number of sent messages that have not yet
64
+ # received a response
65
+ # * `:queued_messages`: the number of messages that couldn't be sent
66
+ # immediately because all channels were occupied and thus had to be queued.
67
+ # * `:sent_messages`: the total number of messages sent since the connection
68
+ # was opened
69
+ # * `:received_responses`: the total number of responses received since
70
+ # the connection was opened
71
+ # * `:timeouts`: the number of sent messages that did not receive a response
72
+ # before their timeout expired
73
+ #
74
+ # @return [Array<Hash>] an array of hashes that each contain info and
75
+ # statistics from an open connection.
76
+ def connection_stats
77
+ @lock.lock
78
+ @connections.map(&:stats)
79
+ ensure
80
+ @lock.unlock
52
81
  end
53
82
 
54
83
  # Start the client and connect to all hosts. This also starts the IO
@@ -150,7 +179,14 @@ module Ione
150
179
  # error response – that is protocol specific and up to the implementation
151
180
  # to handle), or when there was no connection open.
152
181
  def send_request(request, connection=nil, timeout=nil)
153
- connection = connection || @lock.synchronize { choose_connection(@connections, request) }
182
+ unless connection
183
+ @lock.lock
184
+ begin
185
+ connection = choose_connection(@connections, request)
186
+ ensure
187
+ @lock.unlock
188
+ end
189
+ end
154
190
  if connection
155
191
  f = connection.send_message(request, timeout)
156
192
  f = f.fallback do |error|
@@ -14,22 +14,46 @@ module Ione
14
14
  @channels = [nil] * max_channels
15
15
  @queue = []
16
16
  @encode_eagerly = @codec.recoding?
17
+ @sent_messages = 0
18
+ @received_responses = 0
19
+ @timeouts = 0
20
+ end
21
+
22
+ def stats
23
+ @lock.lock
24
+ {
25
+ :host => @host,
26
+ :port => @port,
27
+ :max_channels => @channels.size,
28
+ :active_channels => @channels.size - @channels.count(nil),
29
+ :queued_messages => @queue.size,
30
+ :sent_messages => @sent_messages,
31
+ :received_responses => @received_responses,
32
+ :timeouts => @timeouts,
33
+ }
34
+ ensure
35
+ @lock.unlock
17
36
  end
18
37
 
19
38
  def send_message(request, timeout=nil)
20
39
  promise = Ione::Promise.new
21
- channel = @lock.synchronize do
22
- take_channel(promise)
40
+ channel = nil
41
+ @lock.lock
42
+ begin
43
+ channel = take_channel(promise)
44
+ @sent_messages += 1 if channel
45
+ ensure
46
+ @lock.unlock
23
47
  end
24
48
  if channel
25
49
  @connection.write(@codec.encode(request, channel))
26
50
  else
27
- @lock.synchronize do
28
- if @encode_eagerly
29
- @queue << [@codec.encode(request, -1), promise]
30
- else
31
- @queue << [request, promise]
32
- end
51
+ pair = [@encode_eagerly ? @codec.encode(request, -1) : request, promise]
52
+ @lock.lock
53
+ begin
54
+ @queue << pair
55
+ ensure
56
+ @lock.unlock
33
57
  end
34
58
  end
35
59
  if timeout
@@ -37,6 +61,7 @@ module Ione
37
61
  unless promise.future.completed?
38
62
  error = Rpc::TimeoutError.new('No response received within %ss' % timeout.to_s)
39
63
  promise.fail(error)
64
+ @timeouts += 1
40
65
  end
41
66
  end
42
67
  end
@@ -46,10 +71,14 @@ module Ione
46
71
  private
47
72
 
48
73
  def handle_message(response, channel)
49
- promise = @lock.synchronize do
74
+ promise = nil
75
+ @lock.lock
76
+ begin
50
77
  promise = @channels[channel]
51
78
  @channels[channel] = nil
52
- promise
79
+ @received_responses += 1 if promise
80
+ ensure
81
+ @lock.unlock
53
82
  end
54
83
  if promise && !promise.future.completed?
55
84
  promise.fulfill(response)
@@ -58,24 +87,26 @@ module Ione
58
87
  end
59
88
 
60
89
  def flush_queue
61
- @lock.synchronize do
62
- count = 0
63
- max = @queue.size
64
- while count < max
65
- request, promise = @queue[count]
66
- if (channel = take_channel(promise))
67
- if @encode_eagerly
68
- @connection.write(@codec.recode(request, channel))
69
- else
70
- @connection.write(@codec.encode(request))
71
- end
72
- count += 1
90
+ @lock.lock
91
+ count = 0
92
+ max = @queue.size
93
+ while count < max
94
+ request, promise = @queue[count]
95
+ if (channel = take_channel(promise))
96
+ if @encode_eagerly
97
+ @connection.write(@codec.recode(request, channel))
73
98
  else
74
- break
99
+ @connection.write(@codec.encode(request))
75
100
  end
101
+ count += 1
102
+ else
103
+ break
76
104
  end
77
- @queue = @queue.drop(count)
78
105
  end
106
+ @sent_messages += count
107
+ @queue = @queue.drop(count)
108
+ ensure
109
+ @lock.unlock
79
110
  end
80
111
 
81
112
  def take_channel(promise)
@@ -2,10 +2,21 @@
2
2
 
3
3
  module Ione
4
4
  module Rpc
5
+ CodecError = Class.new(StandardError)
6
+
5
7
  # Codecs are used to encode and decode the messages sent between the client
6
8
  # and server. Codecs must be able to decode frames in a streaming fashion,
7
9
  # i.e. frames that come in pieces.
8
10
  #
11
+ # Codecs can be configured to compress frame bodies by giving them a
12
+ # compressor. A compressor is an object that responds to `#compress`,
13
+ # `#decompress` and `#compress?`. The first takes a string and returns
14
+ # a compressed string, the second does the reverse and the third is a way
15
+ # for the compressor to advice the codec whether or not it's meaningful to
16
+ # encode the frame at all. `#compress?` will get the same argument as
17
+ # `#compress` and should return true or false. It could for example return
18
+ # true when the frame size is over a threshold value.
19
+ #
9
20
  # If you want to control how messages are framed you can implement your
10
21
  # own codec from scratch by implementing {#encode} and {#decode}, but most
11
22
  # of the time you should only need to implement {#encode_message} and
@@ -15,48 +26,90 @@ module Ione
15
26
  #
16
27
  # Codecs must be stateless.
17
28
  #
29
+ # Since the IO layer has no knowledge about the framing it can't know
30
+ # how many bytes are needed before a frame can be fully decoded so instead
31
+ # the codec needs to be able to process partial frames. At the same time
32
+ # a codec can be used concurrently and must be stateless. To support partial
33
+ # decoding a codec can return a state instead of a decoded message until
34
+ # a complete frame can be decoded.
35
+ #
36
+ # The codec must return three values: the last value must be true if a
37
+ # if a message was decoded fully, and false if not enough bytes were
38
+ # available. When it is true the first piece is the message and the second
39
+ # the channel. When it is false the first piece is the partial decoded
40
+ # state (this can be any object at all) and the second is nil. The partial
41
+ # decoded state will be passed in as the second argument on the next call.
42
+ #
43
+ # In other words: the first time {#decode} is called the second argument
44
+ # will be nil, but on subsequent calls, until the third return value is
45
+ # true, the second argument will be whatever was returned by the previous
46
+ # call.
47
+ #
48
+ # The buffer might contain more bytes than needed to decode a frame, and
49
+ # the implementation must not consume these. The implementation must
50
+ # consume all of the bytes of the current frame, but none of the bytes of
51
+ # the next frame.
52
+ #
18
53
  # Codecs must also make sure that these (conceptual) invariants hold:
19
54
  # `decode(encode(message, channel)) == [message, channel]` and
20
55
  # `encode(decode(frame)) == frame` (the return values are not entirely
21
56
  # compatible, but the concept should be clear).
22
57
  class Codec
58
+ # @param [Hash] options
59
+ # @option options [Object] :compressor a compressor to use to compress
60
+ # and decompress frames (see above for the required interface it needs
61
+ # to implement).
62
+ # @option options [Object] :lazy whether or not to decode the frames
63
+ # immediately or return an object that can be decoded later, see {#decode}.
64
+ def initialize(options={})
65
+ @compressor = options[:compressor]
66
+ @lazy = options[:lazy]
67
+ end
68
+
23
69
  # Encodes a frame with a header that includes the frame size and channel,
24
70
  # and the message as body.
25
71
  #
72
+ # Will compress the frame body and set the compression flag when the codec
73
+ # has been configured with a compressor, and it says the frame should be
74
+ # compressed.
75
+ #
26
76
  # @param [Object] message the message to encode
27
77
  # @param [Integer] channel the channel to encode into the frame
28
78
  # @return [String] an encoded frame with the message and channel
29
79
  def encode(message, channel)
30
80
  data = encode_message(message)
31
- [2, 0, channel, data.bytesize, data.to_s].pack(FRAME_V2_FORMAT)
81
+ flags = 0
82
+ if @compressor && @compressor.compress?(data)
83
+ data = @compressor.compress(data)
84
+ flags |= COMPRESSION_FLAG
85
+ end
86
+ [2, flags, channel, data.bytesize, data.to_s].pack(FRAME_V2_FORMAT)
32
87
  end
33
88
 
34
89
  # Decodes a frame, piece by piece if necessary.
35
90
  #
36
- # Since the IO layer has no knowledge about the framing it can't know
37
- # how many bytes are needed before a frame can be fully decoded so instead
38
- # the codec needs to be able to process partial frames. At the same time
39
- # a codec can be used concurrently and must be stateless. To support partial
40
- # decoding a codec can return a state instead of a decoded message until
41
- # a complete frame can be decoded.
42
- #
43
- # The codec must return three values: the last value must be true if a
44
- # if a message was decoded fully, and false if not enough bytes were
45
- # available. When it is true the first piece is the message and the second
46
- # the channel. When it is false the first piece is the partial decoded
47
- # state (this can be any object at all) and the second is nil. The partial
48
- # decoded state will be passed in as the second argument on the next call.
91
+ # Since the codec can be called before a full frame has been received it
92
+ # returns a three-tuple where the last component is a boolean flag that says
93
+ # whether or not the frame is completely decoded. As long as the buffer
94
+ # does not contain a full frame the first component will be a state object
95
+ # which must be passed in as the second argument on the next call to
96
+ # {#decode}. When the buffer contains a full frame {#decode_message} will
97
+ # be called and the decoded message returned as the first component, the
98
+ # second will be the channel and the third will be true.
49
99
  #
50
- # In other words: the first time {#decode} is called the second argument
51
- # will be nil, but on subsequent calls, until the third return value is
52
- # true, the second argument will be whatever was returned by the previous
53
- # call.
100
+ # When the compression flag is set the codec uses the configured compressor
101
+ # to decode the frame before passing the decompressed bytes to
102
+ # {#decode_message}.
54
103
  #
55
- # The buffer might contain more bytes than needed to decode a frame, and
56
- # the implementation must not consume these. The implementation must
57
- # consume all of the bytes of the current frame, but none of the bytes of
58
- # the next frame.
104
+ # Since the IO thread can spend a significant proportion of its time doing
105
+ # frame decoding (the encoding is done in the calling thread) there is an
106
+ # option to create "lazy" codecs that return an intermediate object instead
107
+ # of a decoded message. When the codec is created with the `:lazy` option
108
+ # {#decode} returns an object with a `#decode` method, that in turn returns
109
+ # the decoded message.
59
110
  #
111
+ # @raise [Ione::Rpc::CodecError] when a frame has the compression flag set
112
+ # and the codec is not configured with a compressor.
60
113
  # @param [Ione::ByteBuffer] buffer the byte buffer that contains the frame
61
114
  # data. The byte buffer is owned by the caller and should only be read from.
62
115
  # @param [Object, nil] state the first value returned from the previous
@@ -73,12 +126,31 @@ module Ione
73
126
  state.read_header
74
127
  end
75
128
  if state.body_ready?
76
- return decode_message(state.read_body), state.channel, true
129
+ body = state.read_body
130
+ channel = state.channel
131
+ if @lazy
132
+ message = LazilyDecodedFrame.new(self, state, body)
133
+ else
134
+ message = decode_body(state, body)
135
+ end
136
+ return message, state.channel, true
77
137
  else
78
138
  return state, nil, false
79
139
  end
80
140
  end
81
141
 
142
+ # @private
143
+ def decode_body(state, body)
144
+ if state.compressed?
145
+ if @compressor
146
+ body = @compressor.decompress(body)
147
+ else
148
+ raise CodecError, 'Compressed frame received but no compressor available'
149
+ end
150
+ end
151
+ decode_message(body)
152
+ end
153
+
82
154
  # Whether or not this codec supports channel recoding, see {#recode}.
83
155
  def recoding?
84
156
  true
@@ -127,6 +199,7 @@ module Ione
127
199
  def read_header
128
200
  n = @buffer.read_short
129
201
  @version = n >> 8
202
+ @compressed = n & COMPRESSION_FLAG == COMPRESSION_FLAG
130
203
  if @version == 1
131
204
  @channel = n & 0xff
132
205
  else
@@ -146,6 +219,23 @@ module Ione
146
219
  def body_ready?
147
220
  @length && @buffer.size >= @length
148
221
  end
222
+
223
+ def compressed?
224
+ @compressed
225
+ end
226
+ end
227
+
228
+ # @private
229
+ class LazilyDecodedFrame
230
+ def initialize(codec, state, body)
231
+ @codec = codec
232
+ @state = state
233
+ @body = body
234
+ end
235
+
236
+ def decode
237
+ @decoded ||= @codec.decode_body(@state, @body)
238
+ end
149
239
  end
150
240
 
151
241
  private
@@ -153,6 +243,7 @@ module Ione
153
243
  FRAME_V1_FORMAT = 'ccNa*'.freeze
154
244
  FRAME_V2_FORMAT = 'ccnNa*'.freeze
155
245
  CHANNEL_FORMAT = 'n'.freeze
246
+ COMPRESSION_FLAG = 1
156
247
  end
157
248
 
158
249
  # A codec that works with encoders like JSON, MessagePack, YAML and others
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ione
4
4
  module Rpc
5
- VERSION = '1.0.0.pre2'.freeze
5
+ VERSION = '1.0.0.pre3'.freeze
6
6
  end
7
7
  end
@@ -138,6 +138,58 @@ module Ione
138
138
  end
139
139
  end
140
140
  end
141
+
142
+ describe '#stats' do
143
+ context 'returns a hash that' do
144
+ it 'contains the host' do
145
+ peer.stats.should include(host: connection.host)
146
+ end
147
+
148
+ it 'contains the port' do
149
+ peer.stats.should include(port: connection.port)
150
+ end
151
+
152
+ it 'contains the max number of channels' do
153
+ peer.stats.should include(max_channels: max_channels)
154
+ end
155
+
156
+ it 'contains the number of active channels' do
157
+ peer.stats.should include(active_channels: 0)
158
+ peer.send_message('hello')
159
+ peer.stats.should include(active_channels: 1)
160
+ end
161
+
162
+ it 'contains the number of queued messages' do
163
+ peer.stats.should include(queued_messages: 0)
164
+ 17.times { peer.send_message('hello') }
165
+ peer.stats.should include(queued_messages: 1)
166
+ connection.data_listener.call('bar@000')
167
+ peer.stats.should include(queued_messages: 0)
168
+ end
169
+
170
+ it 'contains the number of sent messages' do
171
+ peer.stats.should include(sent_messages: 0)
172
+ 17.times { peer.send_message('hello') }
173
+ connection.data_listener.call('bar@000')
174
+ peer.stats.should include(sent_messages: 17)
175
+ end
176
+
177
+ it 'contains the number of received responses' do
178
+ peer.stats.should include(received_responses: 0)
179
+ 17.times { peer.send_message('hello') }
180
+ connection.data_listener.call('bar@000')
181
+ connection.data_listener.call('bar@000')
182
+ peer.stats.should include(received_responses: 2)
183
+ end
184
+
185
+ it 'contains the number of timed out responses' do
186
+ peer.stats.should include(timeouts: 0)
187
+ peer.send_message('hello', 1)
188
+ scheduler.timer_promises.first.fulfill
189
+ peer.stats.should include(timeouts: 1)
190
+ end
191
+ end
192
+ end
141
193
  end
142
194
  end
143
195
  end
@@ -419,6 +419,19 @@ module Ione
419
419
  end
420
420
  end
421
421
 
422
+ describe '#connection_stats' do
423
+ it 'is empty when no connections are open' do
424
+ client.connection_stats.should be_empty
425
+ end
426
+
427
+ it 'returns an array of the host, port and statistics from each open connection' do
428
+ client.start.value
429
+ stats = client.connection_stats
430
+ stats.should have(3).items
431
+ stats.should include(host: 'node1.example.com', port: 5432, fake_stats: true)
432
+ end
433
+ end
434
+
422
435
  context 'when disconnected' do
423
436
  it 'logs that the connection closed' do
424
437
  client.start.value
@@ -605,6 +618,14 @@ module ClientSpec
605
618
  @requests = []
606
619
  end
607
620
 
621
+ def stats
622
+ {
623
+ :host => @peer_connection.host,
624
+ :port => @peer_connection.port,
625
+ :fake_stats => true,
626
+ }
627
+ end
628
+
608
629
  def closed?
609
630
  !!@closed
610
631
  end
@@ -34,6 +34,37 @@ module Ione
34
34
  it 'encodes the object as JSON' do
35
35
  encoded_message[8..-1].should == '{"foo":"bar","baz":42}'
36
36
  end
37
+
38
+ context 'with a compressor' do
39
+ let :codec do
40
+ CodecSpec::JsonCodec.new(compressor: compressor)
41
+ end
42
+
43
+ let :compressor do
44
+ double(:compressor)
45
+ end
46
+
47
+ before do
48
+ compressor.stub(:compress?).and_return(true)
49
+ compressor.stub(:compress).and_return('FAKECOMPRESSEDFREAME')
50
+ end
51
+
52
+ it 'sets the compression flag' do
53
+ encoded_message = codec.encode(object, 42)
54
+ encoded_message[1].unpack('c').should == [1]
55
+ end
56
+
57
+ it 'compresses the frame body' do
58
+ encoded_message = codec.encode(object, 42)
59
+ encoded_message[4, 4].unpack('N').should == [20]
60
+ encoded_message[8..-1].should == 'FAKECOMPRESSEDFREAME'
61
+ end
62
+
63
+ it 'does not compress the frame body when the compressor advices against it' do
64
+ compressor.stub(:compress?).and_return(false)
65
+ encoded_message[8..-1].should == '{"foo":"bar","baz":42}'
66
+ end
67
+ end
37
68
  end
38
69
 
39
70
  describe '#decode' do
@@ -88,6 +119,50 @@ module Ione
88
119
  message.should == object
89
120
  channel.should == 42
90
121
  end
122
+
123
+ context 'when the frame is compressed' do
124
+ let :codec do
125
+ CodecSpec::JsonCodec.new(compressor: compressor)
126
+ end
127
+
128
+ let :compressor do
129
+ double(:compressor)
130
+ end
131
+
132
+ let :encoded_message do
133
+ %(\x02\x01\x00\x2a\x00\x00\x00\x13FAKECOMPRESSEDFRAME)
134
+ end
135
+
136
+ before do
137
+ compressor.stub(:decompress).with('FAKECOMPRESSEDFRAME').and_return('{"foo":"bar","baz":42}')
138
+ end
139
+
140
+ it 'decompresses the frame before decoding it' do
141
+ message, channel, complete = codec.decode(Ione::ByteBuffer.new(encoded_message), nil)
142
+ complete.should be_true
143
+ message.should == object
144
+ channel.should == 42
145
+ end
146
+
147
+ it 'raises an error when the frame is compressed and the codec has not been configured with a compressor' do
148
+ codec = CodecSpec::JsonCodec.new
149
+ buffer = Ione::ByteBuffer.new(encoded_message)
150
+ expect { codec.decode(buffer, nil) }.to raise_error(CodecError, 'Compressed frame received but no compressor available')
151
+ end
152
+ end
153
+
154
+ context 'when the codec is lazy' do
155
+ let :codec do
156
+ CodecSpec::JsonCodec.new(lazy: true)
157
+ end
158
+
159
+ it 'returns an object with a #decode method instead of the message' do
160
+ message, channel, complete = codec.decode(Ione::ByteBuffer.new(encoded_message), nil)
161
+ complete.should be_true
162
+ channel.should == 42
163
+ message.decode.should == object
164
+ end
165
+ end
91
166
  end
92
167
 
93
168
  describe '#recode' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ione-rpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre2
4
+ version: 1.0.0.pre3
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-06-10 00:00:00.000000000 Z
11
+ date: 2014-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ione