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 +8 -1
- data/Gemfile.lock +1 -1
- data/README.md +67 -4
- data/lib/krakow/command/cls.rb +10 -0
- data/lib/krakow/command/fin.rb +6 -0
- data/lib/krakow/command/identify.rb +18 -1
- data/lib/krakow/command/mpub.rb +10 -0
- data/lib/krakow/command/pub.rb +10 -0
- data/lib/krakow/command/rdy.rb +6 -0
- data/lib/krakow/command/req.rb +6 -0
- data/lib/krakow/command/sub.rb +10 -0
- data/lib/krakow/command/touch.rb +6 -0
- data/lib/krakow/command.rb +24 -0
- data/lib/krakow/connection.rb +43 -6
- data/lib/krakow/consumer.rb +89 -26
- data/lib/krakow/discovery.rb +6 -1
- data/lib/krakow/distribution/default.rb +124 -0
- data/lib/krakow/distribution.rb +134 -0
- data/lib/krakow/exceptions.rb +4 -0
- data/lib/krakow/frame_type/error.rb +8 -0
- data/lib/krakow/frame_type/message.rb +4 -0
- data/lib/krakow/frame_type/response.rb +4 -0
- data/lib/krakow/frame_type.rb +4 -0
- data/lib/krakow/producer.rb +71 -17
- data/lib/krakow/utils/lazy.rb +11 -0
- data/lib/krakow/utils/logging.rb +32 -0
- data/lib/krakow/utils.rb +1 -0
- data/lib/krakow/version.rb +1 -1
- data/lib/krakow.rb +1 -0
- metadata +5 -2
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -12,7 +12,7 @@ producer = Krakow::Producer(
|
|
12
12
|
:port => 'PORT',
|
13
13
|
:topic => 'target'
|
14
14
|
)
|
15
|
-
producer.write('
|
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
|
-
|
30
|
-
|
31
|
-
consumer.
|
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
|
data/lib/krakow/command/cls.rb
CHANGED
data/lib/krakow/command/fin.rb
CHANGED
@@ -15,10 +15,27 @@ module Krakow
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def to_line
|
18
|
-
|
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
|
data/lib/krakow/command/mpub.rb
CHANGED
@@ -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
|
data/lib/krakow/command/pub.rb
CHANGED
@@ -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
|
data/lib/krakow/command/rdy.rb
CHANGED
data/lib/krakow/command/req.rb
CHANGED
data/lib/krakow/command/sub.rb
CHANGED
data/lib/krakow/command/touch.rb
CHANGED
data/lib/krakow/command.rb
CHANGED
@@ -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(
|
data/lib/krakow/connection.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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]
|
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
|
data/lib/krakow/consumer.rb
CHANGED
@@ -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, :
|
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
|
-
|
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
|
-
|
31
|
+
info "Registered new connection #{connection}"
|
32
|
+
distribution.redistribute!
|
25
33
|
else
|
26
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
end
|
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
|
data/lib/krakow/discovery.rb
CHANGED
@@ -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
|
data/lib/krakow/exceptions.rb
CHANGED
data/lib/krakow/frame_type.rb
CHANGED
data/lib/krakow/producer.rb
CHANGED
@@ -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
|
-
|
15
|
-
|
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
|
-
|
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(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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.
|
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
|
data/lib/krakow/utils/lazy.rb
CHANGED
@@ -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
data/lib/krakow/version.rb
CHANGED
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
|
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-
|
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
|