krakow 0.2.2 → 0.3.0
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.
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +25 -0
- data/LICENSE +13 -0
- data/README.md +62 -9
- data/krakow.gemspec +3 -1
- data/lib/krakow/command/cls.rb +3 -4
- data/lib/krakow/command/fin.rb +13 -4
- data/lib/krakow/command/identify.rb +22 -9
- data/lib/krakow/command/mpub.rb +14 -4
- data/lib/krakow/command/nop.rb +3 -4
- data/lib/krakow/command/pub.rb +15 -5
- data/lib/krakow/command/rdy.rb +13 -4
- data/lib/krakow/command/req.rb +14 -4
- data/lib/krakow/command/sub.rb +14 -4
- data/lib/krakow/command/touch.rb +13 -4
- data/lib/krakow/command.rb +25 -3
- data/lib/krakow/connection.rb +286 -60
- data/lib/krakow/connection_features/deflate.rb +26 -1
- data/lib/krakow/connection_features/snappy_frames.rb +34 -3
- data/lib/krakow/connection_features/ssl.rb +43 -1
- data/lib/krakow/connection_features.rb +1 -0
- data/lib/krakow/consumer.rb +162 -49
- data/lib/krakow/discovery.rb +17 -6
- data/lib/krakow/distribution/default.rb +61 -33
- data/lib/krakow/distribution.rb +107 -57
- data/lib/krakow/exceptions.rb +14 -0
- data/lib/krakow/frame_type/error.rb +13 -7
- data/lib/krakow/frame_type/message.rb +47 -4
- data/lib/krakow/frame_type/response.rb +14 -4
- data/lib/krakow/frame_type.rb +20 -8
- data/lib/krakow/producer/http.rb +95 -6
- data/lib/krakow/producer.rb +60 -17
- data/lib/krakow/utils/lazy.rb +99 -40
- data/lib/krakow/utils/logging.rb +11 -0
- data/lib/krakow/utils.rb +3 -0
- data/lib/krakow/version.rb +3 -1
- data/lib/krakow.rb +1 -0
- metadata +11 -11
- data/Gemfile +0 -5
- data/Gemfile.lock +0 -34
- data/test/spec.rb +0 -81
- data/test/specs/consumer.rb +0 -49
- data/test/specs/http_producer.rb +0 -123
- data/test/specs/producer.rb +0 -20
data/lib/krakow/connection.rb
CHANGED
@@ -1,13 +1,29 @@
|
|
1
|
-
require 'krakow
|
1
|
+
require 'krakow'
|
2
2
|
require 'celluloid/io'
|
3
|
-
require 'celluloid/autostart'
|
4
3
|
|
5
4
|
module Krakow
|
5
|
+
|
6
|
+
# Provides TCP connection to NSQD
|
6
7
|
class Connection
|
7
8
|
|
9
|
+
# Generate identifier for connection
|
10
|
+
#
|
11
|
+
# @param host [String]
|
12
|
+
# @param port [String, Integer]
|
13
|
+
# @param topic [String]
|
14
|
+
# @param channel [String]
|
15
|
+
# @return [String]
|
16
|
+
def self.identifier(host, port, topic, channel)
|
17
|
+
[host, port, topic, channel].compact.join('__')
|
18
|
+
end
|
19
|
+
|
8
20
|
include Utils::Lazy
|
21
|
+
# @!parse include Krakow::Utils::Lazy::InstanceMethods
|
22
|
+
# @!parse extend Krakow::Utils::Lazy::ClassMethods
|
23
|
+
|
9
24
|
include Celluloid::IO
|
10
25
|
|
26
|
+
# Available connection features
|
11
27
|
FEATURES = [
|
12
28
|
:max_rdy_count,
|
13
29
|
:max_msg_timeout,
|
@@ -19,63 +35,125 @@ module Krakow
|
|
19
35
|
:snappy,
|
20
36
|
:sample_rate
|
21
37
|
]
|
38
|
+
|
39
|
+
# List of features that may not be enabled together
|
22
40
|
EXCLUSIVE_FEATURES = [[:snappy, :deflate]]
|
41
|
+
|
42
|
+
# List of features that may be enabled by the client
|
23
43
|
ENABLEABLE_FEATURES = [:tls_v1, :snappy, :deflate]
|
24
44
|
|
25
45
|
finalizer :goodbye_my_love!
|
26
46
|
|
27
|
-
|
47
|
+
# @return [Hash] current configuration for endpoint
|
48
|
+
attr_reader :endpoint_settings
|
49
|
+
# @return [Socket-ish] underlying socket like instance
|
50
|
+
attr_reader :socket
|
51
|
+
|
52
|
+
attr_reader :connector, :reconnector, :reconnect_notifier, :responder, :running
|
53
|
+
|
54
|
+
# @!group Attributes
|
28
55
|
|
56
|
+
# @!macro [attach] attribute
|
57
|
+
# @!method $1
|
58
|
+
# @return [$2] the $1 $0
|
59
|
+
# @!method $1?
|
60
|
+
# @return [TrueClass, FalseClass] truthiness of the $1 $0
|
61
|
+
attribute :host, String, :required => true
|
62
|
+
attribute :port, [String,Integer], :required => true
|
63
|
+
attribute :topic, String
|
64
|
+
attribute :channel, String
|
65
|
+
attribute :version, String, :default => 'v2'
|
66
|
+
attribute :queue, Queue, :default => ->{ Queue.new }
|
67
|
+
attribute :callbacks, Hash, :default => ->{ Hash.new }
|
68
|
+
attribute :responses, Queue, :default => ->{ Queue.new }
|
69
|
+
attribute :notifier, Celluloid::Actor
|
70
|
+
attribute :features, Hash, :default => ->{ Hash.new }
|
71
|
+
attribute :response_wait, Numeric, :default => 1.0
|
72
|
+
attribute :response_interval, Numeric, :default => 0.03
|
73
|
+
attribute :error_wait, Numeric, :default => 0
|
74
|
+
attribute :enforce_features, [TrueClass,FalseClass], :default => true
|
75
|
+
attribute :features_args, Hash, :default => ->{ Hash.new }
|
76
|
+
|
77
|
+
# @!endgroup
|
78
|
+
|
79
|
+
# Create new instance
|
80
|
+
#
|
81
|
+
# @param args [Hash]
|
82
|
+
# @option args [String] :host (required) server host
|
83
|
+
# @option args [String, Numeric] :port (required) server port
|
84
|
+
# @option args [String] :version
|
85
|
+
# @option args [Queue] :queue received message queue
|
86
|
+
# @option args [Hash] :callbacks
|
87
|
+
# @option args [Queue] :responses received responses queue
|
88
|
+
# @option args [Celluloid::Actor] :notifier actor to notify on new message
|
89
|
+
# @option args [Hash] :features features to enable
|
90
|
+
# @option args [Numeric] :response_wait time to wait for response
|
91
|
+
# @option args [Numeric] :response_interval sleep interval for wait loop
|
92
|
+
# @option args [Numeric] :error_wait time to wait for error response
|
93
|
+
# @option args [TrueClass, FalseClass] :enforce_features fail if features are unavailable
|
94
|
+
# @option args [Hash] :feature_args options for connection features
|
29
95
|
def initialize(args={})
|
30
96
|
super
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
arguments[:version] ||= 'v2'
|
39
|
-
arguments[:features] ||= {}
|
40
|
-
arguments[:response_wait] ||= 1
|
41
|
-
arguments[:error_wait] ||= 0.4
|
42
|
-
if(arguments[:enforce_features].nil?)
|
43
|
-
arguments[:enforce_features] = true
|
44
|
-
end
|
45
|
-
@socket = TCPSocket.new(host, port)
|
97
|
+
@connector = Mutex.new
|
98
|
+
@reconnector = Mutex.new
|
99
|
+
@responder = Mutex.new
|
100
|
+
@reconnect_notifier = Celluloid::Signals.new
|
101
|
+
@socket_retries = 0
|
102
|
+
@socket_max_retries = 10
|
103
|
+
@reconnect_pause = 0.5
|
46
104
|
@endpoint_settings = {}
|
105
|
+
@running = false
|
47
106
|
end
|
48
107
|
|
108
|
+
# @return [String] identifier for this connection
|
109
|
+
def identifier
|
110
|
+
self.class.identifier(host, port, topic, channel)
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [String] stringify object
|
49
114
|
def to_s
|
50
115
|
"<#{self.class.name}:#{object_id} {#{host}:#{port}}>"
|
51
116
|
end
|
52
117
|
|
53
118
|
# Initialize the connection
|
119
|
+
#
|
120
|
+
# @return [nil]
|
54
121
|
def init!
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
info 'Connection initialized'
|
122
|
+
connector.synchronize do
|
123
|
+
connect!
|
124
|
+
end
|
125
|
+
nil
|
60
126
|
end
|
61
127
|
|
62
|
-
# message
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# response
|
128
|
+
# Send message to remote server
|
129
|
+
#
|
130
|
+
# @param message [Krakow::Message] message to send
|
131
|
+
# @return [TrueClass, Krakow::FrameType] response if expected or true
|
66
132
|
def transmit(message)
|
67
133
|
output = message.to_line
|
68
|
-
debug ">>> #{output}"
|
69
|
-
socket.write output
|
70
134
|
response_wait = wait_time_for(message)
|
71
|
-
|
72
|
-
|
135
|
+
if(response_wait > 0)
|
136
|
+
transmit_with_response(message, response_wait)
|
137
|
+
else
|
138
|
+
debug ">>> #{output}"
|
139
|
+
safe_socket{|socket| socket.write output }
|
140
|
+
true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Sends message and waits for response
|
145
|
+
#
|
146
|
+
# @param message [Krakow::Message] message to send
|
147
|
+
# @return [Krakow::FrameType] response
|
148
|
+
def transmit_with_response(message, wait_time)
|
149
|
+
responder.synchronize do
|
150
|
+
safe_socket{|socket| socket.write(message.to_line) }
|
151
|
+
responses.clear
|
73
152
|
response = nil
|
74
|
-
(
|
153
|
+
(wait_time / response_interval).to_i.times do |i|
|
75
154
|
response = responses.pop unless responses.empty?
|
76
155
|
break if response
|
77
|
-
|
78
|
-
sleep(0.1)
|
156
|
+
sleep(response_interval)
|
79
157
|
end
|
80
158
|
if(response)
|
81
159
|
message.response = response
|
@@ -90,23 +168,32 @@ module Krakow
|
|
90
168
|
abort Error::BadResponse::NoResponse.new "No response provided for message #{message}"
|
91
169
|
end
|
92
170
|
end
|
93
|
-
else
|
94
|
-
true
|
95
171
|
end
|
96
172
|
end
|
97
173
|
|
98
|
-
#
|
174
|
+
# Destructor method for cleanup
|
175
|
+
#
|
176
|
+
# @return [nil]
|
99
177
|
def goodbye_my_love!
|
100
178
|
debug 'Tearing down connection'
|
101
179
|
if(socket && !socket.closed?)
|
102
|
-
socket.write Command::Cls.new.to_line
|
103
|
-
|
180
|
+
[lambda{ socket.write Command::Cls.new.to_line}, lambda{socket.close}].each do |action|
|
181
|
+
begin
|
182
|
+
action.call
|
183
|
+
rescue IOError, SystemCallError => e
|
184
|
+
warn "Socket error encountered during teardown: #{e.class}: #{e}"
|
185
|
+
end
|
186
|
+
end
|
104
187
|
end
|
105
188
|
@socket = nil
|
106
189
|
info 'Connection torn down'
|
190
|
+
nil
|
107
191
|
end
|
108
192
|
|
109
|
-
# Receive
|
193
|
+
# Receive from server
|
194
|
+
#
|
195
|
+
# @return [Krakow::FrameType, nil] message or nothing if read was empty
|
196
|
+
# @raise [Error::ConnectionUnavailable] socket is closed
|
110
197
|
def receive
|
111
198
|
debug 'Read wait for frame start'
|
112
199
|
buf = socket.recv(8)
|
@@ -115,7 +202,7 @@ module Krakow
|
|
115
202
|
debug "<<< #{buf.inspect}"
|
116
203
|
struct = FrameType.decode(buf)
|
117
204
|
debug "Decoded structure: #{struct.inspect}"
|
118
|
-
struct[:data] = socket.
|
205
|
+
struct[:data] = socket.read(struct[:size])
|
119
206
|
debug "<<< #{struct[:data].inspect}"
|
120
207
|
@receiving = false
|
121
208
|
frame = FrameType.build(struct)
|
@@ -123,31 +210,43 @@ module Krakow
|
|
123
210
|
frame
|
124
211
|
else
|
125
212
|
if(socket.closed?)
|
126
|
-
|
213
|
+
abort Error::ConnectionUnavailable.new("#{self} encountered closed socket!")
|
127
214
|
end
|
128
215
|
nil
|
129
216
|
end
|
130
217
|
end
|
131
218
|
|
132
|
-
#
|
219
|
+
# @return [TrueClass, FalseClass] is connection currently receiving a message
|
133
220
|
def receiving?
|
134
221
|
!!@receiving
|
135
222
|
end
|
136
223
|
|
137
|
-
#
|
224
|
+
# Receive messages and place into queue
|
225
|
+
#
|
226
|
+
# @return [nil]
|
138
227
|
def process_to_queue!
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
228
|
+
@running = true
|
229
|
+
while(@running)
|
230
|
+
begin
|
231
|
+
message = handle(receive)
|
232
|
+
if(message)
|
233
|
+
debug "Adding message to queue #{message}"
|
234
|
+
queue << message
|
235
|
+
notifier.signal(message) if notifier
|
236
|
+
end
|
237
|
+
rescue Error::ConnectionUnavailable => e
|
238
|
+
warn "Failed to receive message: #{e.class} - #{e}"
|
239
|
+
@running = false
|
240
|
+
async.reconnect!
|
145
241
|
end
|
146
242
|
end
|
243
|
+
nil
|
147
244
|
end
|
148
245
|
|
149
|
-
# message::
|
150
|
-
#
|
246
|
+
# Handle non-message type Krakow::FrameType
|
247
|
+
#
|
248
|
+
# @param message [Krakow::FrameType] received message
|
249
|
+
# @return [Krakow::FrameType, nil]
|
151
250
|
def handle(message)
|
152
251
|
# Grab heartbeats upfront
|
153
252
|
if(message.is_a?(FrameType::Response) && message.response == '_heartbeat_')
|
@@ -155,10 +254,7 @@ module Krakow
|
|
155
254
|
transmit Command::Nop.new
|
156
255
|
nil
|
157
256
|
else
|
158
|
-
|
159
|
-
debug "Sending #{message} to callback `#{callback[:actor]}##{callback[:method]}`"
|
160
|
-
message = callback[:actor].send(callback[:method], message, current_actor)
|
161
|
-
end
|
257
|
+
message = callback_for(:handle, message)
|
162
258
|
if(!message.is_a?(FrameType::Message))
|
163
259
|
debug "Captured non-message type response: #{message}"
|
164
260
|
responses << message
|
@@ -169,6 +265,28 @@ module Krakow
|
|
169
265
|
end
|
170
266
|
end
|
171
267
|
|
268
|
+
# Execute callback for given type
|
269
|
+
#
|
270
|
+
# @overload callback_for(type, arg, connection)
|
271
|
+
# @param type [Symbol] type of callback
|
272
|
+
# @param arg [Object] argument for callback (can be multiple)
|
273
|
+
# @param connection [Krakow::Connection] current connection
|
274
|
+
# @return [Object] result of callback
|
275
|
+
def callback_for(type, *args)
|
276
|
+
callback = callbacks[type]
|
277
|
+
if(callback)
|
278
|
+
debug "Processing connection callback for #{type.inspect} (#{callback.inspect})"
|
279
|
+
callback[:actor].send(callback[:method], *(args + [current_actor]))
|
280
|
+
else
|
281
|
+
debug "No connection callback defined for #{type.inspect}"
|
282
|
+
args.size == 1 ? args.first : args
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
# Returns configured wait time for given message type
|
287
|
+
#
|
288
|
+
# @param message [Krakow::Command]
|
289
|
+
# @return [Numeric] seconds to wait
|
172
290
|
def wait_time_for(message)
|
173
291
|
case Command.response_for(message)
|
174
292
|
when :required
|
@@ -178,6 +296,7 @@ module Krakow
|
|
178
296
|
end
|
179
297
|
end
|
180
298
|
|
299
|
+
# @return [Hash] default settings for IDENTIFY
|
181
300
|
def identify_defaults
|
182
301
|
unless(@identify_defaults)
|
183
302
|
@identify_defaults = {
|
@@ -190,12 +309,15 @@ module Krakow
|
|
190
309
|
@identify_defaults
|
191
310
|
end
|
192
311
|
|
312
|
+
# IDENTIFY with server and negotiate features
|
313
|
+
#
|
314
|
+
# @return [TrueClass]
|
193
315
|
def identify_and_negotiate
|
194
316
|
expected_features = identify_defaults.merge(features)
|
195
317
|
ident = Command::Identify.new(
|
196
318
|
expected_features
|
197
319
|
)
|
198
|
-
socket.write(ident.to_line)
|
320
|
+
safe_socket{|socket| socket.write(ident.to_line) }
|
199
321
|
response = receive
|
200
322
|
if(expected_features[:feature_negotiation])
|
201
323
|
begin
|
@@ -219,25 +341,129 @@ module Krakow
|
|
219
341
|
true
|
220
342
|
end
|
221
343
|
|
344
|
+
# Enable snappy feature on underlying socket
|
345
|
+
#
|
346
|
+
# @return [TrueClass]
|
222
347
|
def snappy
|
223
348
|
info 'Loading support for snappy compression and converting connection'
|
224
|
-
@socket = ConnectionFeatures::SnappyFrames::Io.new(socket)
|
349
|
+
@socket = ConnectionFeatures::SnappyFrames::Io.new(socket, features_args)
|
225
350
|
response = receive
|
226
351
|
info "Snappy connection conversion complete. Response: #{response.inspect}"
|
352
|
+
true
|
227
353
|
end
|
228
354
|
|
355
|
+
# Enable deflate feature on underlying socket
|
356
|
+
#
|
357
|
+
# @return [TrueClass]
|
229
358
|
def deflate
|
230
359
|
debug 'Loading support for deflate compression and converting connection'
|
231
|
-
@socket = ConnectionFeatures::Deflate::Io.new(socket)
|
360
|
+
@socket = ConnectionFeatures::Deflate::Io.new(socket, features_args)
|
232
361
|
response = receive
|
233
362
|
info "Deflate connection conversion complete. Response: #{response.inspect}"
|
363
|
+
true
|
234
364
|
end
|
235
365
|
|
366
|
+
# Enable TLS feature on underlying socket
|
367
|
+
#
|
368
|
+
# @return [TrueClass]
|
236
369
|
def tls_v1
|
237
370
|
info 'Enabling TLS for connection'
|
238
|
-
@socket = ConnectionFeatures::Ssl::Io.new(socket)
|
371
|
+
@socket = ConnectionFeatures::Ssl::Io.new(socket, features_args)
|
239
372
|
response = receive
|
240
373
|
info "TLS enable complete. Response: #{response.inspect}"
|
374
|
+
true
|
375
|
+
end
|
376
|
+
|
377
|
+
# @return [TrueClass, FalseClass] underlying socket is connected
|
378
|
+
def connected?
|
379
|
+
socket && !socket.closed?
|
380
|
+
end
|
381
|
+
|
382
|
+
protected
|
383
|
+
|
384
|
+
# Destruct the underlying socket
|
385
|
+
#
|
386
|
+
# @return [nil]
|
387
|
+
def teardown_socket
|
388
|
+
if(socket && (socket.closed? || socket.eof?))
|
389
|
+
socket.close unless socket.closed?
|
390
|
+
@socket = nil
|
391
|
+
warn 'Existing socket instance has been destroyed from this connection'
|
392
|
+
end
|
393
|
+
nil
|
394
|
+
end
|
395
|
+
|
396
|
+
# Provides socket failure state handling around given block
|
397
|
+
#
|
398
|
+
# @yield [socket] execute within socket safety layer
|
399
|
+
# @yieldparam [socket] underlying socket
|
400
|
+
# @return [Object] result of executed block
|
401
|
+
def safe_socket(*args)
|
402
|
+
begin
|
403
|
+
if(socket.nil? || socket.closed?)
|
404
|
+
raise Error::ConnectionUnavailable.new 'Current connection is closed!'
|
405
|
+
end
|
406
|
+
result = yield socket if block_given?
|
407
|
+
result
|
408
|
+
rescue Error::ConnectionUnavailable, SystemCallError, IOError => e
|
409
|
+
warn "Safe socket encountered error (socket in failed state): #{e.class}: #{e}"
|
410
|
+
reconnect!
|
411
|
+
retry
|
412
|
+
rescue Celluloid::Error => e
|
413
|
+
warn "Internal error encountered. Allowing exception to bubble. #{e.class}: #{e}"
|
414
|
+
abort e
|
415
|
+
rescue Exception => e
|
416
|
+
warn "!!! Unexpected error encountered within safe socket: #{e.class}: #{e}"
|
417
|
+
raise
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
# Reconnect the underlying socket
|
422
|
+
#
|
423
|
+
# @return [nil]
|
424
|
+
def reconnect!
|
425
|
+
if(reconnector.try_lock)
|
426
|
+
begin
|
427
|
+
if(@socket_max_retries <= @socket_retries)
|
428
|
+
abort ConnectionFailure.new "Failed to re-establish connection after #{@socket_retries} tries."
|
429
|
+
end
|
430
|
+
pause_interval = @reconnect_pause * @socket_retries
|
431
|
+
@socket_retries += 1
|
432
|
+
warn "Pausing for #{pause_interval} seconds before reconnect"
|
433
|
+
sleep(pause_interval)
|
434
|
+
init!
|
435
|
+
@socket_retries = 0
|
436
|
+
rescue Celluloid::Error => e
|
437
|
+
warn "Internal error encountered. Allowing exception to bubble. #{e.class}: #{e}"
|
438
|
+
abort e
|
439
|
+
rescue SystemCallError, IOError => e
|
440
|
+
error "Reconnect error encountered: #{e.class} - #{e}"
|
441
|
+
retry
|
442
|
+
end
|
443
|
+
callback_for(:reconnect)
|
444
|
+
reconnect_notifier.broadcast(:connected)
|
445
|
+
reconnector.unlock
|
446
|
+
else
|
447
|
+
reconnect_notifier.wait(:connected)
|
448
|
+
end
|
449
|
+
nil
|
450
|
+
end
|
451
|
+
|
452
|
+
# Connect the underlying socket
|
453
|
+
#
|
454
|
+
# @return [nil]
|
455
|
+
def connect!
|
456
|
+
debug 'Initializing connection'
|
457
|
+
if(@socket)
|
458
|
+
@socket.close unless @socket.closed?
|
459
|
+
@socket = nil
|
460
|
+
end
|
461
|
+
@socket = Celluloid::IO::TCPSocket.new(host, port)
|
462
|
+
safe_socket{|socket| socket.write version.rjust(4).upcase}
|
463
|
+
identify_and_negotiate
|
464
|
+
async.process_to_queue!
|
465
|
+
info 'Connection initialized'
|
466
|
+
nil
|
241
467
|
end
|
242
468
|
|
243
469
|
end
|
@@ -1,13 +1,20 @@
|
|
1
1
|
require 'zlib'
|
2
|
+
require 'krakow'
|
2
3
|
|
3
4
|
module Krakow
|
4
5
|
module ConnectionFeatures
|
6
|
+
# Deflate functionality
|
5
7
|
module Deflate
|
8
|
+
# Deflatable IO
|
6
9
|
class Io
|
7
10
|
|
8
11
|
attr_reader :io, :buffer, :headers, :inflator, :deflator
|
9
12
|
|
10
|
-
|
13
|
+
# Create new deflatable IO
|
14
|
+
#
|
15
|
+
# @param io [IO] IO to wrap
|
16
|
+
# @return [Io]
|
17
|
+
def initialize(io, args={})
|
11
18
|
@io = io
|
12
19
|
@buffer = ''
|
13
20
|
@inflator = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
@@ -15,10 +22,17 @@ module Krakow
|
|
15
22
|
end
|
16
23
|
|
17
24
|
# Proxy to underlying socket
|
25
|
+
#
|
26
|
+
# @param args [Object]
|
27
|
+
# @return [Object]
|
18
28
|
def method_missing(*args)
|
19
29
|
io.__send__(*args)
|
20
30
|
end
|
21
31
|
|
32
|
+
# Receive bytes from the IO
|
33
|
+
#
|
34
|
+
# @param n [Integer] nuber of bytes
|
35
|
+
# @return [String]
|
22
36
|
def recv(n)
|
23
37
|
until(buffer.length >= n)
|
24
38
|
read_stream
|
@@ -28,6 +42,9 @@ module Krakow
|
|
28
42
|
end
|
29
43
|
alias_method :read, :recv
|
30
44
|
|
45
|
+
# Read contents from stream
|
46
|
+
#
|
47
|
+
# @return [String]
|
31
48
|
def read_stream
|
32
49
|
str = io.read
|
33
50
|
unless(str.empty?)
|
@@ -35,6 +52,10 @@ module Krakow
|
|
35
52
|
end
|
36
53
|
end
|
37
54
|
|
55
|
+
# Write string to IO
|
56
|
+
#
|
57
|
+
# @param string [String]
|
58
|
+
# @return [Integer] number of bytes written
|
38
59
|
def write(string)
|
39
60
|
unless(string.empty?)
|
40
61
|
output = deflator.deflate(string)
|
@@ -45,10 +66,14 @@ module Krakow
|
|
45
66
|
end
|
46
67
|
end
|
47
68
|
|
69
|
+
# Close the IO
|
70
|
+
#
|
71
|
+
# @return [TrueClass]
|
48
72
|
def close(*args)
|
49
73
|
super
|
50
74
|
deflator.deflate(nil, Zlib::FINISH)
|
51
75
|
deflator.close
|
76
|
+
true
|
52
77
|
end
|
53
78
|
|
54
79
|
end
|
@@ -1,18 +1,24 @@
|
|
1
1
|
require 'snappy'
|
2
2
|
require 'digest/crc'
|
3
|
+
require 'krakow'
|
3
4
|
|
4
|
-
# TODO: Add support for max size + chunks
|
5
|
-
# TODO: Include support for remaining types
|
6
5
|
module Krakow
|
7
6
|
module ConnectionFeatures
|
7
|
+
# Snappy functionality
|
8
|
+
# @todo Add support for max size + chunks
|
9
|
+
# @todo Include support for remaining types
|
8
10
|
module SnappyFrames
|
11
|
+
# Snappy-able IO
|
9
12
|
class Io
|
10
13
|
|
14
|
+
# Header identifier
|
11
15
|
IDENTIFIER = "\x73\x4e\x61\x50\x70\x59".force_encoding('ASCII-8BIT')
|
12
16
|
ident_size = [IDENTIFIER.size].pack('L<')
|
13
17
|
ident_size.slice!(-1,1)
|
18
|
+
# Size of identifier
|
14
19
|
IDENTIFIER_SIZE = ident_size
|
15
20
|
|
21
|
+
# Mapping of types
|
16
22
|
CHUNK_TYPE = {
|
17
23
|
"\xff".force_encoding('ASCII-8BIT') => :identifier,
|
18
24
|
"\x00".force_encoding('ASCII-8BIT') => :compressed,
|
@@ -21,21 +27,36 @@ module Krakow
|
|
21
27
|
|
22
28
|
attr_reader :io, :buffer
|
23
29
|
|
24
|
-
|
30
|
+
# Create new snappy-able IO
|
31
|
+
#
|
32
|
+
# @param io [IO] IO to wrap
|
33
|
+
# @return [Io]
|
34
|
+
def initialize(io, args={})
|
25
35
|
@io = io
|
26
36
|
@snappy_write_ident = false
|
27
37
|
@buffer = ''
|
28
38
|
end
|
29
39
|
|
30
40
|
# Proxy to underlying socket
|
41
|
+
#
|
42
|
+
# @param args [Object]
|
43
|
+
# @return [Object]
|
31
44
|
def method_missing(*args)
|
32
45
|
io.__send__(*args)
|
33
46
|
end
|
34
47
|
|
48
|
+
# Mask the checksum
|
49
|
+
#
|
50
|
+
# @param checksum [String]
|
51
|
+
# @return [String]
|
35
52
|
def checksum_mask(checksum)
|
36
53
|
(((checksum >> 15) | (checksum << 17)) + 0xa282ead8) & 0xffffffff
|
37
54
|
end
|
38
55
|
|
56
|
+
# Receive bytes from the IO
|
57
|
+
#
|
58
|
+
# @param n [Integer] nuber of bytes
|
59
|
+
# @return [String]
|
39
60
|
def recv(n)
|
40
61
|
read_stream unless buffer.size >= n
|
41
62
|
result = buffer.slice!(0,n)
|
@@ -43,6 +64,9 @@ module Krakow
|
|
43
64
|
end
|
44
65
|
alias_method :read, :recv
|
45
66
|
|
67
|
+
# Read contents from stream
|
68
|
+
#
|
69
|
+
# @return [String]
|
46
70
|
def read_stream
|
47
71
|
header = io.recv(4)
|
48
72
|
ident = CHUNK_TYPE[header.slice!(0)]
|
@@ -68,6 +92,10 @@ module Krakow
|
|
68
92
|
end
|
69
93
|
end
|
70
94
|
|
95
|
+
# Write string to IO
|
96
|
+
#
|
97
|
+
# @param string [String]
|
98
|
+
# @return [Integer] number of bytes written
|
71
99
|
def write(string)
|
72
100
|
unless(@snappy_writer_ident)
|
73
101
|
send_snappy_identifier
|
@@ -83,6 +111,9 @@ module Krakow
|
|
83
111
|
io.write output
|
84
112
|
end
|
85
113
|
|
114
|
+
# Send the identifier for snappy content
|
115
|
+
#
|
116
|
+
# @return [Integer] bytes written
|
86
117
|
def send_snappy_identifier
|
87
118
|
io.write [CHUNK_TYPE.key(:identifier), IDENTIFIER_SIZE, IDENTIFIER].pack('a*a*a*')
|
88
119
|
end
|