krakow 0.3.12 → 0.4.0

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: 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