juggernaut 0.5.7 → 0.5.8
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/Manifest.txt +1 -2
- data/README.txt +4 -20
- data/Rakefile +0 -3
- data/bin/juggernaut +0 -0
- data/lib/juggernaut.rb +36 -31
- data/lib/juggernaut/client.rb +258 -152
- data/lib/juggernaut/runner.rb +8 -8
- data/lib/juggernaut/server.rb +34 -38
- data/test/test_client.rb +176 -0
- data/test/test_juggernaut.rb +34 -0
- data/test/test_message.rb +26 -0
- data/test/test_runner.rb +26 -0
- data/test/test_server.rb +573 -0
- data/test/test_utils.rb +26 -0
- metadata +18 -11
- data/History.txt +0 -5
data/lib/juggernaut/runner.rb
CHANGED
@@ -7,12 +7,12 @@ module Juggernaut
|
|
7
7
|
include Juggernaut::Miscel
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def run
|
11
|
-
self.new
|
10
|
+
def run(argv = ARGV)
|
11
|
+
self.new(argv)
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def initialize
|
15
|
+
def initialize(argv = ARGV)
|
16
16
|
self.options = {
|
17
17
|
:host => "0.0.0.0",
|
18
18
|
:port => 5001,
|
@@ -28,7 +28,7 @@ module Juggernaut
|
|
28
28
|
:config_path => config_path
|
29
29
|
})
|
30
30
|
|
31
|
-
parse_options
|
31
|
+
parse_options(argv)
|
32
32
|
|
33
33
|
if !File.exists?(config_path)
|
34
34
|
puts "You must generate a config file (juggernaut -g filename.yml)"
|
@@ -52,7 +52,7 @@ module Juggernaut
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def start
|
55
|
-
puts "Starting Juggernaut server on port: #{options[:port]}..."
|
55
|
+
puts "Starting Juggernaut server #{Juggernaut::VERSION} on port: #{options[:port]}..."
|
56
56
|
|
57
57
|
trap("INT") {
|
58
58
|
stop
|
@@ -70,7 +70,7 @@ module Juggernaut
|
|
70
70
|
end
|
71
71
|
|
72
72
|
EventMachine::run {
|
73
|
-
EventMachine::add_periodic_timer( options[:cleanup_timer]
|
73
|
+
EventMachine::add_periodic_timer( options[:cleanup_timer] || 2 ) { Juggernaut::Client.send_logouts_after_timeout }
|
74
74
|
EventMachine::start_server(options[:host], options[:port].to_i, Juggernaut::Server)
|
75
75
|
EM.set_effective_user( options[:user] ) if options[:user]
|
76
76
|
}
|
@@ -82,7 +82,7 @@ module Juggernaut
|
|
82
82
|
EventMachine::stop
|
83
83
|
end
|
84
84
|
|
85
|
-
def parse_options
|
85
|
+
def parse_options(argv)
|
86
86
|
OptionParser.new do |opts|
|
87
87
|
opts.summary_width = 25
|
88
88
|
opts.banner = "Juggernaut (#{VERSION})\n\n",
|
@@ -162,7 +162,7 @@ module Juggernaut
|
|
162
162
|
puts "Juggernaut #{VERSION}"
|
163
163
|
exit
|
164
164
|
end
|
165
|
-
end.parse!
|
165
|
+
end.parse!(argv)
|
166
166
|
options
|
167
167
|
end
|
168
168
|
|
data/lib/juggernaut/server.rb
CHANGED
@@ -52,13 +52,14 @@ module Juggernaut
|
|
52
52
|
attr_reader :logout_timeout
|
53
53
|
attr_reader :status
|
54
54
|
attr_reader :channels
|
55
|
+
attr_reader :client
|
55
56
|
|
56
57
|
# EM methods
|
57
58
|
|
58
59
|
def post_init
|
59
60
|
logger.debug "New client [#{client_ip}]"
|
61
|
+
@client = nil
|
60
62
|
@channels = []
|
61
|
-
@messages = []
|
62
63
|
@current_msg_id = 0
|
63
64
|
@connected = true
|
64
65
|
@logout_timeout = nil
|
@@ -69,7 +70,6 @@ module Juggernaut
|
|
69
70
|
# so we need to buffer the data until we find the
|
70
71
|
# terminating "\0"
|
71
72
|
def receive_data(data)
|
72
|
-
logger.debug "Receiving data: #{data}"
|
73
73
|
@buffer << data
|
74
74
|
@buffer = process_whole_messages(@buffer)
|
75
75
|
end
|
@@ -120,9 +120,14 @@ module Juggernaut
|
|
120
120
|
end
|
121
121
|
|
122
122
|
case @request[:command].to_sym
|
123
|
-
when :broadcast
|
124
|
-
|
125
|
-
when :
|
123
|
+
when :broadcast
|
124
|
+
broadcast_command
|
125
|
+
when :subscribe
|
126
|
+
subscribe_command
|
127
|
+
when :query
|
128
|
+
query_command
|
129
|
+
when :noop
|
130
|
+
noop_command
|
126
131
|
else
|
127
132
|
raise InvalidCommand, @request
|
128
133
|
end
|
@@ -136,16 +141,18 @@ module Juggernaut
|
|
136
141
|
end
|
137
142
|
|
138
143
|
def unbind
|
139
|
-
|
140
|
-
|
144
|
+
if @client
|
145
|
+
# todo - should be called after timeout?
|
146
|
+
@client.logout_connection_request(@channels)
|
147
|
+
logger.debug "Lost client #{@client.friendly_id}"
|
148
|
+
end
|
141
149
|
mark_dead('Unbind called')
|
142
150
|
end
|
143
151
|
|
144
152
|
# As far as I'm aware, send_data
|
145
153
|
# never throws an exception
|
146
154
|
def publish(msg)
|
147
|
-
logger.debug "Sending msg: #{msg.to_s}"
|
148
|
-
logger.debug "To client: #{@client.id}" if @client
|
155
|
+
logger.debug "Sending msg: #{msg.to_s} to client #{@request[:client_id]} (session #{@request[:session_id]})"
|
149
156
|
send_data(msg.to_s + CR)
|
150
157
|
end
|
151
158
|
|
@@ -153,7 +160,6 @@ module Juggernaut
|
|
153
160
|
|
154
161
|
def broadcast(bdy)
|
155
162
|
msg = Juggernaut::Message.new(@current_msg_id += 1, bdy, self.signature)
|
156
|
-
@messages << msg if options[:store_messages]
|
157
163
|
publish(msg)
|
158
164
|
end
|
159
165
|
|
@@ -162,9 +168,7 @@ module Juggernaut
|
|
162
168
|
# attempt would hook onto a new em instance. A client
|
163
169
|
# usually dies through an unbind
|
164
170
|
@connected = false
|
165
|
-
@
|
166
|
-
@status = "DEAD: %s: Could potentially logout at %s" %
|
167
|
-
[ reason, @logout_timeout ]
|
171
|
+
@client.remove_connection(self) if @client
|
168
172
|
end
|
169
173
|
|
170
174
|
def alive?
|
@@ -203,25 +207,6 @@ module Juggernaut
|
|
203
207
|
end
|
204
208
|
end
|
205
209
|
|
206
|
-
def broadcast_all_messages_from(msg_id, signature_id)
|
207
|
-
return unless msg_id or signature_id
|
208
|
-
client = Juggernaut::Client.find_by_signature(signature)
|
209
|
-
return if !client
|
210
|
-
msg_id = Integer(msg_id)
|
211
|
-
return if msg_id >= client.current_msg_id
|
212
|
-
client.messages.select {|msg|
|
213
|
-
(msg_id..client.current_msg_id).include?(msg.id)
|
214
|
-
}.each {|msg| publish(msg) }
|
215
|
-
end
|
216
|
-
|
217
|
-
# todo - how should this be called - if at all?
|
218
|
-
def clean_up_old_messages(how_many_to_keep = 1000)
|
219
|
-
while @messages.length > how_many_to_keep
|
220
|
-
# We need to shift, as we want to remove the oldest first
|
221
|
-
@messages.shift
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
210
|
protected
|
226
211
|
|
227
212
|
# Commands
|
@@ -262,6 +247,13 @@ module Juggernaut
|
|
262
247
|
client = Juggernaut::Client.find_by_id(client_id)
|
263
248
|
client.remove_channels!(@request[:channels]) if client
|
264
249
|
end
|
250
|
+
when :show_channels_for_client
|
251
|
+
query_needs :client_id
|
252
|
+
if client = Juggernaut::Client.find_by_id(@request[:client_id])
|
253
|
+
publish client.channels.to_json
|
254
|
+
else
|
255
|
+
publish nil.to_json
|
256
|
+
end
|
265
257
|
when :show_clients
|
266
258
|
if @request[:client_ids] and @request[:client_ids].any?
|
267
259
|
clients = @request[:client_ids].collect{ |client_id| Client.find_by_id(client_id) }.compact.uniq
|
@@ -280,7 +272,13 @@ module Juggernaut
|
|
280
272
|
end
|
281
273
|
end
|
282
274
|
|
283
|
-
def
|
275
|
+
def noop_command
|
276
|
+
logger.debug "NOOP"
|
277
|
+
end
|
278
|
+
|
279
|
+
def subscribe_command
|
280
|
+
logger.debug "SUBSCRIBE: #{@request.inspect}"
|
281
|
+
|
284
282
|
if channels = @request[:channels]
|
285
283
|
add_channels(channels)
|
286
284
|
end
|
@@ -291,10 +289,8 @@ module Juggernaut
|
|
291
289
|
raise UnauthorisedSubscription, @client
|
292
290
|
end
|
293
291
|
|
294
|
-
Juggernaut::Client.add_client(@client)
|
295
|
-
|
296
292
|
if options[:store_messages]
|
297
|
-
|
293
|
+
@client.send_queued_messages(self)
|
298
294
|
end
|
299
295
|
end
|
300
296
|
|
@@ -366,7 +362,7 @@ module Juggernaut
|
|
366
362
|
end
|
367
363
|
|
368
364
|
def client_ip
|
369
|
-
Socket.unpack_sockaddr_in(get_peername)[1]
|
365
|
+
Socket.unpack_sockaddr_in(get_peername)[1] rescue nil
|
370
366
|
end
|
371
367
|
end
|
372
|
-
end
|
368
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
|
2
|
+
$:.unshift "../lib"
|
3
|
+
require "juggernaut"
|
4
|
+
require "test/unit"
|
5
|
+
require "shoulda"
|
6
|
+
require "mocha"
|
7
|
+
|
8
|
+
class TestClient < Test::Unit::TestCase
|
9
|
+
|
10
|
+
CONFIG = File.join(File.dirname(__FILE__), "files", "juggernaut.yml")
|
11
|
+
|
12
|
+
class DummySubscriber; end
|
13
|
+
|
14
|
+
EXAMPLE_URL = "http://localhost:5000/callbacks/example"
|
15
|
+
SECURE_URL = "https://localhost:5000/callbacks/example"
|
16
|
+
|
17
|
+
context "Client" do
|
18
|
+
|
19
|
+
setup do
|
20
|
+
Juggernaut.options = {
|
21
|
+
:logout_connection_url => "http://localhost:5000/callbacks/logout_connection",
|
22
|
+
:logout_url => "http://localhost:5000/callbacks/logout",
|
23
|
+
:subscription_url => "http://localhost:5000/callbacks/subscription"
|
24
|
+
}
|
25
|
+
@s1 = DummySubscriber.new
|
26
|
+
@request = {
|
27
|
+
:client_id => "jonny",
|
28
|
+
:session_id => rand(1_000_000).to_s(16)
|
29
|
+
}
|
30
|
+
@client = Juggernaut::Client.find_or_create(@s1, @request)
|
31
|
+
end
|
32
|
+
|
33
|
+
teardown do
|
34
|
+
Juggernaut::Client.reset!
|
35
|
+
end
|
36
|
+
|
37
|
+
should "have correct JSON representation" do
|
38
|
+
assert_nothing_raised do
|
39
|
+
json = {
|
40
|
+
"client_id" => "jonny",
|
41
|
+
"num_connections" => 1,
|
42
|
+
"session_id" => @request[:session_id]
|
43
|
+
}
|
44
|
+
assert_equal json, JSON.parse(@client.to_json)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
should "return the client based on subscriber's signature" do
|
49
|
+
@s1.stubs(:signature).returns("012345")
|
50
|
+
assert_equal @client, Juggernaut::Client.find_by_signature("012345")
|
51
|
+
end
|
52
|
+
|
53
|
+
should "return the client based on client ID and channel list" do
|
54
|
+
@client.stubs(:has_channels?).with(%w(a couple of channels)).returns(true)
|
55
|
+
assert_equal @client, Juggernaut::Client.find_by_id_and_channels("jonny", %w(a couple of channels))
|
56
|
+
assert_nil Juggernaut::Client.find_by_id_and_channels("peter", %w(a couple of channels))
|
57
|
+
@client.stubs(:has_channels?).with(%w(something else)).returns(false)
|
58
|
+
assert_nil Juggernaut::Client.find_by_id_and_channels("jonny", %w(something else))
|
59
|
+
end
|
60
|
+
|
61
|
+
should "automatically be registered, and can unregister" do
|
62
|
+
assert @client.send(:registered?)
|
63
|
+
assert_equal @client, @client.send(:unregister)
|
64
|
+
assert_equal false, @client.send(:registered?)
|
65
|
+
end
|
66
|
+
|
67
|
+
should "be alive if at least one subscriber is alive" do
|
68
|
+
@s1.stubs(:alive?).returns(true)
|
69
|
+
@s2 = DummySubscriber.new
|
70
|
+
@client.add_new_connection(@s2)
|
71
|
+
@s2.stubs(:alive?).returns(false)
|
72
|
+
assert @client.alive?
|
73
|
+
end
|
74
|
+
|
75
|
+
should "not be alive if no subscriber is alive" do
|
76
|
+
@s1.stubs(:alive?).returns(false)
|
77
|
+
@s2 = DummySubscriber.new
|
78
|
+
@client.add_new_connection(@s2)
|
79
|
+
@s2.stubs(:alive?).returns(false)
|
80
|
+
assert_equal false, @client.alive?
|
81
|
+
end
|
82
|
+
|
83
|
+
should "not give up if within the timeout period" do
|
84
|
+
Juggernaut.options[:timeout] = 10
|
85
|
+
@s1.stubs(:alive?).returns(false)
|
86
|
+
@client.send(:reset_logout_timeout!)
|
87
|
+
assert_equal false, @client.give_up?
|
88
|
+
end
|
89
|
+
|
90
|
+
should "not give up if at least one subscriber is alive" do
|
91
|
+
Juggernaut.options[:timeout] = 0
|
92
|
+
@s1.stubs(:alive?).returns(true)
|
93
|
+
@client.send(:reset_logout_timeout!)
|
94
|
+
assert_equal false, @client.give_up?
|
95
|
+
end
|
96
|
+
|
97
|
+
should "send logouts after timeout" do
|
98
|
+
Juggernaut.options[:timeout] = 0
|
99
|
+
@s1.stubs(:alive?).returns(false)
|
100
|
+
@client.send(:reset_logout_timeout!)
|
101
|
+
@client.expects(:logout_request).once
|
102
|
+
Juggernaut::Client.send_logouts_after_timeout
|
103
|
+
end
|
104
|
+
|
105
|
+
%w(subscription logout_connection).each do |type|
|
106
|
+
|
107
|
+
context "#{type} request" do
|
108
|
+
|
109
|
+
should "post to the correct URL" do
|
110
|
+
@client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(true)
|
111
|
+
assert_equal true, @client.send("#{type}_request", %w(master))
|
112
|
+
end
|
113
|
+
|
114
|
+
should "not raise exceptions if posting raises an exception" do
|
115
|
+
@client.expects(:post_request).with(Juggernaut.options[:"#{type}_url"], %w(master), :timeout => 5).returns(false)
|
116
|
+
assert_nothing_raised {
|
117
|
+
assert_equal false, @client.send("#{type}_request", %w(master))
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
context "post to URL" do
|
126
|
+
|
127
|
+
should "return true when successful" do
|
128
|
+
Net::HTTP.any_instance.
|
129
|
+
expects(:post).
|
130
|
+
with("/callbacks/example", "client_id=jonny&session_id=#{@request[:session_id]}&channels[]=master&channels[]=slave", {"User-Agent" => "Ruby/#{RUBY_VERSION}"}).
|
131
|
+
returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
|
132
|
+
assert_equal true, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
|
133
|
+
end
|
134
|
+
|
135
|
+
should "return false on an internal server error" do
|
136
|
+
Net::HTTP.any_instance.expects(:post).returns([Net::HTTPInternalServerError.new('1.1', '500', 'Internal Server Error'), ''])
|
137
|
+
assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
|
138
|
+
end
|
139
|
+
|
140
|
+
should "return false when a runtime error is caught" do
|
141
|
+
Net::HTTP.any_instance.expects(:post).raises(RuntimeError)
|
142
|
+
assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
|
143
|
+
end
|
144
|
+
|
145
|
+
should "return false when callback times out" do
|
146
|
+
Net::HTTP.any_instance.expects(:post).raises(Timeout::Error)
|
147
|
+
assert_equal false, @client.send(:post_request, EXAMPLE_URL, %w(master slave))
|
148
|
+
end
|
149
|
+
|
150
|
+
context "using a secure URL" do
|
151
|
+
|
152
|
+
should "return true when successful" do
|
153
|
+
Net::HTTP.any_instance.expects(:post).returns([Net::HTTPOK.new('1.1', '200', 'OK'), ''])
|
154
|
+
Net::HTTP.any_instance.expects(:use_ssl=).with(true).returns(true)
|
155
|
+
assert_equal true, @client.send(:post_request, SECURE_URL, %w(master slave))
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
context "channel list" do
|
163
|
+
|
164
|
+
should "be the unique list of all channels in the subscribers" do
|
165
|
+
@s1.stubs(:channels).returns(%w(master slave1))
|
166
|
+
@s2 = DummySubscriber.new
|
167
|
+
@s2.stubs(:channels).returns(%w(master slave2))
|
168
|
+
@client.add_new_connection(@s2)
|
169
|
+
assert_same_elements %w(master slave1 slave2), @client.channels
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
$:.unshift "../lib"
|
3
|
+
require "juggernaut"
|
4
|
+
require "test/unit"
|
5
|
+
require "shoulda"
|
6
|
+
require "mocha"
|
7
|
+
|
8
|
+
class TestJuggernaut < Test::Unit::TestCase
|
9
|
+
|
10
|
+
context "Juggernaut" do
|
11
|
+
|
12
|
+
setup do
|
13
|
+
Juggernaut.options = { }
|
14
|
+
end
|
15
|
+
|
16
|
+
should "set options correctly" do
|
17
|
+
options = {
|
18
|
+
:host => "0.0.0.0",
|
19
|
+
:port => 5001,
|
20
|
+
:debug => false
|
21
|
+
}
|
22
|
+
Juggernaut.options = options
|
23
|
+
assert_equal options, Juggernaut.options
|
24
|
+
end
|
25
|
+
|
26
|
+
should "have a debug logger by default" do
|
27
|
+
log = Juggernaut.logger
|
28
|
+
assert_not_nil log
|
29
|
+
assert_equal Logger::DEBUG, log.level
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
$:.unshift "../lib"
|
3
|
+
require "juggernaut"
|
4
|
+
require "test/unit"
|
5
|
+
require "shoulda"
|
6
|
+
require "mocha"
|
7
|
+
|
8
|
+
class TestMessage < Test::Unit::TestCase
|
9
|
+
|
10
|
+
context "Message" do
|
11
|
+
|
12
|
+
setup do
|
13
|
+
@msg = Juggernaut::Message.new(1, "A pre-determined message body", "a81fef13919")
|
14
|
+
assert_not_nil @msg
|
15
|
+
end
|
16
|
+
|
17
|
+
should "generate valid JSON" do
|
18
|
+
obj = {"signature" => "a81fef13919", "body" => "A pre-determined message body", "id" => "1"}
|
19
|
+
assert_nothing_raised do
|
20
|
+
assert_equal obj, JSON.parse(@msg.to_s)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|