right_agent 1.0.1 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. metadata +71 -12
@@ -0,0 +1,594 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
25
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'right_agent', 'clients', 'router_client'))
26
+
27
+ describe RightScale::RouterClient do
28
+
29
+ include FlexMock::ArgumentTypes
30
+
31
+ before(:each) do
32
+ @log = flexmock(RightScale::Log)
33
+ @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
34
+ @log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
35
+ @timer = flexmock("timer", :cancel => true, :interval= => 0).by_default
36
+ flexmock(EM::PeriodicTimer).should_receive(:new).and_return(@timer).by_default
37
+ @http_client = flexmock("http client", :get => true, :check_health => true).by_default
38
+ flexmock(RightScale::BalancedHttpClient).should_receive(:new).and_return(@http_client).by_default
39
+ @websocket = WebSocketClientMock.new
40
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket).by_default
41
+ @auth_header = {"Authorization" => "Bearer <session>"}
42
+ @url = "http://test.com"
43
+ @ws_url = "ws://test.com"
44
+ @auth_client = AuthClientMock.new(@url, @auth_header, :authorized)
45
+ @routing_keys = nil
46
+ @options = {}
47
+ @client = RightScale::RouterClient.new(@auth_client, @options)
48
+ @version = RightScale::AgentConfig.protocol_version
49
+ @event = {:uuid => "uuid", :type => "Push", :path => "/foo/bar", :from => "rs-agent-1-1", :data => {}, :version => @version}
50
+ end
51
+
52
+ context :initialize do
53
+ it "initializes options" do
54
+ @options = {
55
+ :open_timeout => 1,
56
+ :request_timeout => 2,
57
+ :listen_timeout => 3,
58
+ :retry_timeout => 4,
59
+ :retry_intervals => [1, 2, 3],
60
+ :reconnect_interval => 5 }
61
+ @client = RightScale::RouterClient.new(@auth_client, @options)
62
+ options = @client.instance_variable_get(:@options)
63
+ options[:server_name] = "RightNet"
64
+ options[:api_version] = "2.0"
65
+ options[:open_timeout] = 1
66
+ options[:request_timeout] = 2
67
+ options[:listen_timeout] = 3
68
+ options[:retry_timeout] = 4
69
+ options[:retry_intervals] = [1, 2, 3]
70
+ options[:reconnect_interval] = 5
71
+ end
72
+
73
+ it "initializes options to defaults if no value specified" do
74
+ options = @client.instance_variable_get(:@options)
75
+ options[:listen_timeout].should == 60
76
+ end
77
+ end
78
+
79
+ context "requests" do
80
+
81
+ before(:each) do
82
+ @type = "/foo/bar"
83
+ @action = "bar"
84
+ @payload = {:some => "data"}
85
+ @target = "rs-agent-2-2"
86
+ @token = "random token"
87
+ @params = {
88
+ :type => @type,
89
+ :payload => @payload,
90
+ :target => @target }
91
+ end
92
+
93
+ context :push do
94
+ it "makes post request to router" do
95
+ flexmock(@client).should_receive(:make_request).with(:post, "/push", @params, @action, @token).
96
+ and_return(nil).once
97
+ @client.push(@type, @payload, @target, @token).should be_nil
98
+ end
99
+
100
+ it "does not require token" do
101
+ flexmock(@client).should_receive(:make_request).with(:post, "/push", @params, @action, nil).
102
+ and_return(nil).once
103
+ @client.push(@type, @payload, @target).should be_nil
104
+ end
105
+ end
106
+
107
+ context :request do
108
+ it "makes post request to router" do
109
+ flexmock(@client).should_receive(:make_request).with(:post, "/request", @params, @action, @token).
110
+ and_return(nil).once
111
+ @client.request(@type, @payload, @target, @token)
112
+ end
113
+
114
+ it "does not require token" do
115
+ flexmock(@client).should_receive(:make_request).with(:post, "/request", @params, @action, nil).
116
+ and_return(nil).once
117
+ @client.request(@type, @payload, @target).should be_nil
118
+ end
119
+ end
120
+ end
121
+
122
+ context "events" do
123
+
124
+ before(:each) do
125
+ @later = Time.at(@now = Time.now)
126
+ @tick = 30
127
+ flexmock(Time).should_receive(:now).and_return { @later += @tick }
128
+ end
129
+
130
+ context :notify do
131
+ before(:each) do
132
+ @routing_keys = ["key"]
133
+ @params = {
134
+ :event => @event,
135
+ :routing_keys => @routing_keys }
136
+ end
137
+
138
+ it "sends using websocket if available" do
139
+ @client.send(:connect, @routing_keys) { |_| }
140
+ @client.notify(@event, @routing_keys).should be_true
141
+ @websocket.sent.should == JSON.dump(@params)
142
+ end
143
+
144
+ it "makes post request by default" do
145
+ flexmock(@client).should_receive(:make_request).with(:post, "/notify", @params, "notify", "uuid",
146
+ {:filter_params => ["event"]}).once
147
+ @client.notify(@event, @routing_keys).should be_true
148
+ end
149
+ end
150
+
151
+ context :listen do
152
+ it "raises if block missing" do
153
+ lambda { @client.listen(@routing_keys) }.should raise_error(ArgumentError, "Block missing")
154
+ end
155
+
156
+ it "loops forever until closed" do
157
+ @client.close
158
+ @client.listen(@routing_keys) { |_| }.should be_true
159
+ end
160
+
161
+ it "loops forever until closing" do
162
+ @client.close(:receive)
163
+ @client.listen(@routing_keys) { |_| }.should be_true
164
+ end
165
+
166
+ it "sleeps if websocket already exists" do
167
+ @client.send(:connect, @routing_keys) { |_| }
168
+ flexmock(@client).should_receive(:sleep).with(5).and_return { @client.close }.once
169
+ @client.listen(@routing_keys) { |_| }
170
+ @client.instance_variable_get(:@connect_interval).should == 30
171
+ end
172
+
173
+ it "tries to create websocket if time to" do
174
+ @client.instance_variable_get(:@websocket).should be_nil
175
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket).once
176
+ flexmock(@client).should_receive(:sleep).and_return { @client.close }
177
+ @client.listen(@routing_keys) { |_| }
178
+ end
179
+
180
+ it "does not try long-polling if websocket connect attempt indicates not to" do
181
+ @client.instance_variable_get(:@websocket).should be_nil
182
+ flexmock(@client).should_receive(:try_connect).and_return { @client.close; true }
183
+ flexmock(@client).should_receive(:try_long_poll).never
184
+ @client.listen(@routing_keys) { |_| }
185
+ end
186
+
187
+ it "tries long-polling if could not create websocket" do
188
+ @client.instance_variable_get(:@websocket).should be_nil
189
+ flexmock(@client).should_receive(:retry_connect?).and_return(false)
190
+ flexmock(@client).should_receive(:long_poll).and_return { @client.close; ["uuid"] }.once
191
+ @client.listen(@routing_keys) { |_| }.should be_true
192
+ end
193
+ end
194
+
195
+ context :close do
196
+ it "closes websocket" do
197
+ @client.send(:connect, nil) { |_| }
198
+ @client.close
199
+ @websocket.closed.should be_true
200
+ @websocket.code.should == 1001
201
+ end
202
+ end
203
+
204
+ context :retry_connect? do
205
+ before(:each) do
206
+ @client.instance_variable_set(:@last_connect_time, @now)
207
+ @client.instance_variable_set(:@connect_interval, 30)
208
+ end
209
+
210
+ it "requires websocket to be enabled" do
211
+ @client = RightScale::RouterClient.new(@auth_client, :long_polling_only => true)
212
+ @client.send(:retry_connect?).should be_false
213
+ end
214
+
215
+ it "requires there be no existing websocket connection" do
216
+ @client.instance_variable_set(:@websocket, @websocket)
217
+ @client.send(:retry_connect?).should be_false
218
+ end
219
+
220
+ context "when no existing websocket" do
221
+ before(:each) do
222
+ @client.instance_variable_get(:@websocket).should be_nil
223
+ end
224
+
225
+ it "allows retry if enough time has elapsed" do
226
+ @tick = 1
227
+ @client.instance_variable_set(:@last_connect_time, @now - 29)
228
+ @client.send(:retry_connect?).should be_false
229
+ @client.instance_variable_set(:@last_connect_time, @now - 30)
230
+ @client.send(:retry_connect?).should be_true
231
+ end
232
+
233
+ [RightScale::RouterClient::NORMAL_CLOSE, RightScale::RouterClient::SHUTDOWN_CLOSE].each do |code|
234
+ it "allows retry if previous close code is #{code}" do
235
+ @client.instance_variable_set(:@close_code, code)
236
+ @client.instance_variable_set(:@connect_interval, 300)
237
+ @client.send(:retry_connect?).should be_true
238
+ end
239
+ end
240
+
241
+ [502, 503].each do |code|
242
+ it "allows retry if previous close has reason with code #{code} indicating router inaccessible" do
243
+ @client.instance_variable_set(:@close_code, RightScale::RouterClient::PROTOCOL_ERROR_CLOSE)
244
+ @client.instance_variable_set(:@close_reason, "Unexpected response code: #{code}")
245
+ @client.instance_variable_set(:@connect_interval, 300)
246
+ @client.send(:retry_connect?).should be_true
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ context :try_connect do
253
+ before(:each) do
254
+ @handler = lambda { |_| }
255
+ @client.instance_variable_get(:@websocket).should be_nil
256
+ @client.instance_variable_set(:@connect_interval, 30)
257
+ @client.instance_variable_set(:@reconnect_interval, 2)
258
+ end
259
+
260
+ it "makes websocket connect request" do
261
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket).once
262
+ flexmock(@client).should_receive(:sleep).and_return { @client.close }
263
+ @client.send(:try_connect, @routing_keys, &@handler)
264
+ end
265
+
266
+ it "periodically checks whether websocket creation was really successful" do
267
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket)
268
+ flexmock(@client).should_receive(:sleep).with(1).times(3).ordered
269
+ flexmock(@client).should_receive(:sleep).and_return { @client.instance_variable_set(:@websocket, nil) }.once.ordered
270
+ @client.send(:try_connect, @routing_keys, &@handler)
271
+ end
272
+
273
+ it "sleeps if websocket creation failed because router not responding" do
274
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket)
275
+ flexmock(@client).should_receive(:sleep).with(1).and_return do
276
+ @client.instance_variable_set(:@websocket, nil)
277
+ @client.instance_variable_set(:@close_code, RightScale::RouterClient::PROTOCOL_ERROR_CLOSE)
278
+ @client.instance_variable_set(:@close_reason, "Unexpected response code: 502")
279
+ end.once
280
+ flexmock(@client).should_receive(:sleep).with(4).and_return { @client.close }.once
281
+ @client.send(:try_connect, @routing_keys, &@handler)
282
+ @client.instance_variable_get(:@reconnect_interval).should == 4
283
+ @client.instance_variable_get(:@connect_interval).should == 30
284
+ end
285
+
286
+ it "adjusts connect interval if websocket creation was unsuccessful" do
287
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket)
288
+ flexmock(@client).should_receive(:sleep).and_return { @client.instance_variable_set(:@websocket, nil); @client.close }.once
289
+ @client.send(:try_connect, @routing_keys, &@handler)
290
+ @client.instance_variable_get(:@connect_interval).should == 60
291
+ @client.instance_variable_get(:@reconnect_interval).should == 2
292
+ end
293
+
294
+ it "loops instead of long-polling if websocket creation was unsuccessful" do
295
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket)
296
+ flexmock(@client).should_receive(:sleep).and_return { @client.instance_variable_set(:@websocket, nil); @client.close }
297
+ flexmock(@client).should_receive(:long_poll).never
298
+ @client.send(:try_connect, @routing_keys, &@handler)
299
+ end
300
+
301
+ it "sleeps after successfully creating websocket" do
302
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket)
303
+ flexmock(@client).should_receive(:sleep).with(1).times(4).ordered
304
+ flexmock(@client).should_receive(:sleep).with(1).and_return { @client.close }.once.ordered
305
+ @client.send(:try_connect, @routing_keys, &@handler)
306
+ end
307
+
308
+ it "adjusts connect interval if websocket creation fails" do
309
+ @log.should_receive(:error).with("Failed creating WebSocket", StandardError).once
310
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_raise(StandardError).once
311
+ flexmock(@client).should_receive(:sleep).never
312
+ @client.send(:try_connect, @routing_keys, &@handler)
313
+ @client.instance_variable_get(:@connect_interval).should == 60
314
+ end
315
+ end
316
+
317
+ context :connect do
318
+ it "raises if block missing" do
319
+ lambda { @client.send(:connect, @routing_keys) }.should raise_error(ArgumentError, "Block missing")
320
+ end
321
+
322
+ context "when creating connection" do
323
+ it "connects to router" do
324
+ flexmock(Faye::WebSocket::Client).should_receive(:new).with(@ws_url + "/connect", nil, Hash).and_return(@websocket).once
325
+ @client.send(:connect, @routing_keys) { |_| }
326
+ end
327
+
328
+ it "chooses scheme based on scheme in router URL" do
329
+ @url = "https://test.com"
330
+ @ws_url = "wss://test.com"
331
+ @auth_client = AuthClientMock.new(@url, @auth_header, :authorized)
332
+ @client = RightScale::RouterClient.new(@auth_client, @options)
333
+ flexmock(Faye::WebSocket::Client).should_receive(:new).with(@ws_url + "/connect", nil, Hash).and_return(@websocket).once
334
+ @client.send(:connect, @routing_keys) { |_| }
335
+ end
336
+
337
+ it "uses headers containing only API version and authorization" do
338
+ headers = @auth_header.merge("X-API-Version" => "2.0")
339
+ flexmock(Faye::WebSocket::Client).should_receive(:new).with(String, nil, hsh(:headers => headers)).and_return(@websocket).once
340
+ @client.send(:connect, @routing_keys) { |_| }
341
+ end
342
+
343
+ it "enables ping" do
344
+ flexmock(Faye::WebSocket::Client).should_receive(:new).with(String, nil, hsh(:ping => 60)).and_return(@websocket).once
345
+ @client.send(:connect, @routing_keys) { |_| }
346
+ end
347
+
348
+ it "adds routing keys as query parameters" do
349
+ url = @ws_url + "/connect" + "?routing_keys[]=a%3Ab%3Dc"
350
+ flexmock(Faye::WebSocket::Client).should_receive(:new).with(url, nil, Hash).and_return(@websocket).once
351
+ @client.send(:connect, ["a:b=c"]) { |_| }
352
+ end
353
+
354
+ it "returns websocket" do
355
+ flexmock(Faye::WebSocket::Client).should_receive(:new).and_return(@websocket).once
356
+ @client.send(:connect, @routing_keys) { |_| }.should == @websocket
357
+ @client.instance_variable_get(:@websocket).should == @websocket
358
+ end
359
+ end
360
+
361
+ context "when message received" do
362
+ before(:each) do
363
+ @json_event = JSON.dump(@event)
364
+ @json_ack = JSON.dump({:ack => "uuid"})
365
+ end
366
+
367
+ it "presents JSON-decoded event to the specified handler" do
368
+ event = nil
369
+ @client.send(:connect, @routing_keys) { |e| event = e }
370
+ @websocket.onmessage(@json_event)
371
+ event.should == @event
372
+ end
373
+
374
+ it "logs event" do
375
+ @log.should_receive(:info).with("Creating WebSocket connection to ws://test.com/connect").once.ordered
376
+ @log.should_receive(:info).with("Received EVENT <uuid> Push /foo/bar from rs-agent-1-1").once.ordered
377
+ @log.should_receive(:info).with("Sending EVENT <uuid> Push /foo/bar to rs-agent-1-1").once.ordered
378
+ event = nil
379
+ @client.send(:connect, @routing_keys) { |e| event = e }
380
+ @websocket.onmessage(@json_event)
381
+ event.should == @event
382
+ end
383
+
384
+ it "acknowledges event" do
385
+ @client.send(:connect, @routing_keys) { |_| nil }
386
+ @websocket.onmessage(@json_event)
387
+ @websocket.sent.should == @json_ack
388
+ end
389
+
390
+ it "sends event response using websocket" do
391
+ result = {:uuid => "uuid2", :type => "Result", :from => "rs-agent-2-2", :data => {}, :version => @version}
392
+ @client.send(:connect, @routing_keys) { |_| result }
393
+ @websocket.onmessage(@json_event)
394
+ @websocket.sent.should == [@json_ack, JSON.dump({:event => result, :routing_keys => ["rs-agent-1-1"]})]
395
+ end
396
+
397
+ it "only sends non-nil responses" do
398
+ @client.send(:connect, @routing_keys) { |_| nil }
399
+ @websocket.onmessage(@json_event)
400
+ @websocket.sent.should == @json_ack
401
+ end
402
+
403
+ it "logs failures" do
404
+ @log.should_receive(:error).with("Failed handling WebSocket event", StandardError, :trace).once
405
+ request = RightScale::Request.new("/foo/bar", "payload")
406
+ @client.send(:connect, @routing_keys) { |_| raise StandardError, "bad event" }
407
+ @websocket.onmessage(JSON.dump(request))
408
+ end
409
+ end
410
+
411
+ context "on close" do
412
+ it "logs info" do
413
+ @log.should_receive(:info).with("Creating WebSocket connection to ws://test.com/connect").once.ordered
414
+ @log.should_receive(:info).with("WebSocket closed (1000)").once.ordered
415
+ @client.send(:connect, @routing_keys) { |_| }
416
+ @websocket.onclose(1000)
417
+ @client.instance_variable_get(:@websocket).should be_nil
418
+ end
419
+
420
+ it "logged info includes reason if available" do
421
+ @log.should_receive(:info).with("Creating WebSocket connection to ws://test.com/connect").once.ordered
422
+ @log.should_receive(:info).with("WebSocket closed (1001: Going Away)").once.ordered
423
+ @client.send(:connect, @routing_keys) { |_| }
424
+ @websocket.onclose(1001, "Going Away")
425
+ end
426
+
427
+ it "logs unexpected exceptions" do
428
+ @log.should_receive(:info).with("Creating WebSocket connection to ws://test.com/connect").once.ordered
429
+ @log.should_receive(:info).and_raise(RuntimeError).once.ordered
430
+ @log.should_receive(:error).with("Failed closing WebSocket", RuntimeError, :trace).once
431
+ @client.send(:connect, @routing_keys) { |_| }
432
+ @websocket.onclose(1000)
433
+ end
434
+ end
435
+
436
+ context "on error" do
437
+ it "logs error" do
438
+ @log.should_receive(:error).with("WebSocket error (Protocol Error)")
439
+ @client.send(:connect, @routing_keys) { |_| }
440
+ @websocket.onerror("Protocol Error")
441
+ end
442
+
443
+ it "does not log if there is no error data" do
444
+ @log.should_receive(:error).never
445
+ @client.send(:connect, @routing_keys) { |_| }
446
+ @websocket.onerror(nil)
447
+ end
448
+ end
449
+ end
450
+
451
+ context :try_long_poll do
452
+ before(:each) do
453
+ @uuids = ["uuid"]
454
+ @handler = lambda { |_| }
455
+ @client.instance_variable_set(:@connect_interval, 30)
456
+ @client.instance_variable_set(:@reconnect_interval, 2)
457
+ end
458
+
459
+ it "makes long-polling request" do
460
+ flexmock(@client).should_receive(:long_poll).with(@routing_keys, @uuids, @handler).and_return([]).once
461
+ @client.send(:try_long_poll, @routing_keys, @uuids, &@handler).should == []
462
+ end
463
+
464
+ it "returns UUIDs of events received" do
465
+ flexmock(@client).should_receive(:long_poll).with(@routing_keys, [], @handler).and_return { @uuids }.once
466
+ @client.send(:try_long_poll, @routing_keys, [], &@handler).should == @uuids
467
+ end
468
+
469
+ it "sleeps if there is a long-polling failure" do
470
+ @log.should_receive(:error).with("Failed long-polling", StandardError, :trace).once
471
+ flexmock(@client).should_receive(:long_poll).and_raise(StandardError).once
472
+ flexmock(@client).should_receive(:sleep).with(4).once
473
+ @client.send(:try_long_poll, @routing_keys, @uuids, &@handler).should be_nil
474
+ end
475
+
476
+ [RightScale::Exceptions::Unauthorized,
477
+ RightScale::Exceptions::ConnectivityFailure,
478
+ RightScale::Exceptions::RetryableError].each do |e|
479
+ it "does not trace #{e} exceptions" do
480
+ @log.should_receive(:error).with("Failed long-polling", e, :no_trace).once
481
+ flexmock(@client).should_receive(:long_poll).and_raise(e, "failed").once
482
+ flexmock(@client).should_receive(:sleep).with(4).once
483
+ @client.send(:try_long_poll, @routing_keys, @uuids, &@handler).should be_nil
484
+ end
485
+ end
486
+ end
487
+
488
+ context :long_poll do
489
+ before(:each) do
490
+ @ack = []
491
+ end
492
+
493
+ it "raises if block missing" do
494
+ lambda { @client.send(:long_poll, @routing_keys, @ack) }.should raise_error(ArgumentError, "Block missing")
495
+ end
496
+
497
+ it "makes listen request to router" do
498
+ flexmock(@client).should_receive(:make_request).with(:get, "/listen",
499
+ on { |a| a[:wait_time].should == 55 && !a.key?(:routing_keys) &&
500
+ a[:timestamp] == @later.to_f }, "listen", nil, Hash).and_return([@event]).once
501
+ @client.send(:long_poll, @routing_keys, @ack) { |_| }
502
+ end
503
+
504
+ it "uses listen timeout for request" do
505
+ flexmock(@client).should_receive(:make_request).with(:get, "/listen", Hash, "listen", nil,
506
+ {:request_timeout => 60, :log_level => :debug}).and_return([@event]).once
507
+ @client.send(:long_poll, @routing_keys, @ack) { |_| }
508
+ end
509
+
510
+ it "logs event" do
511
+ @log.should_receive(:info).with("Received EVENT <uuid> Push /foo/bar from rs-agent-1-1").once
512
+ flexmock(@client).should_receive(:make_request).and_return([@event])
513
+ @client.send(:long_poll, @routing_keys, @ack) { |_| }
514
+ end
515
+
516
+ it "presents event to handler" do
517
+ flexmock(@client).should_receive(:make_request).and_return([@event])
518
+ event = nil
519
+ @client.send(:long_poll, @routing_keys, @ack) { |e| event = e }
520
+ event.should == @event
521
+ end
522
+
523
+ it "handles event keys that are strings" do
524
+ event = {"uuid" => "uuid", "type" => "Push", "path" => "/foo/bar", "from" => "rs-agent-1-1", "data" => {}, "version" => @version}
525
+ @log.should_receive(:info).with("Received EVENT <uuid> Push /foo/bar from rs-agent-1-1").once
526
+ flexmock(@client).should_receive(:make_request).and_return([event])
527
+ event = nil
528
+ @client.send(:long_poll, @routing_keys, @ack) { |e| event = e }
529
+ event.should == @event
530
+ end
531
+
532
+ it "does nothing if no events are returned" do
533
+ flexmock(@client).should_receive(:make_request).and_return(nil)
534
+ event = nil
535
+ @client.send(:long_poll, @routing_keys, @ack) { |e| event = e }
536
+ event.should be_nil
537
+ end
538
+
539
+ it "returns event UUIDs" do
540
+ flexmock(@client).should_receive(:make_request).and_return([@event])
541
+ @client.send(:long_poll, @routing_keys, @ack) { |_| }.should == ["uuid"]
542
+ end
543
+
544
+ it "returns nil if no events are received" do
545
+ flexmock(@client).should_receive(:make_request).and_return(nil)
546
+ @client.send(:long_poll, @routing_keys, @ack) { |_| }.should be_nil
547
+ end
548
+ end
549
+
550
+ context :backoff_connect_interval do
551
+ it "backs off exponentially" do
552
+ @client.instance_variable_set(:@connect_interval, 30)
553
+ @client.send(:backoff_connect_interval).should == 60
554
+ @client.send(:backoff_connect_interval).should == 120
555
+ end
556
+
557
+ it "limits backoff" do
558
+ @client.instance_variable_set(:@connect_interval, 30)
559
+ 12.times { @client.send(:backoff_connect_interval) }
560
+ @client.instance_variable_get(:@connect_interval).should == RightScale::RouterClient::MAX_CONNECT_INTERVAL
561
+ end
562
+ end
563
+
564
+ context :backoff_reconnect_interval do
565
+ it "backs off exponentially" do
566
+ @client.instance_variable_set(:@reconnect_interval, 2)
567
+ @client.send(:backoff_reconnect_interval).should == 4
568
+ @client.send(:backoff_reconnect_interval).should == 8
569
+ end
570
+
571
+ it "limits backoff" do
572
+ @client.instance_variable_set(:@reconnect_interval, 2)
573
+ 6.times { @client.send(:backoff_reconnect_interval) }
574
+ @client.instance_variable_get(:@reconnect_interval).should == RightScale::RouterClient::MAX_RECONNECT_INTERVAL
575
+ end
576
+ end
577
+
578
+ context :router_not_responding? do
579
+ [502, 503].each do |code|
580
+ it "declares not responding if have close reason code #{code} indicating router inaccessible" do
581
+ @client.instance_variable_set(:@close_code, RightScale::RouterClient::PROTOCOL_ERROR_CLOSE)
582
+ @client.instance_variable_set(:@close_reason, "Unexpected response code: #{code}")
583
+ @client.send(:router_not_responding?).should be_true
584
+ end
585
+ end
586
+
587
+ it "does not declare not responding for other close codes" do
588
+ @client.instance_variable_set(:@close_code, RightScale::RouterClient::UNEXPECTED_ERROR_CLOSE)
589
+ @client.instance_variable_set(:@close_reason, "Unexpected response code: 502")
590
+ @client.send(:router_not_responding?).should be_false
591
+ end
592
+ end
593
+ end
594
+ end