krakow 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,2 +1,9 @@
1
- ## v0.0.1
1
+ ## v0.1.0
2
+ * Add logging support
3
+ * Include valid responses within commands
4
+ * Segregate responses from messages
5
+ * Manage connections in consumer (closed/reconnect)
6
+ * Add message distribution support
7
+
8
+ ## v0.0.1
2
9
  * Initial release
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- krakow (0.1.0)
4
+ krakow (0.0.1)
5
5
  celluloid-io
6
6
  http
7
7
  multi_json
data/README.md CHANGED
@@ -12,7 +12,7 @@ producer = Krakow::Producer(
12
12
  :port => 'PORT',
13
13
  :topic => 'target'
14
14
  )
15
- producer.write('direct hit!')
15
+ producer.write('KRAKOW!', 'KRAKOW!')
16
16
  ```
17
17
 
18
18
  ## Zargons
@@ -26,11 +26,74 @@ consumer = Krakow::Consumer(
26
26
  :channel => 'ship'
27
27
  )
28
28
 
29
- message = consumer.queue.pop
30
- # do stuff
31
- consumer.confirm(message.message_id)
29
+ consumer.queue.size # => 2
30
+ 2.times do
31
+ msg = consumer.queue.pop
32
+ puts "Received: #{msg}"
33
+ consumer.confirm(msg.message_id)
34
+ end
32
35
  ```
33
36
 
37
+ ## What is this?
38
+
39
+ It's a Ruby library for NSQ[1] using Celluloid[2] under the hood.
40
+
41
+ ## Information and FAQ that I totally made up
42
+
43
+ ### Max in flight for consumers is 1, regardless of number of producers
44
+
45
+ Yep, that's right. Just one lowly message at a time. And that's probably not what
46
+ you want, so adjust it when you create your consumer instance.
47
+
48
+ ```ruby
49
+ require 'krakow'
50
+
51
+ consumer = Krakow::Consumer(
52
+ :nsqlookupd => 'http://HOST:PORT',
53
+ :topic => 'target',
54
+ :channel => 'ship',
55
+ :max_in_flight => 30
56
+ )
57
+ ```
58
+
59
+ ### Please make it shutup!
60
+
61
+ Sure:
62
+
63
+ ```
64
+ Krakow::Utils::Logging.level = :warn # :debug / :info / :warn / :error / :fatal
65
+ ```
66
+
67
+ ### Why is it forcing something called an "unready state"?
68
+
69
+ Because forcing starvation is mean.
70
+
71
+ ### I just want to connect to a producer, not a lookup service
72
+
73
+ Fine!
74
+
75
+ ```ruby
76
+ consumer = Krakow::Consumer(
77
+ :host => 'HOST',
78
+ :port => 'PORT',
79
+ :topic => 'target',
80
+ :channel => 'ship',
81
+ :max_in_flight => 30
82
+ )
83
+ ```
84
+ Great for testing, but you really should use the lookup service in the "real world"
85
+
86
+ ### It doesn't work
87
+
88
+ Create an issue on the github repository.
89
+
90
+ #### It doesn't do `x`
91
+
92
+ Create an issue, or even better, send a PR. Just base it off the `develop` branch.
93
+
34
94
  # Info
35
95
  * Repo: https://github.com/chrisroberts/krakow
36
96
  * IRC: Freenode @ spox
97
+
98
+ [1] https://github.com/bitly/nsq
99
+ [2] https://github.com/celluloid/celluloid
@@ -10,6 +10,16 @@ module Krakow
10
10
  "#{name}\n"
11
11
  end
12
12
 
13
+ class << self
14
+ def ok
15
+ %w(CLOSE_WAIT)
16
+ end
17
+
18
+ def error
19
+ %w(E_INVALID)
20
+ end
21
+ end
22
+
13
23
  end
14
24
  end
15
25
  end
@@ -11,6 +11,12 @@ module Krakow
11
11
  "#{name} #{message_id}\n"
12
12
  end
13
13
 
14
+ class << self
15
+ def error
16
+ %w(E_INVALID E_FIN_FAILED)
17
+ end
18
+ end
19
+
14
20
  end
15
21
  end
16
22
  end
@@ -15,10 +15,27 @@ module Krakow
15
15
  end
16
16
 
17
17
  def to_line
18
- payload = MultiJson.dump(arguments)
18
+ filtered = Hash[*
19
+ arguments.map do |key, value|
20
+ unless(value.nil?)
21
+ [key, value]
22
+ end
23
+ end.compact.flatten
24
+ ]
25
+ payload = MultiJson.dump(filtered)
19
26
  [name, "\n", payload.length, payload].pack('a*a*l>a*')
20
27
  end
21
28
 
