krakow 0.0.1 → 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.
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