ione-rpc 1.0.0.pre2 → 1.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
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