nsq-krakow 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +84 -0
  3. data/CONTRIBUTING.md +25 -0
  4. data/LICENSE +13 -0
  5. data/README.md +249 -0
  6. data/krakow.gemspec +22 -0
  7. data/lib/krakow.rb +25 -0
  8. data/lib/krakow/command.rb +89 -0
  9. data/lib/krakow/command/auth.rb +36 -0
  10. data/lib/krakow/command/cls.rb +24 -0
  11. data/lib/krakow/command/fin.rb +31 -0
  12. data/lib/krakow/command/identify.rb +55 -0
  13. data/lib/krakow/command/mpub.rb +39 -0
  14. data/lib/krakow/command/nop.rb +14 -0
  15. data/lib/krakow/command/pub.rb +37 -0
  16. data/lib/krakow/command/rdy.rb +31 -0
  17. data/lib/krakow/command/req.rb +32 -0
  18. data/lib/krakow/command/sub.rb +36 -0
  19. data/lib/krakow/command/touch.rb +31 -0
  20. data/lib/krakow/connection.rb +417 -0
  21. data/lib/krakow/connection_features.rb +10 -0
  22. data/lib/krakow/connection_features/deflate.rb +82 -0
  23. data/lib/krakow/connection_features/snappy_frames.rb +129 -0
  24. data/lib/krakow/connection_features/ssl.rb +75 -0
  25. data/lib/krakow/consumer.rb +355 -0
  26. data/lib/krakow/consumer/queue.rb +151 -0
  27. data/lib/krakow/discovery.rb +57 -0
  28. data/lib/krakow/distribution.rb +229 -0
  29. data/lib/krakow/distribution/default.rb +159 -0
  30. data/lib/krakow/exceptions.rb +30 -0
  31. data/lib/krakow/frame_type.rb +66 -0
  32. data/lib/krakow/frame_type/error.rb +26 -0
  33. data/lib/krakow/frame_type/message.rb +74 -0
  34. data/lib/krakow/frame_type/response.rb +26 -0
  35. data/lib/krakow/ksocket.rb +102 -0
  36. data/lib/krakow/producer.rb +162 -0
  37. data/lib/krakow/producer/http.rb +224 -0
  38. data/lib/krakow/utils.rb +9 -0
  39. data/lib/krakow/utils/lazy.rb +125 -0
  40. data/lib/krakow/utils/logging.rb +43 -0
  41. data/lib/krakow/version.rb +4 -0
  42. metadata +184 -0
