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.
@@ -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].to_i ) { Juggernaut::Client.send_logouts_after_timeout }
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
 
@@ -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: broadcast_command
124
- when :subscribe: subscribe_command
125
- when :query: query_command
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
- @client.logout_connection_request(@channels) if @client # todo - should be called after timeout?
140
- logger.debug "Lost client: #{@client.id}" if @client
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
- @logout_timeout = Time::now + (options[:timeout] || 30)
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 subscribe_command
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
- broadcast_all_messages_from(@request[:last_msg_id], @request[:signature])
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
@@ -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