krakow 0.3.12 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ec832b26d703bbb80cc726f4253e665489f1d959
4
- data.tar.gz: bc00e4942eff7a0fbf36248fc88166e51cbac638
3
+ metadata.gz: 01fd8940f20cb30ff26c163b8851fa88cad2f986
4
+ data.tar.gz: d6f277aeaaaf5adcadf08dbfa51fd6baf5370218
5
5
  SHA512:
6
- metadata.gz: 35b372c4ea0551163a44fed973eb1b456dd41f3f219b23f6645117858d77ace6f817f515b880b01d1194cc9316d9b42a4bcbe893c91fa664abf97445e08814fb
7
- data.tar.gz: 4046e076dfcfd812796622c37a2ec671be021d2ec842efb6ba7898307c91de0b5eca93b7ee06b210cf96822fee2cf1dc73ff56a72666b7bb2dde77701737aec3
6
+ metadata.gz: 86b969cfe4a3894983809cbe142497a658fc5bf369021fc6294629cde4443646be242bc4c3a870dab1efcd2da1615d7ba89c30ad04806187fb8448633d897778
7
+ data.tar.gz: 793e1ae1de7689b41fdd8c495404c238f7acd77a4520a08d4803607815205ec21cce4008c01781cc974128f347347e40a397b1657019da02d0d3964481ca6ea9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## v0.4.0
2
+ * Prevent duplicate connection consumption loops (#23) (thanks @phopkins)
3
+ * Refactor connection implementation to prevent crashing blocks (#22) (thanks @phopkins)
4
+ * Properly calculate message sizes using byte length, not string length (#24) (thanks @phopkins)
5
+ * Clear responses prior to message transmission (#20) (thanks @i2amsam)
6
+ * Rebuild testing specs (not fully covered, but a good start)
7
+ * Consumer and Producer provide better connection failure recovery
8
+ * Fix in-flight issues within distribution on connection failures
9
+
10
+ _NOTE: Large portions of `Krakow::Connection` has been refactored_
11
+
1
12
  ## v0.3.12
2
13
  * Update Consumer#confirm and Consumer#touch to rescue out lookups and abort
3
14
 
data/README.md CHANGED
@@ -218,6 +218,8 @@ Or, run part of them:
218
218
  bundle exec ruby test/specs/consumer_spec.rb
219
219
  ```
220
220
 
221
+ *NOTE*: the specs expect that `nsqd` and `nsqlookupd` are available in `$PATH`
222
+
221
223
  ### It doesn't work
222
224
 
223
225
  Create an issue on the github repository
@@ -240,6 +242,8 @@ Create an issue, or even better, send a PR.
240
242
 
241
243
  # Contributors
242
244
 
245
+ * Pete Hopkins (@phopkins)
246
+ * Sam Phillips (@i2amsam)
243
247
  * Brendan Schwartz (@bschwartz)
244
248
  * Thomas Holmes (@thomas-holmes)
245
249
  * Jeremy Hinegardner (@copiousfreetime)
data/krakow.gemspec CHANGED
@@ -10,10 +10,13 @@ Gem::Specification.new do |s|
10
10
  s.description = 'NSQ ruby library'
11
11
  s.license = 'Apache 2.0'
12
12
  s.require_path = 'lib'
13
- s.add_dependency 'celluloid-io'
14
- s.add_dependency 'http'
15
- s.add_dependency 'multi_json'
16
- s.add_dependency 'digest-crc'
13
+ s.add_runtime_dependency 'celluloid'
14
+ s.add_runtime_dependency 'http'
15
+ s.add_runtime_dependency 'multi_json'
16
+ s.add_runtime_dependency 'digest-crc'
17
+ s.add_development_dependency 'childprocess'
18
+ s.add_development_dependency 'snappy'
19
+ s.add_development_dependency 'minitest'
17
20
  s.files = Dir['lib/**/*'] + %w(krakow.gemspec README.md CHANGELOG.md CONTRIBUTING.md LICENSE)
18
21
  s.extra_rdoc_files = %w(CHANGELOG.md CONTRIBUTING.md LICENSE)
19
22
  end
data/lib/krakow.rb CHANGED
@@ -1,4 +1,10 @@
1
1
  require 'krakow/version'
2
+ require 'celluloid'
3
+
4
+ if(ENV['DEBUG'])
5
+ Celluloid.task_class = Celluloid::TaskThread
6
+ end
7
+
2
8
  require 'celluloid/autostart'
3
9
  require 'multi_json'
4
10
 
@@ -13,6 +19,7 @@ module Krakow
13
19
  autoload :Distribution, 'krakow/distribution'
14
20
  autoload :Error, 'krakow/exceptions'
15
21
  autoload :FrameType, 'krakow/frame_type'
22
+ autoload :Ksocket, 'krakow/ksocket'
16
23
  autoload :Producer, 'krakow/producer'
17
24
  autoload :Utils, 'krakow/utils'
18
25
 
@@ -18,7 +18,7 @@ module Krakow
18
18
 
19
19
  def to_line
20
20
  scrt = secret.to_s
21
- [name, "\n", scrt.length, scrt].pack('a*a*a*a*l>a*')
21
+ [name, "\n", scrt.bytesize, scrt].pack('a*a*a*a*l>a*')
22
22
  end
23
23
 
24
24
  class << self
@@ -36,7 +36,7 @@ module Krakow
36
36
  end.compact.flatten
37
37
  ]
38
38
  payload = MultiJson.dump(filtered)
39
- [name, "\n", payload.length, payload].pack('a*a*l>a*')
39
+ [name, "\n", payload.bytesize, payload].pack('a*a*l>a*')
40
40
  end
41
41
 
42
42
  class << self
@@ -19,9 +19,9 @@ module Krakow
19
19
  def to_line
20
20
  formatted_messages = messages.map do |message|
21
21
  message = message.to_s
22
- [message.length, message].pack('l>a*')
22
+ [message.bytesize, message].pack('l>a*')
23
23
  end.join
24
- [name, ' ', topic_name, "\n", formatted_messages.length, messages.size, formatted_messages].pack('a*a*a*a*l>l>a*')
24
+ [name, ' ', topic_name, "\n", formatted_messages.bytesize, messages.size, formatted_messages].pack('a*a*a*a*l>l>a*')
25
25
  end
26
26
 
27
27
  class << self
@@ -19,7 +19,7 @@ module Krakow
19
19
 
20
20
  def to_line
21
21
  msg = message.to_s
22
- [name, ' ', topic_name, "\n", msg.length, msg].pack('a*a*a*a*l>a*')
22
+ [name, ' ', topic_name, "\n", msg.bytesize, msg].pack('a*a*a*a*l>a*')
23
23
  end
24
24
 
25
25
  class << self
@@ -1,5 +1,4 @@
1
1
  require 'krakow'
2
- require 'celluloid/io'
3
2
 
4
3
  module Krakow
5
4
 
@@ -21,7 +20,7 @@ module Krakow
21
20
  # @!parse include Krakow::Utils::Lazy::InstanceMethods
22
21
  # @!parse extend Krakow::Utils::Lazy::ClassMethods
23
22
 
24
- include Celluloid::IO
23
+ include Celluloid
25
24
 
26
25
  # Available connection features
27
26
  FEATURES = [
@@ -43,14 +42,14 @@ module Krakow
43
42
  # List of features that may be enabled by the client
44
43
  ENABLEABLE_FEATURES = [:tls_v1, :snappy, :deflate, :auth_required]
45
44
 
46
- finalizer :goodbye_my_love!
45
+ finalizer :connection_cleanup
47
46
 
48
47
  # @return [Hash] current configuration for endpoint
49
48
  attr_reader :endpoint_settings
50
- # @return [Socket-ish] underlying socket like instance
49
+ # @return [Ksocket] underlying socket like instance
51
50
  attr_reader :socket
52
-
53
- attr_reader :reconnect_notifier, :running
51
+ # @return [TrueClass, FalseClass]
52
+ attr_reader :running
54
53
 
55
54
  # @!group Attributes
56
55
 
@@ -64,7 +63,7 @@ module Krakow
64
63
  attribute :topic, String
65
64
  attribute :channel, String
66
65
  attribute :version, String, :default => 'v2'
67
- attribute :queue, Queue, :default => ->{ Queue.new }
66
+ attribute :queue, [Queue, Consumer::Queue], :default => ->{ Queue.new }
68
67
  attribute :callbacks, Hash, :default => ->{ Hash.new }
69
68
  attribute :responses, Queue, :default => ->{ Queue.new }
70
69
  attribute :notifier, [Celluloid::Signals, Celluloid::Condition, Celluloid::Actor]
@@ -95,10 +94,6 @@ module Krakow
95
94
  # @option args [Hash] :feature_args options for connection features
96
95
  def initialize(args={})
97
96
  super
98
- @reconnect_notifier = Celluloid::Signals.new
99
- @socket_retries = 0
100
- @socket_max_retries = 10
101
- @reconnect_pause = 0.5
102
97
  @endpoint_settings = {}
103
98
  @running = false
104
99
  end
@@ -118,6 +113,7 @@ module Krakow
118
113
  # @return [nil]
119
114
  def init!
120
115
  connect!
116
+ async.process_to_queue!
121
117
  nil
122
118
  end
123
119
 
@@ -126,13 +122,16 @@ module Krakow
126
122
  # @param message [Krakow::Message] message to send
127
123
  # @return [TrueClass, Krakow::FrameType] response if expected or true
128
124
  def transmit(message)
125
+ unless(message.respond_to?(:to_line))
126
+ abort TypeError.new("Expecting type `Krakow::FrameType` but received `#{message.class}`")
127
+ end
129
128
  output = message.to_line
130
129
  response_wait = wait_time_for(message)
131
130
  if(response_wait > 0)
132
131
  transmit_with_response(message, response_wait)
133
132
  else
134
133
  debug ">>> #{output}"
135
- safe_socket{|socket| socket.write output }
134
+ socket.put(output)
136
135
  true
137
136
  end
138
137
  end
@@ -142,8 +141,8 @@ module Krakow
142
141
  # @param message [Krakow::Message] message to send
143
142
  # @return [Krakow::FrameType] response
144
143
  def transmit_with_response(message, wait_time)
145
- safe_socket{|socket| socket.write(message.to_line) }
146
144
  responses.clear
145
+ socket.put(message.to_line)
147
146
  response = nil
148
147
  (wait_time / response_interval).to_i.times do |i|
149
148
  response = responses.pop unless responses.empty?
@@ -168,16 +167,11 @@ module Krakow
168
167
  # Destructor method for cleanup
169
168
  #
170
169
  # @return [nil]
171
- def goodbye_my_love!
170
+ def connection_cleanup
172
171
  debug 'Tearing down connection'
173
- if(socket && !socket.closed?)
174
- [lambda{ socket.write Command::Cls.new.to_line}, lambda{socket.close}].each do |action|
175
- begin
176
- action.call
177
- rescue IOError, SystemCallError => e
178
- warn "Socket error encountered during teardown: #{e.class}: #{e}"
179
- end
180
- end
172
+ @running = false
173
+ if(connected?)
174
+ socket.terminate
181
175
  end
182
176
  @socket = nil
183
177
  info 'Connection torn down'
@@ -190,22 +184,19 @@ module Krakow
190
184
  # @raise [Error::ConnectionUnavailable] socket is closed
191
185
  def receive
192
186
  debug 'Read wait for frame start'
193
- buf = socket.recv(8)
187
+ buf = socket.get(8)
194
188
  if(buf)
195
189
  @receiving = true
196
190
  debug "<<< #{buf.inspect}"
197
191
  struct = FrameType.decode(buf)
198
192
  debug "Decoded structure: #{struct.inspect}"
199
- struct[:data] = socket.read(struct[:size])
193
+ struct[:data] = socket.get(struct[:size])
200
194
  debug "<<< #{struct[:data].inspect}"
201
195
  @receiving = false
202
196
  frame = FrameType.build(struct)
203
197
  debug "Struct: #{struct.inspect} Frame: #{frame.inspect}"
204
198
  frame
205
199
  else
206
- if(socket.closed?)
207
- abort Error::ConnectionUnavailable.new("#{self} encountered closed socket!")
208
- end
209
200
  nil
210
201
  end
211
202
  end
@@ -219,19 +210,20 @@ module Krakow
219
210
  #
220
211
  # @return [nil]
221
212
  def process_to_queue!
222
- @running = true
223
- while(@running)
224
- begin
213
+ unless(@running)
214
+ @running = true
215
+ while(@running)
225
216
  message = handle(receive)
226
217
  if(message)
227
218
  debug "Adding message to queue #{message}"
228
219
  queue << message
229
- notifier.broadcast(message) if notifier
220
+ if(notifier)
221
+ warn "Sending new message notification: #{notifier} - #{message}"
222
+ notifier.broadcast(message)
223
+ end
224
+ else
225
+ debug 'Received `nil` message. Ignoring.'
230
226
  end
231
- rescue Error::ConnectionUnavailable => e
232
- warn "Failed to receive message: #{e.class} - #{e}"
233
- @running = false
234
- async.reconnect!
235
227
  end
236
228
  end
237
229
  nil
@@ -270,7 +262,11 @@ module Krakow
270
262
  callback = callbacks[type]
271
263
  if(callback)
272
264
  debug "Processing connection callback for #{type.inspect} (#{callback.inspect})"
273
- callback[:actor].send(callback[:method], *(args + [current_actor]))
265
+ if(callback[:actor].alive?)
266
+ callback[:actor].send(callback[:method], *(args + [current_actor]))
267
+ else
268
+ error "Expected actor for callback processing is not alive! (type: `#{type.inspect}`)"
269
+ end
274
270
  else
275
271
  debug "No connection callback defined for #{type.inspect}"
276
272
  args.size == 1 ? args.first : args
@@ -313,7 +309,7 @@ module Krakow
313
309
  ident = Command::Identify.new(
314
310
  expected_features
315
311
  )
316
- safe_socket{|socket| socket.write(ident.to_line) }
312
+ socket.put(ident.to_line)
317
313
  response = receive
318
314
  if(expected_features[:feature_negotiation])
319
315
  begin
@@ -387,87 +383,33 @@ module Krakow
387
383
 
388
384
  # @return [TrueClass, FalseClass] underlying socket is connected
389
385
  def connected?
390
- socket && !socket.closed?
391
- end
392
-
393
- protected
394
-
395
- # Destruct the underlying socket
396
- #
397
- # @return [nil]
398
- def teardown_socket
399
- if(socket && (socket.closed? || socket.eof?))
400
- socket.close unless socket.closed?
401
- @socket = nil
402
- warn 'Existing socket instance has been destroyed from this connection'
403
- end
404
- nil
405
- end
406
-
407
- # Provides socket failure state handling around given block
408
- #
409
- # @yield [socket] execute within socket safety layer
410
- # @yieldparam [socket] underlying socket
411
- # @return [Object] result of executed block
412
- def safe_socket(*args)
413
386
  begin
414
- if(socket.nil? || socket.closed?)
415
- raise Error::ConnectionUnavailable.new 'Current connection is closed!'
416
- end
417
- result = yield socket if block_given?
418
- result
419
- rescue Error::ConnectionUnavailable, SystemCallError, IOError => e
420
- warn "Safe socket encountered error (socket in failed state): #{e.class}: #{e}"
421
- reconnect!
422
- retry
423
- rescue Celluloid::Error => e
424
- warn "Internal error encountered. Allowing exception to bubble. #{e.class}: #{e}"
425
- abort e
426
- rescue Exception => e
427
- warn "!!! Unexpected error encountered within safe socket: #{e.class}: #{e}"
428
- raise
387
+ !!(socket && socket.alive?)
388
+ rescue Celluloid::DeadActorError
389
+ false
429
390
  end
430
391
  end
431
392
 
432
- # Reconnect the underlying socket
433
- #
434
- # @return [nil]
435
- def reconnect!
436
- begin
437
- if(@socket_max_retries <= @socket_retries)
438
- abort ConnectionFailure.new "Failed to re-establish connection after #{@socket_retries} tries."
439
- end
440
- pause_interval = @reconnect_pause * @socket_retries
441
- @socket_retries += 1
442
- warn "Pausing for #{pause_interval} seconds before reconnect"
443
- sleep(pause_interval)
444
- init!
445
- @socket_retries = 0
446
- rescue Celluloid::Error => e
447
- warn "Internal error encountered. Allowing exception to bubble. #{e.class}: #{e}"
448
- abort e
449
- rescue SystemCallError, IOError => e
450
- error "Reconnect error encountered: #{e.class} - #{e}"
451
- retry
452
- end
453
- callback_for(:reconnect)
454
- nil
455
- end
393
+ protected
456
394
 
457
395
  # Connect the underlying socket
458
396
  #
459
397
  # @return [nil]
460
398
  def connect!
461
399
  debug 'Initializing connection'
462
- if(@socket)
463
- @socket.close unless @socket.closed?
464
- @socket = nil
400
+ unless(@connecting)
401
+ @connecting = true
402
+ if(socket && socket.alive?)
403
+ socket.terminate
404
+ @socket = nil
405
+ end
406
+ @socket = Ksocket.new(:host => host, :port => port)
407
+ self.link socket
408
+ socket.put version.rjust(4).upcase
409
+ identify_and_negotiate
410
+ info 'Connection initialized'
411
+ @connecting = false
465
412
  end
466
- @socket = Celluloid::IO::TCPSocket.new(host, port)
467
- safe_socket{|socket| socket.write version.rjust(4).upcase}
468
- identify_and_negotiate
469
- async.process_to_queue!
470
- info 'Connection initialized'
471
413
  nil
472
414
  end
473
415
 
@@ -4,6 +4,8 @@ module Krakow
4
4
  # Consume messages from a server
5
5
  class Consumer
6
6
 
7
+ autoload :Queue, 'krakow/consumer/queue'
8
+
7
9
  include Utils::Lazy
8
10
  # @!parse include Krakow::Utils::Lazy::InstanceMethods
9
11
  # @!parse extend Krakow::Utils::Lazy::ClassMethods
@@ -11,7 +13,7 @@ module Krakow
11
13
  include Celluloid
12
14
 
13
15
  trap_exit :connection_failure
14
- finalizer :goodbye_my_love!
16
+ finalizer :consumer_cleanup
15
17
 
16
18
  attr_reader :connections, :discovery, :distribution, :queue
17
19
 
@@ -42,30 +44,53 @@ module Krakow
42
44
  arguments[:connection_options] || {}
43
45
  )
44
46
  @connections = {}
47
+ @queue = Queue.new(
48
+ current_actor,
49
+ :removal_callback => :remove_message
50
+ )
45
51
  @distribution = Distribution::Default.new(
46
52
  :max_in_flight => max_in_flight,
47
53
  :backoff_interval => backoff_interval,
48
54
  :consumer => current_actor
49
55
  )
50
- @queue = Queue.new
51
56
  if(nsqlookupd)
52
57
  debug "Connections will be established via lookup #{nsqlookupd.inspect}"
53
58
  @discovery = Discovery.new(:nsqlookupd => nsqlookupd)
54
59
  discover
55
60
  elsif(host && port)
56
- debug "Connection will be established via direct connection #{host}:#{port}"
57
- connection = build_connection(host, port, queue)
58
- if(register(connection))
59
- info "Registered new connection #{connection}"
60
- distribution.redistribute!
61
- else
62
- abort Error::ConnectionFailure.new("Failed to establish subscription at provided end point (#{host}:#{port}")
63
- end
61
+ direct_connect
64
62
  else
65
63
  abort Error::ConfigurationError.new('No connection information provided!')
66
64
  end
67
65
  end
68
66
 
67
+ # @return [TrueClass, FalseClass] currently connected to at least
68
+ # one nsqd
69
+ def connected?
70
+ !!connections.values.any? do |con|
71
+ begin
72
+ con.connected?
73
+ rescue Celluloid::DeadActorError
74
+ false
75
+ end
76
+ end
77
+ end
78
+
79
+ # Connect to nsqd instance directly
80
+ #
81
+ # @return [Connection]
82
+ def direct_connect
83
+ debug "Connection will be established via direct connection #{host}:#{port}"
84
+ connection = build_connection(host, port, queue)
85
+ if(register(connection))
86
+ info "Registered new connection #{connection}"
87
+ distribution.redistribute!
88
+ else
89
+ abort Error::ConnectionFailure.new("Failed to establish subscription at provided end point (#{host}:#{port}")
90
+ end
91
+ connection
92
+ end
93
+
69
94
  # Returns [Krakow::Connection] associated to key
70
95
  #
71
96
  # @param key [Object] identifier
@@ -82,12 +107,17 @@ module Krakow
82
107
  # Instance destructor
83
108
  #
84
109
  # @return [nil]
85
- def goodbye_my_love!
110
+ def consumer_cleanup
86
111
  debug 'Tearing down consumer'
112
+ if(distribution && distribution.alive?)
113
+ distribution.terminate
114
+ end
115
+ if(queue && queue.alive?)
116
+ queue.terminate
117
+ end
87
118
  connections.values.each do |con|
88
119
  con.terminate if con.alive?
89
120
  end
90
- distribution.terminate if distribution && distribution.alive?
91
121
  info 'Consumer torn down'
92
122
  nil
93
123
  end
@@ -113,13 +143,11 @@ module Krakow
113
143
  :handle => {
114
144
  :actor => current_actor,
115
145
  :method => :process_message
116
- },
117
- :reconnect => {
118
- :actor => current_actor,
119
- :method => :connection_reconnect
120
146
  }
121
147
  }
122
148
  )
149
+ queue.register_connection(connection)
150
+ connection
123
151
  rescue => e
124
152
  error "Failed to build connection (host: #{host} port: #{port} queue: #{queue}) - #{e.class}: #{e}"
125
153
  debug "#{e.class}: #{e}\n#{e.backtrace.join("\n")}"
@@ -132,22 +160,30 @@ module Krakow
132
160
  # @param message [Krakow::FrameType]
133
161
  # @param connection [Krakow::Connection]
134
162
  # @return [Krakow::FrameType]
163
+ # @note If we receive a message that is already in flight, attempt
164
+ # to scrub message from wait queue. If message is found, retry
165
+ # distribution registration. If message is not found, assume it
166
+ # is currently being processed and do not allow new message to
167
+ # be queued
135
168
  def process_message(message, connection)
169
+ discard = false
136
170
  if(message.is_a?(FrameType::Message))
137
- distribution.register_message(message, connection.identifier)
138
171
  message.origin = current_actor
172
+ message.connection = connection
173
+ retried = false
174
+ begin
175
+ distribution.register_message(message, connection.identifier)
176
+ rescue KeyError => e
177
+ if(!retried && queue.scrub_duplicate_message(message))
178
+ retried = true
179
+ retry
180
+ else
181
+ error "Received message is currently in flight and not in wait queue. Discarding! (#{message})"
182
+ discard = true
183
+ end
184
+ end
139
185
  end
140
- message
141
- end
142
-
143
- # Action to take when a connection has reconnected
144
- #
145
- # @param connection [Krakow::Connection]
146
- # @return [nil]
147
- def connection_reconnect(connection)
148
- connection.transmit(Command::Sub.new(:topic_name => topic, :channel_name => channel))
149
- distribution.set_ready_for(connection)
150
- nil
186
+ discard ? nil : message
151
187
  end
152
188
 
153
189
  # Send RDY for connection based on distribution rules
@@ -214,17 +250,30 @@ module Krakow
214
250
  # @param reason [Exception] reason for termination
215
251
  # @return [nil]
216
252
  def connection_failure(actor, reason)
217
- connections.delete_if do |key, value|
218
- if(value == actor && reason.nil?)
219
- warn "Connection failure detected. Removing connection: #{key} - #{reason || 'no reason provided'}"
220
- begin
221
- distribution.remove_connection(key)
222
- rescue Error::ConnectionUnavailable, Error::ConnectionFailure
223
- warn 'Caught connection unavailability'
224
- end
225
- distribution.redistribute!
226
- true
253
+ if(reason && key = connections.key(actor))
254
+ warn "Connection failure detected. Removing connection: #{key} - #{reason}"
255
+ connections.delete(key)
256
+ begin
257
+ distribution.remove_connection(key)
258
+ rescue Error::ConnectionUnavailable, Error::ConnectionFailure
259
+ warn 'Caught connection unavailability'
227
260
  end
261
+ queue.deregister_connection(key)
262
+ distribution.redistribute!
263
+ direct_connect unless discovery
264
+ end
265
+ nil
266
+ end
267
+
268
+ # Remove message
269
+ #
270
+ # @param messages [Array<FrameType::Message>]
271
+ # @return [NilClass]
272
+ # @note used mainly for queue callback
273
+ def remove_message(messages)
274
+ [messages].flatten.compact.each do |msg|
275
+ distribution.unregister_message(msg.message_id)
276
+ update_ready!(msg.connection)
228
277
  end
229
278
  nil
230
279
  end
@@ -237,11 +286,12 @@ module Krakow
237
286
  def confirm(message_id)
238
287
  message_id = message_id.message_id if message_id.respond_to?(:message_id)
239
288
  begin
240
- distribution.in_flight_lookup(message_id) do |connection|
241
- distribution.unregister_message(message_id)
289
+ begin
290
+ connection = distribution.in_flight_lookup(message_id)
242
291
  connection.transmit(Command::Fin.new(:message_id => message_id))
243
292
  distribution.success(connection.identifier)
244
- update_ready!(connection)
293
+ rescue => e
294
+ abort e
245
295
  end
246
296
  true
247
297
  rescue KeyError => e
@@ -251,7 +301,12 @@ module Krakow
251
301
  error "Lookup of message for confirmation failed! <Message ID: #{message_id} - Error: #{e}>"
252
302
  abort e
253
303
  rescue Error::ConnectionUnavailable => e
254
- retry
304
+ abort e
305
+ rescue Celluloid::DeadActorError
306
+ abort Error::ConnectionUnavailable.new
307
+ ensure
308
+ con = distribution.unregister_message(message_id)
309
+ update_ready!(con) if con
255
310
  end
256
311
  end
257
312
  alias_method :finish, :confirm
@@ -0,0 +1,151 @@
1
+ require 'krakow'
2
+
3
+ module Krakow
4
+ # Consume messages from a server
5
+ class Consumer
6
+
7
+ class Queue
8
+
9
+ include Celluloid
10
+ include Utils::Lazy
11
+
12
+ # @return [Consumer]
13
+ attr_reader :consumer
14
+ # @return [Array] order of message removal
15
+ attr_reader :pop_order
16
+ # @return [Symbol] callback method name
17
+ attr_reader :removal_callback
18
+
19
+ # Create new consumer queue instance
20
+ #
21
+ # @param consumer [Consumer]
22
+ # @return [self]
23
+ def initialize(consumer, *args)
24
+ opts = args.detect{|x| x.is_a?(Hash)}
25
+ @consumer = consumer
26
+ @removal_callback = opts[:removal_callback]
27
+ @messages = {}
28
+ @pop_order = []
29
+ @cleaner = nil
30
+ end
31
+
32
+ # Message container
33
+ #
34
+ # @yieldparam [Hash] messages
35
+ # @return [Hash] messages or block result
36
+ def messages
37
+ if(block_given?)
38
+ yield @messages
39
+ else
40
+ @messages
41
+ end
42
+ end
43
+
44
+ # Register a new connection
45
+ #
46
+ # @param connection [Connection]
47
+ # @return [TrueClass]
48
+ def register_connection(connection)
49
+ messages do |collection|
50
+ collection[connection.identifier] = []
51
+ end
52
+ true
53
+ end
54
+
55
+ # Remove connection registration and remove all messages
56
+ #
57
+ # @param identifier [String] connection identifier
58
+ # @return [Array<FrameType::Message>] messages queued for deregistered connection
59
+ def deregister_connection(identifier)
60
+ messages do |collection|
61
+ removed = collection.delete(identifier)
62
+ pop_order.delete(identifier)
63
+ removed
64
+ end
65
+ end
66
+
67
+ # Push new message into queue
68
+ #
69
+ # @param message [FrameType::Message]
70
+ # @return [self]
71
+ def push(message)
72
+ unless(message.is_a?(FrameType::Message))
73
+ abort TypeError.new "Expecting `FrameType::Message` but received `#{message.class}`!"
74
+ end
75
+ messages do |collection|
76
+ begin
77
+ collection[message.connection.identifier] << message
78
+ pop_order << message.connection.identifier
79
+ rescue Celluloid::DeadActorError
80
+ abort Error::ConnectionUnavailable.new
81
+ end
82
+ end
83
+ signal(:new_message)
84
+ current_actor
85
+ end
86
+ alias_method :<<, :push
87
+ alias_method :enq, :push
88
+
89
+ # Pop first item off the queue
90
+ #
91
+ # @return [Object]
92
+ def pop
93
+ message = nil
94
+ until(message)
95
+ wait(:new_message) if pop_order.empty?
96
+ messages do |collection|
97
+ key = pop_order.shift
98
+ if(key)
99
+ message = collection[key].shift
100
+ message = validate_message(message)
101
+ end
102
+ end
103
+ end
104
+ message
105
+ end
106
+ alias_method :deq, :pop
107
+
108
+ # @return [Integer] number of queued messages
109
+ def size
110
+ messages do |collection|
111
+ collection.values.map(&:size).inject(&:+)
112
+ end
113
+ end
114
+
115
+ # Remove duplicate message from queue if possible
116
+ #
117
+ # @param message [FrameType::Message]
118
+ # @return [TrueClass, FalseClass]
119
+ def scrub_duplicate_message(message)
120
+ messages do |collection|
121
+ idx = collection[message.connection.identifier].index do |msg|
122
+ msg.message_id == message.message_id
123
+ end
124
+ if(idx)
125
+ msg = collection[message.connection.identifier].delete_at(idx)
126
+ if(removal_callback)
127
+ consumer.send(removal_callback, [message])
128
+ end
129
+ true
130
+ else
131
+ false
132
+ end
133
+ end
134
+ end
135
+
136
+ # Validate message
137
+ def validate_message(message)
138
+ if(message.instance_stamp > message.instance_stamp + (message.connection.endpoint_settings[:msg_timeout] / 1000.0))
139
+ warn "Message exceeded timeout! Discarding. (#{message})"
140
+ if(removal_callback)
141
+ consumer.send(removal_callback, [message])
142
+ end
143
+ nil
144
+ else
145
+ message
146
+ end
147
+ end
148
+
149
+ end
150
+ end
151
+ end
@@ -52,15 +52,24 @@ module Krakow
52
52
  # Remove message metadata from registry
53
53
  #
54
54
  # @param message [Krakow::FrameType::Message, String] message or ID
55
- # @return [Krakow::Connection]
55
+ # @return [Krakow::Connection, NilClass]
56
56
  def unregister_message(message)
57
57
  msg_id = message.respond_to?(:message_id) ? message.message_id : message.to_s
58
58
  connection = connection_lookup(flight_record[msg_id])
59
- registry_info = registry_lookup(connection.identifier)
60
59
  flight_record.delete(msg_id)
61
- registry_info[:in_flight] -= 1
62
- calculate_ready!(connection.identifier)
63
- connection
60
+ if(connection)
61
+ begin
62
+ ident = connection.identifier
63
+ registry_info = registry_lookup(ident)
64
+ registry_info[:in_flight] -= 1
65
+ calculate_ready!(ident)
66
+ connection
67
+ rescue Celluloid::DeadActorError
68
+ warn 'Connection is dead. No recalculation applied on ready.'
69
+ end
70
+ else
71
+ warn 'No connection associated to message via lookup. No recalculation applied on ready.'
72
+ end
64
73
  end
65
74
 
66
75
  # Return the currently configured RDY value for given connnection
@@ -97,10 +106,14 @@ module Krakow
97
106
  # @param connection_identifier [String]
98
107
  # @return [Integer]
99
108
  def register_message(message, connection_identifier)
100
- registry_info = registry_lookup(connection_identifier)
101
- registry_info[:in_flight] += 1
102
- flight_record[message.message_id] = connection_identifier
103
- calculate_ready!(connection_identifier)
109
+ if(flight_record[message.message_id])
110
+ abort KeyError.new "Message is already registered in flight record! (#{message.message_id})"
111
+ else
112
+ registry_info = registry_lookup(connection_identifier)
113
+ registry_info[:in_flight] += 1
114
+ flight_record[message.message_id] = connection_identifier
115
+ calculate_ready!(connection_identifier)
116
+ end
104
117
  end
105
118
 
106
119
  # Add connection to make available for RDY distribution
@@ -128,8 +141,8 @@ module Krakow
128
141
  registry.delete(connection_identifier)
129
142
  # remove any in flight messages
130
143
  flight_record.delete_if do |k,v|
131
- if(k == connection_identifier)
132
- warn "Removing in flight reference due to failed connection: #{k}"
144
+ if(v == connection_identifier)
145
+ warn "Removing in flight reference due to failed connection: #{v}"
133
146
  true
134
147
  end
135
148
  end
@@ -156,7 +169,11 @@ module Krakow
156
169
  abort Krakow::Error::LookupFailed.new("Failed to locate in flight message (ID: #{msg_id})")
157
170
  end
158
171
  if(block_given?)
159
- yield connection
172
+ begin
173
+ yield connection
174
+ rescue => e
175
+ abort e
176
+ end
160
177
  else
161
178
  connection
162
179
  end
@@ -5,8 +5,9 @@ module Krakow
5
5
  # Message received from server
6
6
  class Message < FrameType
7
7
 
8
- # @return [Krakow::Consumer]
9
- attr_accessor :origin
8
+ # @return [Float] time of message instance creation
9
+ attr_reader :instance_stamp
10
+ attr_accessor :origin, :connection
10
11
 
11
12
  # @!group Attributes
12
13
 
@@ -22,6 +23,11 @@ module Krakow
22
23
 
23
24
  # @!endgroup
24
25
 
26
+ def initialize(*args)
27
+ super
28
+ @instance_stamp = Time.now.to_f
29
+ end
30
+
25
31
  # Message content
26
32
  #
27
33
  # @return [String]
@@ -38,6 +44,15 @@ module Krakow
38
44
  @origin
39
45
  end
40
46
 
47
+ # @return [Krakow::Connection]
48
+ def connection
49
+ unless(@connection)
50
+ error 'No origin connection has been specified for this message'
51
+ abort Krakow::Error::ConnectionNotFound.new('No connection specified for this message')
52
+ end
53
+ @connection
54
+ end
55
+
41
56
  # Proxy to [Krakow::Consumer#confirm]
42
57
  def confirm(*args)
43
58
  origin.confirm(*[self, *args].compact)
@@ -0,0 +1,102 @@
1
+ require 'krakow'
2
+ require 'socket'
3
+
4
+ module Krakow
5
+ class Ksocket
6
+
7
+ include Utils::Lazy
8
+ include Celluloid
9
+
10
+ # @return [String]
11
+ attr_reader :buffer
12
+
13
+ finalizer :closedown_socket
14
+
15
+ # Teardown helper
16
+ def closedown_socket
17
+ @writing = @reading = false
18
+ if(socket && !socket.closed?)
19
+ socket.close
20
+ end
21
+ end
22
+
23
+ # Create new socket wrapper
24
+ #
25
+ # @param args [Hash]
26
+ # @option args [Socket-ish] :socket
27
+ # @option args [String] :host
28
+ # @option args [Integer] :port
29
+ # @return [self]
30
+ def initialize(args={})
31
+ if(args[:socket])
32
+ @socket = args[:socket]
33
+ else
34
+ unless([:host, :port].all?{|k| args.include?(k)})
35
+ raise ArgumentError.new 'Missing required arguments. Expecting `:socket` or `:host` and `:port`.'
36
+ end
37
+ @socket = TCPSocket.new(args[:host], args[:port])
38
+ end
39
+ @buffer = ''
40
+ async.read_loop
41
+ end
42
+
43
+ # @return [TrueClass, FalseClass] read loop enabled
44
+ def reading?
45
+ !!@reading
46
+ end
47
+
48
+ # Read from socket and push into local Queue
49
+ def read_loop
50
+ unless(reading?)
51
+ @reading = true
52
+ while(reading?)
53
+ res = defer do
54
+ Kernel.select([socket], nil, nil, nil)
55
+ socket{|s| s.readpartial(1024)}
56
+ end
57
+ if(res)
58
+ debug "Received content from socket: #{res.inspect}"
59
+ buffer << res
60
+ signal(:content_read)
61
+ else
62
+ debug 'No content received from socket read. Ignoring.'
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # Fetch bytes from socket
69
+ #
70
+ # @param n [Integer]
71
+ # @return [String]
72
+ def get(n)
73
+ until(buffer.length >= n)
74
+ wait(:content_read)
75
+ end
76
+ buffer.slice!(0, n)
77
+ end
78
+ alias_method :recv, :get
79
+ alias_method :read, :get
80
+ alias_method :sysread, :get
81
+ alias_method :readpartial, :get
82
+
83
+ # Push bytes to socket
84
+ #
85
+ # @param line [String]
86
+ # @return [Integer]
87
+ def put(line)
88
+ socket{|s| s.write(line)}
89
+ end
90
+ alias_method :write, :put
91
+
92
+ # @return [Socket]
93
+ def socket
94
+ if(block_given?)
95
+ yield @socket
96
+ else
97
+ @socket
98
+ end
99
+ end
100
+
101
+ end
102
+ end
@@ -14,12 +14,13 @@ module Krakow
14
14
  include Celluloid
15
15
 
16
16
  trap_exit :connection_failure
17
- finalizer :goodbye_my_love!
17
+ finalizer :producer_cleanup
18
18
 
19
19
  # set exclusive methods
20
20
  exclusive :write
21
21
 
22
22
  attr_reader :connection
23
+ attr_reader :notifier
23
24
 
24
25
  # @!group Attributes
25
26
 
@@ -49,6 +50,7 @@ module Krakow
49
50
  #
50
51
  # @return nil
51
52
  def connect
53
+ @connecting = true
52
54
  info "Establishing connection to: #{host}:#{port}"
53
55
  begin
54
56
  con_args = connection_options[:options].dup.tap do |args|
@@ -62,13 +64,14 @@ module Krakow
62
64
  end
63
65
  end
64
66
  @connection = Connection.new(con_args)
65
- connection.init!
66
- self.link connection
67
- info "Connection established: #{connection}"
67
+ @connection.init!
68
+ self.link @connection
69
+ info "Connection established: #{@connection}"
68
70
  nil
69
71
  rescue => e
70
72
  abort e
71
73
  end
74
+ @connecting = false
72
75
  end
73
76
 
74
77
  # @return [String] stringify object
@@ -78,28 +81,38 @@ module Krakow
78
81
 
79
82
  # @return [TrueClass, FalseClass] currently connected to server
80
83
  def connected?
81
- !!(connection && connection.alive? && connection.connected?)
84
+ begin
85
+ !!(!@connecting &&
86
+ connection &&
87
+ connection.alive? &&
88
+ connection.connected?)
89
+ rescue Celluloid::DeadActorError
90
+ false
91
+ end
82
92
  end
83
93
 
84
94
  # Process connection failure and attempt reconnection
85
95
  #
86
96
  # @return [TrueClass]
87
- def connection_failure(*args)
88
- @connection = nil
89
- begin
90
- warn "Connection failure detected for #{host}:#{port}"
91
- connect
92
- rescue => e
93
- warn "Failed to establish connection to #{host}:#{port}. Pausing #{reconnect_interval} before retry"
94
- sleep reconnect_interval
95
- connect
97
+ def connection_failure(obj, reason)
98
+ if(obj == connection && !reason.nil?)
99
+ begin
100
+ @connection = nil
101
+ warn "Connection failure detected for #{host}:#{port} - #{reason}"
102
+ obj.terminate if obj.alive?
103
+ connect
104
+ rescue => reason
105
+ warn "Failed to establish connection to #{host}:#{port}. Pausing #{reconnect_interval} before retry"
106
+ sleep reconnect_interval
107
+ retry
108
+ end
96
109
  end
97
110
  true
98
111
  end
99
112
 
100
113
  # Instance destructor
101
114
  # @return nil
102
- def goodbye_my_love!
115
+ def producer_cleanup
103
116
  debug 'Tearing down producer'
104
117
  if(connection && connection.alive?)
105
118
  connection.terminate
@@ -112,13 +125,15 @@ module Krakow
112
125
  # Write message to server
113
126
  #
114
127
  # @param message [String] message to write
115
- # @return [Krakow::FrameType::Error,nil]
128
+ # @return [Krakow::FrameType, TrueClass]
129
+ # @note if connection response wait is set to 0, writes will
130
+ # return a `true` value on completion
116
131
  # @raise [Krakow::Error::ConnectionUnavailable]
117
132
  def write(*message)
118
133
  if(message.empty?)
119
134
  abort ArgumentError.new 'Expecting one or more messages to send. None provided.'
120
135
  end
121
- if(connection && connection.alive?)
136
+ begin
122
137
  if(message.size > 1)
123
138
  debug 'Multiple message publish'
124
139
  connection.transmit(
@@ -136,8 +151,10 @@ module Krakow
136
151
  )
137
152
  )