@@ -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
@@ -0,0 +1,57 @@
1
+ require 'uri'
2
+ require 'http'
3
+ require 'multi_json'
4
+ require 'krakow'
5
+
6
+ module Krakow
7
+
8
+ # Provides queue topic discovery
9
+ class Discovery
10
+
11
+ include Utils::Lazy
12
+
13
+ # @!group Attributes
14
+
15
+ # @!macro [attach] attribute
16
+ # @!method $1
17
+ # @return [$2] the $1 $0
18
+ # @!method $1?
19
+ # @return [TrueClass, FalseClass] truthiness of the $1 $0
20
+ attribute :nsqlookupd, [Array, String], :required => true
21
+
22
+ # @!endgroup
23
+
24
+ # Get list of end points with given topic name available
25
+ #
26
+ # @param topic [String] topic name
27
+ # @return [Array<Hash>]
28
+ def lookup(topic)
29
+ result = [nsqlookupd].flatten.map do |location|
30
+ uri = URI.parse(location)
31
+ uri.path = '/lookup'
32
+ uri.query = "topic=#{topic}&ts=#{Time.now.to_i}"
33
+ begin
34
+ debug "Requesting lookup for topic #{topic} - #{uri}"
35
+ content = HTTP.with(:accept => 'application/octet-stream').get(uri.to_s)
36
+ unless(content.respond_to?(:to_hash))
37
+ data = MultiJson.load(content.to_s)
38
+ else
39
+ data = content.to_hash
40
+ end
41
+ debug "Lookup response (#{uri.to_s}): #{data.inspect}"
42
+ if(data['data'] && data['data']['producers'])
43
+ data['data']['producers'].map do |producer|
44
+ Hash[*producer.map{|k,v| [k.to_sym, v]}.flatten]
45
+ end
46
+ end
47
+ rescue => e
48
+ warn "Lookup exception encountered: #{e.class.name} - #{e}"
49
+ nil
50
+ end
51
+ end.compact.flatten(1).uniq
52
+ debug "Discovery lookup result: #{result.inspect}"
53
+ result
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,229 @@
1
+ require 'krakow'
2
+
3
+ module Krakow
4
+ # Message distribution
5
+ # @abstract
6
+ class Distribution
7
+
8
+ autoload :Default, 'krakow/distribution/default'
9
+ # autoload :ProducerWeighted, 'krakow/distribution/producer_weighted'
10
+ # autoload :ConsumerWeighted, 'krakow/distribution/consumer_weighted'
11
+
12
+ include Celluloid
13
+ include Utils::Lazy
14
+ # @!parse include Krakow::Utils::Lazy::InstanceMethods
15
+ # @!parse extend Krakow::Utils::Lazy::ClassMethods
16
+
17
+ attr_accessor :ideal, :flight_record, :registry
18
+
19
+ # @!group Attributes
20
+
21
+ # @!macro [attach] attribute
22
+ # @!method $1
23
+ # @return [$2] the $1 $0
24
+ # @!method $1?
25
+ # @return [TrueClass, FalseClass] truthiness of the $1 $0
26
+ attribute :consumer, Krakow::Consumer, :required => true
27
+ attribute :watch_dog_interval, Numeric, :default => 1.0
28
+ attribute :backoff_interval, Numeric
29
+ attribute :max_in_flight, Integer, :default => 1
30
+
31
+ # @!endgroup
32
+
33
+ def initialize(args={})
34
+ super
35
+ @ideal = 0
36
+ @flight_record = {}
37
+ @registry = {}
38
+ end
39
+
40
+ # [Abstract] Reset flight distributions
41
+ def redistribute!
42
+ raise NotImplementedError.new 'Custom `#redistrubute!` method must be provided!'
43
+ end
44
+
45
+ # [Abstract] Determine RDY value for given connection
46
+ # @param connection_identifier [String]
47
+ # @return [Integer]
48
+ def calculate_ready!(connection_identifier)
49
+ raise NotImplementedError.new 'Custom `#calculate_ready!` method must be provided!'
50
+ end
51
+
52
+ # Remove message metadata from registry
53
+ #
54
+ # @param message [Krakow::FrameType::Message, String] message or ID
55
+ # @return [Krakow::Connection, NilClass]
56
+ def unregister_message(message)
57
+ msg_id = message.respond_to?(:message_id) ? message.message_id : message.to_s
58
+ connection = connection_lookup(flight_record[msg_id])
59
+ flight_record.delete(msg_id)
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
73
+ end
74
+
75
+ # Return the currently configured RDY value for given connnection
76
+ #
77
+ # @param connection_identifier [String]
78
+ # @return [Integer]
79
+ def ready_for(connection_identifier)
80
+ registry_lookup(connection_identifier)[:ready]
81
+ end
82
+
83
+
84
+ # Send RDY for given connection
85
+ #
86
+ # @param connection [Krakow::Connection]
87
+ # @return [Krakow::FrameType::Error,nil]
88
+ def set_ready_for(connection, *_)
89
+ connection.transmit(
90
+ Command::Rdy.new(
91
+ :count => ready_for(connection.identifier)
92
+ )
93
+ )
94
+ end
95
+
96
+ # Initial ready value used for new connections
97
+ #
98
+ # @return [Integer]
99
+ def initial_ready
100
+ ideal > 0 ? 1 : 0
101
+ end
102
+
103
+ # Registers message into registry and configures for distribution
104
+ #
105
+ # @param message [FrameType::Message]
106
+ # @param connection_identifier [String]
107
+ # @return [Integer]
108
+ def register_message(message, 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
117
+ end
118
+
119
+ # Add connection to make available for RDY distribution
120
+ #
121
+ # @param connection [Krakow::Connection]
122
+ # @return [TrueClass]
123
+ def add_connection(connection)
124
+ unless(registry[connection.identifier])
125
+ registry[connection.identifier] = {
126
+ :ready => initial_ready,
127
+ :in_flight => 0,
128
+ :failures => 0,
129
+ :backoff_until => 0
130
+ }
131
+ end
132
+ true
133
+ end
134
+
135
+ # Remove connection from RDY distribution
136
+ #
137
+ # @param connection_identifier [String]
138
+ # @return [TrueClass]
139
+ def remove_connection(connection_identifier, *args)
140
+ # remove connection from registry
141
+ registry.delete(connection_identifier)
142
+ # remove any in flight messages
143
+ flight_record.delete_if do |k,v|
144
+ if(v == connection_identifier)
145
+ warn "Removing in flight reference due to failed connection: #{v}"
146
+ true
147
+ end
148
+ end
149
+ true
150
+ end
151
+
152
+ # Return connection associated with given registry key
153
+ #
154
+ # @param identifier [String] connection identifier
155
+ # @return [Krakow::Connection, nil]
156
+ def connection_lookup(identifier)
157
+ consumer.connection(identifier)
158
+ end
159
+
160
+ # Return source connection for given message ID
161
+ #
162
+ # @param msg_id [String]
163
+ # @yield execute with connection
164
+ # @yieldparam connection [Krakow::Connection]
165
+ # @return [Krakow::Connection, Object]
166
+ def in_flight_lookup(msg_id)
167
+ connection = connection_lookup(flight_record[msg_id])
168
+ unless(connection)
169
+ abort Krakow::Error::LookupFailed.new("Failed to locate in flight message (ID: #{msg_id})")
170
+ end
171
+ if(block_given?)
172
+ begin
173
+ yield connection
174
+ rescue => e
175
+ abort e
176
+ end
177
+ else
178
+ connection
179
+ end
180
+ end
181
+
182
+ # Return registry information for given connection
183
+ # @param connection_identifier [String]
184
+ # @return [Hash] registry information
185
+ # @raise [Krakow::Error::LookupFailed]
186
+ def registry_lookup(connection_identifier)
187
+ registry[connection_identifier] ||
188
+ abort(Krakow::Error::LookupFailed.new("Failed to locate connection information in registry (#{connection_identifier})"))
189
+ end
190
+
191
+ # @return [Array<Krakow::Connection>] connections in registry
192
+ def connections
193
+ registry.keys.map do |identifier|
194
+ connection_lookup(identifier)
195
+ end.compact
196
+ end
197
+
198
+ # Log failure of processed message
199
+ #
200
+ # @param connection_identifier [String]
201
+ # @return [TrueClass]
202
+ def failure(connection_identifier)
203
+ if(backoff_interval)
204
+ registry_info = registry_lookup(connection_identifier)
205
+ registry_info[:failures] += 1
206
+ registry_info[:backoff_until] = Time.now.to_i + (registry_info[:failures] * backoff_interval)
207
+ end
208
+ true
209
+ end
210
+
211
+ # Log success of processed message
212
+ #
213
+ # @param connection_identifier [String]
214
+ # @return [TrueClass]
215
+ def success(connection_identifier)
216
+ if(backoff_interval)
217
+ registry_info = registry_lookup(connection_identifier)
218
+ if(registry_info[:failures] > 1)
219
+ registry_info[:failures] -= 1
220
+ registry_info[:backoff_until] = Time.now.to_i + (registry_info[:failures] * backoff_interval)
221
+ else
222
+ registry_info[:failures] = 0
223
+ end
224
+ end
225
+ true
226
+ end
227
+
228
+ end
229
+ end
@@ -0,0 +1,159 @@
1
+ require 'krakow'
2
+
3
+ module Krakow
4
+ class Distribution
5
+ # Default distribution implementation. This uses a round-robin
6
+ # approach for less than ideal states.
7
+ class Default < Distribution
8
+
9
+ attr_reader :less_than_ideal_stack, :watch_dog
10
+
11
+ # recalculate `ideal` and update RDY on connections
12
+ def redistribute!
13
+ @ideal = registry.size < 1 ? 0 : max_in_flight / registry.size
14
+ debug "Distribution calculated ideal: #{ideal}"
15
+ if(less_than_ideal?)
16
+ registry.each do |connection_id, reg_info|
17
+ reg_info[:ready] = 0
18
+ end
19
+ max_in_flight.times do
20
+ less_than_ideal_ready!
21
+ end
22
+ connections.each do |connection|
23
+ set_ready_for(connection, :force)
24
+ end
25
+ watch_dog.cancel if watch_dog
26
+ @watch_dog = every(watch_dog_interval) do
27
+ force_unready
28
+ end
29
+ else
30
+ if(watch_dog)
31
+ watch_dog.cancel
32
+ @watch_dog = nil
33
+ end
34
+ connections.each do |connection|
35
+ current_ready = ready_for(connection.identifier)
36
+ calculate_ready!(connection.identifier)
37
+ unless(current_ready == ready_for(connection.identifier))
38
+ debug "Redistribution ready setting update for connection #{connection}"
39
+ set_ready_for(connection)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Is ideal less than 1
46
+ #
47
+ # @return [TrueClass, FalseClass]
48
+ def less_than_ideal?
49
+ ideal < 1
50
+ end
51
+
52
+ # Find next connection to receive RDY count
53
+ #
54
+ # @return [Krakow::Connection, nil]
55
+ def less_than_ideal_ready!
56
+ admit_defeat = false
57
+ connection = nil
58
+ until(connection || (admit_defeat && less_than_ideal_stack.empty?))
59
+ if(less_than_ideal_stack.nil? || less_than_ideal_stack.empty?)
60
+ @less_than_ideal_stack = waiting_connections
61
+ admit_defeat = true
62
+ end
63
+ con = less_than_ideal_stack.pop
64
+ if(con)
65
+ unless(registry_lookup(con.identifier)[:backoff_until] > Time.now.to_i)
66
+ connection = con
67
+ end
68
+ end
69
+ end
70
+ if(connection)
71
+ registry_lookup(connection.identifier)[:ready] = 1
72
+ connection
73
+ end
74
+ end
75
+
76
+ # Adds extra functionality to provide round robin RDY setting
77
+ # when in less than ideal state
78
+ #
79
+ # @param connection [Krakow::Connection]
80
+ # @param args [Symbol]
81
+ # @return [Krakow::FrameType::Error, nil]
82
+ def set_ready_for(connection, *args)
83
+ super connection
84
+ if(less_than_ideal? && !args.include?(:force))
85
+ debug "RDY set ignored due to less than ideal state (con: #{connection})"
86
+ con = less_than_ideal_ready!
87
+ if(con)
88
+ watch_dog.reset if watch_dog
89
+ super con
90
+ else
91
+ warn 'Failed to set RDY state while less than ideal. Connection stack is empty!'
92
+ end
93
+ end
94
+ end
95
+
96
+ # Update connection ready count
97
+ # @param connection_identifier [String]
98
+ # @return [Integer, nil]
99
+ def calculate_ready!(connection_identifier)
100
+ begin
101
+ registry_info = registry_lookup(connection_identifier)
102
+ unless(less_than_ideal?)
103
+ registry_info[:ready] = ideal - registry_info[:in_flight]
104
+ if(registry_info[:ready] < 0 || registry_info[:backoff_until] > Time.now.to_i)
105
+ registry_info[:ready] = 0
106
+ registry_info[:backoff_timer].cancel if registry[:backoff_timer]
107
+ registry_info[:backoff_timer] = after(registry_info[:backoff_until] - Time.now.to_i) do
108
+ calculate_ready!(connection_identifier)
109
+ set_ready_for(connection_lookup(connection_identifier)) unless less_than_ideal?
110
+ end
111
+ end
112
+ registry_info[:ready]
113
+ else
114
+ registry_info[:ready] = 0
115
+ end
116
+ rescue Error::ConnectionFailure
117
+ warn 'Failed connection encountered!'
118
+ rescue Error::ConnectionUnavailable
119
+ warn 'Unavailable connection encountered!'
120
+ end
121
+ end
122
+
123
+ # All connections without RDY state
124
+ #
125
+ # @return [Array<Krakow::Connection>]
126
+ def waiting_connections
127
+ registry.find_all do |conn_id, info|
128
+ info[:ready] < 1 && info[:in_flight] < 1 && info[:backoff_until] < Time.now.to_i
129
+ end.map{|conn_id, info| connection_lookup(conn_id) }.compact
130
+ end
131
+
132
+ # All connections with RDY state
133
+ #
134
+ # @return [Array<Krakow::Connection>]
135
+ def rdy_connections
136
+ registry.find_all do |conn_id, info|
137
+ info[:ready] > 0
138
+ end.map{|conn_id, info| connection_lookup(conn_id) }.compact
139
+ end
140
+
141
+ # Force a connection to give up RDY state so next in stack can receive
142
+ #
143
+ # @return [nil]
144
+ def force_unready
145
+ debug 'Forcing a connection into an unready state due to less than ideal state'
146
+ connection = rdy_connections.shuffle.first
147
+ if(connection)
148
+ debug "Stripping RDY state from connection: #{connection}"
149
+ calculate_ready!(connection.identifier)
150
+ set_ready_for(connection)
151
+ else
152
+ warn "Failed to locate available connection for RDY aquisition!"
153
+ end
154
+ nil
155
+ end
156
+
157
+ end
158
+ end
159
+ end