29
+ class << self
30
+ def ok
31
+ %w(OK)
32
+ end
33
+
34
+ def error
35
+ %w(E_INVALID E_BAD_BODY)
36
+ end
37
+ end
38
+
22
39
  end
23
40
  end
24
41
  end
@@ -14,6 +14,16 @@ module Krakow
14
14
  [name, ' ', topic_name, "\n", formatted_messages.length, messages.size, formatted_messages].pack('a*a*a*a*l>l>a*')
15
15
  end
16
16
 
17
+ class << self
18
+ def ok
19
+ %w(OK)
20
+ end
21
+
22
+ def error
23
+ %w(E_INVALID E_BAD_TOPIC E_BAD_BODY E_BAD_MESSAGE E_MPUB_FAILED)
24
+ end
25
+ end
26
+
17
27
  end
18
28
  end
19
29
  end
@@ -12,6 +12,16 @@ module Krakow
12
12
  [name, ' ', topic_name, "\n", message.length, message].pack('a*a*a*a*l>a*')
13
13
  end
14
14
 
15
+ class << self
16
+ def ok
17
+ %w(OK)
18
+ end
19
+
20
+ def error
21
+ %w(E_INVALID E_BAD_TOPIC E_BAD_MESSAGE E_PUB_FAILED)
22
+ end
23
+ end
24
+
15
25
  end
16
26
  end
17
27
  end
@@ -11,6 +11,12 @@ module Krakow
11
11
  "#{name} #{count}\n"
12
12
  end
13
13
 
14
+ class << self
15
+ def error
16
+ %w(E_INVALID)
17
+ end
18
+ end
19
+
14
20
  end
15
21
  end
16
22
  end
@@ -11,6 +11,12 @@ module Krakow
11
11
  "#{name} #{message_id} #{timeout}\n"
12
12
  end
13
13
 
14
+ class << self
15
+ def error
16
+ %w(E_INVALID E_REQ_FAILED)
17
+ end
18
+ end
19
+
14
20
  end
15
21
  end
16
22
  end
@@ -11,6 +11,16 @@ module Krakow
11
11
  "#{name} #{topic_name} #{channel_name}\n"
12
12
  end
13
13
 
14
+ class << self
15
+ def ok
16
+ %w(OK)
17
+ end
18
+
19
+ def error
20
+ %w(E_INVALID E_BAD_TOPIC E_BAD_CHANNEL)
21
+ end
22
+ end
23
+
14
24
  end
15
25
  end
16
26
  end
@@ -11,6 +11,12 @@ module Krakow
11
11
  "#{name} #{message_id}\n"
12
12
  end
13
13
 
14
+ class << self
15
+ def error
16
+ %w(E_INVALID E_TOUCH_FAILED)
17
+ end
18
+ end
19
+
14
20
  end
15
21
  end
16
22
  end
@@ -3,6 +3,20 @@ module Krakow
3
3
 
4
4
  include Utils::Lazy
5
5
 
6
+ class << self
7
+
8
+ def ok
9
+ []
10
+ end
11
+
12
+ def error
13
+ []
14
+ end
15
+
16
+ end
17
+
18
+ attr_accessor :response
19
+
6
20
  # Return command name
7
21
  def name
8
22
  self.class.name.split('::').last.upcase
@@ -13,6 +27,16 @@ module Krakow
13
27
  raise NoMethodError.new 'No line conversion method defined!'
14
28
  end
15
29
 
30
+ def ok?(response)
31
+ response = response.content if response.is_a?(FrameType)
32
+ self.class.ok.include?(response)
33
+ end
34
+
35
+ def error?(response)
36
+ response = response.content if response.is_a?(FrameType)
37
+ self.class.error.include?(response)
38
+ end
39
+
16
40
  # Make all the commands available
17
41
  Dir.glob(File.join(File.dirname(__FILE__), 'command', '*')).each do |path|