138
153
  end
139
- else
140
- abort Error::ConnectionUnavailable.new 'Remote connection is unavailable!'
154
+ rescue Celluloid::Task::TerminatedError
155
+ abort Error::ConnectionUnavailable.new 'Connection is currently unavailable'
156
+ rescue => e
157
+ abort e
141
158
  end
142
159
  end
143
160
 
@@ -72,7 +72,12 @@ module Krakow
72
72
  begin
73
73
  response = MultiJson.load(response.body.to_s)
74
74
  rescue MultiJson::LoadError
75
- response = {'status_code' => response.code, 'status_txt' => response.body.to_s, 'data' => nil}
75
+ response = {
76
+ 'status_code' => response.code,
77
+ 'status_txt' => response.body.to_s,
78
+ 'response' => response.body.to_s,
79
+ 'data' => nil,
80
+ }
76
81
  end
77
82
  Response.new(response)
78
83
  end
@@ -1,4 +1,4 @@
1
1
  module Krakow
2
2
  # Current version
3
- VERSION = Gem::Version.new('0.3.12')
3
+ VERSION = Gem::Version.new('0.4.0')
4
4
  end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: krakow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.12
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Roberts
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-12-15 00:00:00.000000000 Z
11
+ date: 2015-02-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: celluloid-io
14
+ name: celluloid
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -66,6 +66,48 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: childprocess
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: snappy
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
69
111
  description: NSQ ruby library
70
112
  email: code@chrisroberts.org
71
113
  executables: []
@@ -99,6 +141,7 @@ files:
99
141
  - lib/krakow/connection_features/snappy_frames.rb
100
142
  - lib/krakow/connection_features/ssl.rb
101
143
  - lib/krakow/consumer.rb
144
+ - lib/krakow/consumer/queue.rb
102
145
  - lib/krakow/discovery.rb
103
146
  - lib/krakow/distribution.rb
104
147
  - lib/krakow/distribution/default.rb
@@ -107,6 +150,7 @@ files:
107
150
  - lib/krakow/frame_type/error.rb
108
151
  - lib/krakow/frame_type/message.rb
109
152
  - lib/krakow/frame_type/response.rb
153
+ - lib/krakow/ksocket.rb
110
154
  - lib/krakow/producer.rb
111
155
  - lib/krakow/producer/http.rb
112
156
  - lib/krakow/utils.rb