18
42
  autoload(
@@ -16,41 +16,70 @@ module Krakow
16
16
  required! :host, :port
17
17
  optional :version, :queue, :callback
18
18
  arguments[:queue] ||= Queue.new
19
+ arguments[:responses] ||= Queue.new
19
20
  arguments[:version] ||= 'v2'
20
21
  @socket = TCPSocket.new(host, port)
21
22
  end
22
23
 
24
+ def to_s
25
+ "<#{self.class.name}:#{object_id} {#{host}:#{port}}>"
26
+ end
27
+
23
28
  # Initialize the connection
24
29
  def init!
30
+ debug 'Initializing connection'
25
31
  socket.write version.rjust(4).upcase
26
32
  async.process_to_queue!
33
+ info 'Connection initialized'
27
34
  end
28
35
 
29
36
  # message:: Command instance to send
30
37
  # Send the message
31
38
  def transmit(message)
32
- socket.write message.to_line
39
+ output = message.to_line
40
+ debug ">>> #{output}"
41
+ socket.write output
42
+ unless(responses.empty?)
43
+ response = responses.pop
44
+ message.response = response
45
+ if(message.error?(response))
46
+ res = Error::BadResponse.new "Message transmission failed #{message}"
47
+ res.result = response
48
+ abort res
49
+ end
50
+ end
33
51
  end
34
52
 
35
53
  # Cleanup prior to destruction
36
54
  def goodbye_my_love!
55
+ debug 'Tearing down connection'
37
56
  if(socket && !socket.closed?)
38
57
  socket.write Command::Cls.new.to_line
39
58
  socket.close
40
59
  end
41
60
  @socket = nil
61
+ info 'Connection torn down'
42
62
  end
43
63
 
44
64
  # Receive message and return proper FrameType instance
45
65
  def receive
46
- buf = socket.read(8)
66
+ debug 'Read wait for frame start'
67
+ buf = socket.recv(8)
47
68
  if(buf)
48
69
  @receiving = true
70
+ debug "<<< #{buf.inspect}"
49
71
  struct = FrameType.decode(buf)
50
- struct[:data] = socket.read(struct[:size])
72
+ debug "Decoded structure: #{struct.inspect}"
73
+ struct[:data] = socket.recv(struct[:size])
74
+ debug "<<< #{struct[:data].inspect}"
51
75
  @receiving = false
52
- FrameType.build(struct)
76
+ frame = FrameType.build(struct)
77
+ debug "Struct: #{struct.inspect} Frame: #{frame.inspect}"
78
+ frame
53
79
  else
80
+ if(socket.closed?)
81
+ raise Error.new("#{self} encountered closed socket!")
82
+ end
54
83
  nil
55
84
  end
56
85
  end
@@ -65,6 +94,7 @@ module Krakow
65
94
  loop do
66
95
  message = handle(receive)
67
96
  if(message)
97
+ debug "Adding message to queue #{message}"
68
98
  queue << message
69
99
  end
70
100
  end
@@ -75,11 +105,18 @@ module Krakow
75
105
  def handle(message)
76
106
  # Grab heartbeats upfront
77
107
  if(message.is_a?(FrameType::Response) && message.response == '_heartbeat_')
78
- socket.write Command::Nop.new.to_line
108
+ debug 'Responding to heartbeat'
109
+ transmit Command::Nop.new
79
110
  nil
80
111
  else
81
112
  if(callback && callback[:actor] && callback[:method])
82
- callback[:actor].send(callback[:method], message, current_actor)
113
+ debug "Sending #{message} to callback `#{callback[:actor]}##{callback[:method]}`"
114
+ message = callback[:actor].send(callback[:method], message, current_actor)
115
+ end
116
+ if(!message.is_a?(FrameType::Message))
117
+ debug "Captured non-message type response: #{message}"
118
+ responses << message
119
+ nil
83
120
  else
84
121
  message
85
122
  end
@@ -4,82 +4,142 @@ module Krakow
4
4
  include Utils::Lazy
5
5
  include Celluloid
6
6
 
7
+ trap_exit :connection_failure
7
8
  finalizer :goodbye_my_love!
8
9
 
9
- attr_reader :connections, :discovery, :queue
10
+ attr_reader :connections, :discovery, :distribution, :queue
10
11
 
11
12
  def initialize(args={})
12
13
  super
13
14
  required! :topic, :channel
14
- optional :host, :port, :nslookupd, :receive_count
15
+ optional :host, :port, :nslookupd, :max_in_flight
16
+ arguments[:max_in_flight] ||= 1
15
17
  @connections = {}
18
+ @distribution = Distribution::Default.new(
19
+ :max_in_flight => max_in_flight
20
+ )
16
21
  @queue = Queue.new
17
22
  if(nslookupd)
23
+ debug "Connections will be established via lookup #{nslookupd.inspect}"
18
24
  @discovery = Discovery.new(:nslookupd => nslookupd)
19
25
  every(60){ init! }
20
26
  init!
21
- else
27
+ elsif(host && port)
28
+ debug "Connection will be established via direct connection #{host}:#{port}"
22
29
  connection = build_connection(host, port, queue)
23
30
  if(register(connection))
24
- connections[:default] = connection
31
+ info "Registered new connection #{connection}"
32
+ distribution.redistribute!
25
33
  else
26
- raise Error.new("Failed to establish subscription at provided end point (#{host}:#{port}")
34
+ abort ConnectionFailure.new("Failed to establish subscription at provided end point (#{host}:#{port}")
27
35
  end
36
+ else
37
+ abort ConfigurationError.new('No connection information provided!')
28
38
  end
29
39
  end
30
40
 
41
+ def to_s
42
+ "<#{self.class.name}:#{object_id} T:#{topic} C:#{channel}>"
43
+ end
44
+
31
45
  def goodbye_my_love!
46
+ debug 'Tearing down consumer'
32
47
  connections.values.each do |con|
33
- con.terminate
48
+ con.terminate if con.alive?
34
49
  end
50
+ distribution.terminate if distribution && distribution.alive?
51
+ info 'Consumer torn down'
35
52
  end
36
53
 
54
+ # host:: remote address
55
+ # port:: remote port
56
+ # queue:: message store queue
57
+ # Build new `Connection`
37
58
  def build_connection(host, port, queue)
38
59
  connection = Connection.new(
39
60
  :host => host,
40
61
  :port => port,
41
- :queue => queue
62
+ :queue => queue,
63
+ :callback => {
64
+ :actor => current_actor,
65
+ :method => :process_message
66
+ }
42
67
  )
43
68
  end
44
69
 
70
+ # message:: FrameType
71
+ # connection:: Connection
72
+ # Process message if required
45
73
  def process_message(message, connection)
46
- puts 'PROCESSING!'
47
74
  if(message.is_a?(FrameType::Message))
48
- connection.transmit(Command::Rdy.new(:count => receive_count || 1))
75
+ distribution.register_message(message, connection)
49
76
  end
50
77
  message
51
78
  end
52
79
 
80
+ # connection:: Connection
81
+ # Send RDY for connection based on distribution rules
82
+ def update_ready!(connection)
83
+ distribution.set_ready_for(connection)
84
+ end
85
+
53
86
  # Requests lookup and adds connections
54
87
  def init!
88
+ debug 'Running consumer `init!` connection builds'
55
89
  found = discovery.lookup(topic)
90
+ debug "Discovery results: #{found.inspect}"
91
+ connection = nil
56
92
  found.each do |node|
57
- unless(connections[node[:hostname]])
93
+ debug "Processing discovery result: #{node.inspect}"
94
+ key = "#{node[:broadcast_address]}_#{node[:tcp_port]}"
95
+ unless(connections[key])
58
96
  connection = build_connection(node[:broadcast_address], node[:tcp_port], queue)
59
- connections[node[:hostname]] = connection if register(connection)
97
+ info "Registered new connection #{connection}" if register(connection)
98
+ else
99
+ debug "Discovery result already registered: #{node.inspect}"
60
100
  end
61
101
  end
102
+ distribution.redistribute! if connection
62
103
  end
63
104
 
64
105
  # connection:: Connection
65
106
  # Registers connection with subscription. Returns false if failed
66
107
  def register(connection)
67
- connection.init!
68
- connection.transmit(Command::Sub.new(:topic_name => topic, :channel_name => channel))
69
- unless(connection.queue.pop.is_a?(FrameType::Error))
70
- connection.transmit(Command::Rdy.new(:count => receive_count || 1))
108
+ begin
109
+ connection.init!
110
+ connection.transmit(Command::Sub.new(:topic_name => topic, :channel_name => channel))
111
+ self.link connection
112
+ connections["#{connection.host}_#{connection.port}"] = connection
113
+ distribution.add_connection(connection)
71
114
  true
72
- else
115
+ rescue Error::BadResponse => e
116
+ debug "Failed to establish connection: #{e.result.error}"
73
117
  connection.terminate
74
118
  false
75
119
  end
76
120
  end
77
121
 
122
+ # con:: actor
123
+ # reason:: Exception
124
+ # Remove connection from register if found
125
+ def connection_failure(con, reason)
126
+ connections.delete_if do |key, value|
127
+ if(value == con)
128
+ warn "Connection failure detected. Removing connection: #{key}"
129
+ discovery.remove_connection(con)
130
+ true
131
+ end
132
+ end
133
+ end
134
+
78
135
  # message_id:: Message ID
79
136
  # Confirm message has been processed
80
137
  def confirm(message_id)
81
- writer.transmit(Command::Fin.new(:message_id => message_id))
82
- writer.transmit(Command::Rdy.new(:count => (receive_count - queue.size) + 1))
138
+ distribution.in_flight_lookup(message_id) do |connection|
139
+ connection.transmit(Command::Fin.new(:message_id => message_id))
140
+ end
141
+ connection = distribution.unregister_message(message_id)
142
+ update_ready!(connection)
83
143
  true
84
144
  end
85
145
 
@@ -87,14 +147,17 @@ module Krakow
87
147
  # timeout:: Requeue timeout (default is none)
88
148
  # Requeue message (processing failure)
89
149
  def requeue(message_id, timeout=0)
90
- writer.transmit(Command::Req.new(:message_id => message_id, :timeout => timeout))
91
- end
92
-
93
- # Attempt to return free connection from pool for writing
94
- def writer
95
- connections.values.detect do |con|
96
- !con.receiving?
97
- end || connections.values.first
150
+ distribution.in_flight_lookup(message_id) do |connection|
151
+ connection.transmit(
152
+ Command::Req.new(
153
+ :message_id => message_id,
154
+ :timeout => timeout
155
+ )
156
+ )
157
+ end
158
+ connection = distributrion.unregister_message(message_id)
159
+ update_ready!(connection)
160
+ true
98
161
  end
99
162
 
100
163
  end
@@ -15,26 +15,31 @@ module Krakow
15
15
  # topic:: Topic name
16
16
  # Return list of end points with given topic name available
17
17
  def lookup(topic)
18
- [nslookupd].flatten.map do |location|
18
+ result = [nslookupd].flatten.map do |location|
19
19
  uri = URI.parse(location)
20
20
  uri.path = '/lookup'
21
21
  uri.query = "topic=#{topic}&ts=#{Time.now.to_i}"
22
22
  begin
23
+ debug "Requesting lookup for topic #{topic} - #{uri}"
23
24
  content = HTTP.with(:accept => 'application/octet-stream').get(uri.to_s)
24
25
  unless(content.respond_to?(:to_hash))
25
26
  data = MultiJson.load(content.to_s)
26
27
  else
27
28
  data = content.to_hash
28
29
  end
30
+ debug "Lookup response (#{uri.to_s}): #{data.inspect}"
29
31
  if(data['data'] && data['data']['producers'])
30
32
  data['data']['producers'].map do |producer|
31
33
  Hash[*producer.map{|k,v| [k.to_sym, v]}.flatten]
32
34
  end
33
35
  end
34
36
  rescue => e
37
+ warn "Lookup exception encountered: #{e.class.name} - #{e}"
35
38
  nil
36
39
  end
37
40
  end.compact.flatten(1).uniq
41
+ debug "Discovery lookup result: #{result.inspect}"
42
+ result
38
43
  end
39
44
 
40
45
  end
@@ -0,0 +1,124 @@
1
+ module Krakow
2
+ class Distribution
3
+ class Default < Distribution
4
+
5
+ attr_reader :less_than_ideal_stack, :watch_dog
6
+
7
+ # recalculate `ideal` and update RDY on connections
8
+ def redistribute!
9
+ @ideal = max_in_flight / registry.size
10
+ debug "Distribution calculated ideal: #{ideal}"
11
+ if(less_than_ideal?)
12
+ registry.each do |connection, reg_info|
13
+ reg_info[:ready] = 0
14
+ end
15
+ max_in_flight.times do
16
+ less_than_ideal_ready!
17
+ end
18
+ connections.each do |connection|
19
+ set_ready_for(connection, :force)
20
+ end
21
+ # TODO: Make interval configurable
22
+ watch_dog.cancel if watch_dog
23
+ @watch_dog = every(5) do
24
+ force_unready
25
+ end
26
+ else
27
+ if(watch_dog)
28
+ watch_dog.cancel
29
+ @watch_dog = nil
30
+ end
31
+ connections.each do |connection|
32
+ current_ready = ready_for(connection)
33
+ calculate_ready!(connection)
34
+ unless(current_ready == ready_for(connection))
35
+ debug "Redistribution ready setting update for connection #{connection}"
36
+ set_ready_for(connection)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # Returns if `ideal` is less than 1
43
+ def less_than_ideal?
44
+ ideal < 1
45
+ end
46
+
47
+ # Returns next connection to receive RDY count
48
+ def less_than_ideal_ready!
49
+ if(less_than_ideal_stack.nil? || less_than_ideal_stack.empty?)
50
+ @less_than_ideal_stack = waiting_connections
51
+ end
52
+ connection = less_than_ideal_stack.pop
53
+ if(connection)
54
+ registry_lookup(connection)[:ready] = 1
55
+ connection
56
+ end
57
+ end
58
+
59
+ # connection:: Connection
60
+ # args:: optional args (:force)
61
+ # Provides customized RDY set when less than ideal to round
62
+ # robin through connections
63
+ def set_ready_for(connection, *args)
64
+ if(less_than_ideal?)
65
+ if(args.include?(:force))
66
+ super connection
67
+ else
68
+ debug "RDY set ignored due to less than ideal state (con: #{connection})"
69
+ con = less_than_ideal_ready!
70
+ if(con)
71
+ watch_dog.reset if watch_dog
72
+ super con
73
+ else
74
+ warn 'Failed to set RDY state while less than ideal. Connection stack is empty!'
75
+ end
76
+ end
77
+ else
78
+ super connection
79
+ end
80
+ end
81
+
82
+ # connection:: Connection
83
+ # Update connection ready count
84
+ def calculate_ready!(connection)
85
+ registry_info = registry_lookup(connection)
86
+ unless(less_than_ideal?)
87
+ registry_info[:ready] = ideal - registry_info[:in_flight]
88
+ registry_info[:ready] = 0 if registry_info[:ready] < 0
89
+ registry_info[:ready]
90
+ else
91
+ registry_info[:ready] = 0
92
+ end
93
+ end
94
+
95
+ # Returns all connections without RDY state
96
+ def waiting_connections
97
+ registry.find_all do |connection, info|
98
+ info[:ready] < 1 && info[:in_flight] < 1
99
+ end.map(&:first).compact
100
+ end
101
+
102
+ # Returns all connections with RDY state
103
+ def rdy_connections
104
+ registry.find_all do |connection, info|
105
+ info[:ready] > 0
106
+ end.map(&:first).compact
107
+ end
108
+
109
+ # Force a connection to give up RDY state so next in stack can receive
110
+ def force_unready
111
+ debug 'Forcing a connection into an unready state due to less than ideal state'
112
+ connection = rdy_connections.shuffle.first
113
+ if(connection)
114
+ debug "Stripping RDY state from connection: #{connection}"
115
+ calculate_ready!(connection)
116
+ set_ready_for(connection)
117
+ else
118
+ warn "Failed to locate available connection for RDY aquisition!"
119
+ end
120
+ end
121
+
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,134 @@
1
+ module Krakow
2
+ class Distribution
3
+
4
+ autoload :Default, 'krakow/distribution/default'
5
+ # autoload :ProducerWeighted, 'krakow/distribution/producer_weighted'
6
+ # autoload :ConsumerWeighted, 'krakow/distribution/consumer_weighted'
7
+
8
+ include Celluloid
9
+ include Utils::Lazy
10
+
11
+ attr_accessor :max_in_flight, :ideal, :flight_record, :registry
12
+
13
+ def initialize(args={})
14
+ super
15
+ @max_in_flight = arguments[:max_in_flight] || 1
16
+ @ideal = 0
17
+ @flight_record = {}
18
+ @registry = {}
19
+ end
20
+
21
+ # Reset flight distributions
22
+ def redistribute!
23
+ raise NoMethodError.new 'Custom `#redistrubute!` method must be provided!'
24
+ end
25
+
26
+ # connection:: Connection
27
+ # Determine RDY value for given connection
28
+ def calculate_ready!(connection)
29
+ raise NoMethodError.new 'Custom `#calculate_ready!` method must be provided!'
30
+ end
31
+
32
+ # message:: FrameType::Message or message ID string
33
+ # Remove message metadata from registry. Should be used after
34
+ # confirmations or requeue.
35
+ def unregister_message(message)
36
+ msg_id = message.respond_to?(:message_id) ? message.message_id : message.to_s
37
+ connection = flight_record[msg_id]
38
+ # TODO: Add lookup error
39
+ registry_info = registry[connection_key(connection)]
40
+ flight_record.delete(msg_id)
41
+ registry_info[:in_flight] -= 1
42
+ calculate_ready!(connection)
43
+ connection
44
+ end
45
+
46
+ # connection:: Connection
47
+ # Return the currently configured RDY value for given connnection
48
+ def ready_for(connection)
49
+ registry_lookup(connection)[:ready]
50
+ end
51
+
52
+ # connection:: Connection
53
+ # Send RDY for given connection
54
+ def set_ready_for(connection)
55
+ connection.transmit(
56
+ Command::Rdy.new(
57
+ :count => ready_for(connection)
58
+ )
59
+ )
60
+ end
61
+
62
+ # Initial ready value used for new connections
63
+ def initial_ready
64
+ ideal > 0 ? 1 : 0
65
+ end
66
+
67
+ # message:: FrameType::Message
68
+ # connection:: Connection
69
+ # Registers message into registry and configures for distribution
70
+ def register_message(message, connection)
71
+ registry_info = registry_lookup(connection)
72
+ registry_info[:in_flight] += 1
73
+ flight_record[message.message_id] = connection_key(connection)
74
+ calculate_ready!(connection)
75
+ end
76
+
77
+ # connection:: Connection
78
+ # Add connection to make available for RDY distribution
79
+ def add_connection(connection)
80
+ registry[connection_key(connection)] = {
81
+ :ready => initial_ready,
82
+ :in_flight => 0
83
+ }
84
+ true
85
+ end
86
+
87
+ # connection:: Connection
88
+ # Remove connection from RDY distribution
89
+ def remove_connection(connection)
90
+ # remove connection from registry
91
+ registry.delete(connection_key(connection))
92
+ # remove any in flight messages
93
+ in_flight.delete_if do |k,v|
94
+ v == connection_key(connection)
95
+ end
96
+ true
97
+ end
98
+
99
+ # connection:: Connection
100
+ # Return lookup key (actor reference)
101
+ def connection_key(connection)
102
+ connection.current_actor
103
+ end
104
+
105
+ # msg_id:: Message ID string
106
+ # Return source connection of given `msg_id`. If block is
107
+ # provided, the connection instance will be yielded to the block
108
+ # and the result returned.
109
+ def in_flight_lookup(msg_id)
110
+ connection = flight_record[msg_id]
111
+ unless(connection)
112
+ abort LookupFailed.new("Failed to locate in flight message (ID: #{msg_id})")
113
+ end
114
+ if(block_given?)
115
+ yield connection
116
+ else
117
+ connection
118
+ end
119
+ end
120
+
121
+ # connection:: Connection
122
+ # Return registry information for given connection
123
+ def registry_lookup(connection)
124
+ registry[connection_key(connection)] ||
125
+ abort(LookupFailed.new("Failed to locate connection information in registry (#{connection})"))
126
+ end
127
+
128
+ # Return list of all connections in registry
129
+ def connections
130
+ registry.keys
131
+ end
132
+
133
+ end
134
+ end
@@ -1,6 +1,10 @@
1
1
  module Krakow
2
2
  class Error < StandardError
3
3
 
4
+ class LookupFailed < Error; end
5
+ class ConnectionFailure < Error; end
6
+ class ConfigurationError < Error; end
7
+
4
8
  class BadResponse < Error
5
9
  attr_accessor :result
6
10
  end
@@ -7,6 +7,14 @@ module Krakow
7
7
  required! :error
8
8
  end
9
9
 
10
+ def error
11
+ arguments[:error]
12
+ end
13
+
14
+ def content
15
+ error
16
+ end
17
+
10
18
  end
11
19
  end
12
20
  end
@@ -7,6 +7,10 @@ module Krakow
7
7
  required! :attempts, :timestamp, :message_id, :message
8
8
  end
9
9
 
10
+ def content
11
+ message
12
+ end
13
+
10
14
  end
11
15
  end
12
16
  end
@@ -7,6 +7,10 @@ module Krakow
7
7
  required! :response
8
8
  end
9
9
 
10
+ def content
11
+ response
12
+ end
13
+
10
14
  end
11
15
  end
12
16
  end
@@ -46,5 +46,9 @@ module Krakow
46
46
  super
47
47
  end
48
48
 
49
+ def content
50
+ raise NoMethodError.new 'Content method not properly defined!'
51
+ end
52
+
49
53
  end
50
54
  end
@@ -4,6 +4,7 @@ module Krakow
4
4
  include Utils::Lazy
5
5
  include Celluloid
6
6
 
7
+ trap_exit :connection_failure
7
8
  finalizer :goodbye_my_love!
8
9
 
9
10
  attr_reader :connection
@@ -11,43 +12,96 @@ module Krakow
11
12
  def initialize(args={})
12
13
  super
13
14
  required! :host, :port, :topic
14
- @connection = Connection.new(:host => host, :port => port)
15
- connection.init!
15
+ optional :connect_retries
16
+ arguments[:reconnect_retries] ||= 10
17
+ arguments[:reconnect_interval] = 5
18
+ connect
19
+ end
20
+
21
+ # Establish connection to configured `host` and `port`
22
+ def connect
23
+ info "Establishing connection to: #{host}:#{port}"
24
+ begin
25
+ @connection = Connection.new(:host => host, :port => port)
26
+ self.link connection
27
+ connection.init!
28
+ info "Connection established: #{connection}"
29
+ rescue => e
30
+ abort e
31
+ end
32
+ end
33
+
34
+ def to_s
35
+ "<#{self.class.name}:#{object_id} {#{host}:#{port}} T:#{topic}>"
36
+ end
37
+
38
+ # Return if connected
39
+ def connected?
40
+ connection && connection.alive?
41
+ end
42
+
43
+ # Process connection failure and attempt reconnection
44
+ def connection_failure(*args)
45
+ warn "Connection has failed to #{host}:#{port}"
46
+ retries = 0
47
+ begin
48
+ connect
49
+ rescue => e
50
+ retries += 1
51
+ warn "Connection retry #{retries}/#{reconnect_retries} failed. #{e.class}: #{e}"
52
+ if(retries < reconnect_retries)
53
+ sleep_interval = retries * reconnect_interval
54
+ debug "Sleeping for reconnect interval of #{sleep_interval} seconds"
55
+ sleep sleep_interval
56
+ retry
57
+ else
58
+ abort e
59
+ end
60
+ end
16
61
  end
17
62
 
18
63
  def goodbye_my_love!
19
- if(connection)
64
+ debug 'Tearing down producer'
65
+ if(connection && connection.alive?)
20
66
  connection.terminate
21
67
  end
22
68
  @connection = nil
69
+ info 'Producer torn down'
23
70
  end
24
71
 
25
72
  # message:: Message to send
26
73
  # Write message
27
74
  def write(*message)
28
- if(message.size > 1)
29
- connection.transmit(
30
- Command::Mpub.new(
31
- :topic_name => topic,
32
- :messages => message
75
+ if(connection.alive?)
76
+ if(message.size > 1)
77
+ debug 'Multiple message publish'
78
+ connection.transmit(
79
+ Command::Mpub.new(
80
+ :topic_name => topic,
81
+ :messages => message
82
+ )
33
83
  )
34
- )
35
- else
36
- connection.transmit(
37
- Command::Pub.new(
38
- :message => message.first,
39
- :topic_name => topic
84
+ else
85
+ debug 'Single message publish'
86
+ connection.transmit(
87
+ Command::Pub.new(
88
+ :message => message.first,
89
+ :topic_name => topic
90
+ )
40
91
  )
41
- )
92
+ end
93
+ read(:validate)
94
+ else
95
+ abort Error.new 'Remote connection is unavailable!'
42
96
  end
43
- read(:validate)
44
97
  end
45
98
 
46
99
  # args:: Options (:validate)
47
100
  # Read response from connection. If :validate is included an
48
101
  # exception will be raised if `FrameType::Error` is received
49
102
  def read(*args)
50
- result = connection.queue.pop
103
+ result = connection.responses.pop
104
+ debug "Read response: #{result}"
51
105
  if(args.include?(:validate) && result.is_a?(FrameType::Error))
52
106
  error = Error::BadResponse.new('Write failed')
53
107
  error.result = result
@@ -1,6 +1,9 @@
1
1
  module Krakow
2
2
  module Utils
3
3
  module Lazy
4
+
5
+ include Utils::Logging
6
+
4
7
  attr_reader :arguments
5
8
 
6
9
  def initialize(args={})
@@ -33,6 +36,14 @@ module Krakow
33
36
  end
34
37
  end
35
38
 
39
+ def to_s
40
+ "<#{self.class.name}:#{object_id}>"
41
+ end
42
+
43
+ def inspect
44
+ "<#{self.class.name}:#{object_id} [#{arguments.inspect}]>"
45
+ end
46
+
36
47
  def method_missing(*args)
37
48
  key = args.first.to_sym
38
49
  if(arguments.has_key?(key))
@@ -0,0 +1,32 @@
1
+ module Krakow
2
+ module Utils
3
+ module Logging
4
+
5
+ # Define base logging types
6
+ %w(debug info warn error).each do |key|
7
+ define_method(key) do |string|
8
+ log(key, string)
9
+ end
10
+ end
11
+
12
+ # Log message
13
+ def log(*args)
14
+ if(args.empty?)
15
+ Celluloid::Logger
16
+ else
17
+ severity, string = args
18
+ Celluloid::Logger.send(severity.to_sym, "#{self}: #{string}")
19
+ end
20
+ end
21
+
22
+ class << self
23
+ def level=(level)
24
+ if(Celluloid.logger.class == Logger)
25
+ Celluloid.logger.level = Logger.const_get(level.to_s.upcase.to_sym)
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
data/lib/krakow/utils.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  module Krakow
2
2
  module Utils
3
3
  autoload :Lazy, 'krakow/utils/lazy'
4
+ autoload :Logging, 'krakow/utils/logging'
4
5
  end
5
6
  end
@@ -3,5 +3,5 @@ require 'krakow'
3
3
  module Krakow
4
4
  class Version < Gem::Version
5
5
  end
6
- VERSION = Version.new('0.0.1')
6
+ VERSION = Version.new('0.1.0')
7
7
  end
data/lib/krakow.rb CHANGED
@@ -6,6 +6,7 @@ module Krakow
6
6
  autoload :Connection, 'krakow/connection'
7
7
  autoload :Consumer, 'krakow/consumer'
8
8
  autoload :Discovery, 'krakow/discovery'
9
+ autoload :Distribution, 'krakow/distribution'
9
10
  autoload :Error, 'krakow/exceptions'
10
11
  autoload :FrameType, 'krakow/frame_type'
11
12
  autoload :Producer, 'krakow/producer'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: krakow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-01-24 00:00:00.000000000 Z
12
+ date: 2014-01-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: celluloid-io
@@ -70,8 +70,11 @@ files:
70
70
  - lib/krakow/frame_type.rb
71
71
  - lib/krakow/command.rb
72
72
  - lib/krakow/version.rb
73
+ - lib/krakow/distribution.rb
73
74
  - lib/krakow/utils/lazy.rb
75
+ - lib/krakow/utils/logging.rb
74
76
  - lib/krakow/producer.rb
77
+ - lib/krakow/distribution/default.rb
75
78
  - lib/krakow/frame_type/response.rb
76
79
  - lib/krakow/frame_type/error.rb
77
80
  - lib/krakow/frame_type/message.rb