right_agent 2.0.8 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -48,32 +48,16 @@ module RightScale
48
48
  class Terminating < RuntimeError; end
49
49
 
50
50
  # Not authorized to make request
51
- class Unauthorized < NestedException
52
- def initialize(message, nested_exception = nil)
53
- super(message, nested_exception)
54
- end
55
- end
51
+ class Unauthorized < NestedException; end
56
52
 
57
53
  # Cannot connect to service, lost connection to it, or it is out of service or too busy to respond
58
- class ConnectivityFailure < NestedException
59
- def initialize(message, nested_exception = nil)
60
- super(message, nested_exception)
61
- end
62
- end
54
+ class ConnectivityFailure < NestedException; end
63
55
 
64
56
  # Request failed but potentially will succeed if retried
65
- class RetryableError < NestedException
66
- def initialize(message, nested_exception = nil)
67
- super(message, nested_exception)
68
- end
69
- end
57
+ class RetryableError < NestedException; end
70
58
 
71
59
  # Database query failed
72
- class QueryFailure < NestedException
73
- def initialize(message, nested_exception = nil)
74
- super(message, nested_exception)
75
- end
76
- end
60
+ class QueryFailure < NestedException; end
77
61
 
78
62
  # Error internal to specified server
79
63
  class InternalServerError < NestedException
@@ -0,0 +1,87 @@
1
+ require 'restclient'
2
+
3
+ module RightScale
4
+
5
+ # This code is largely borrowed from RestClient
6
+
7
+ # Container for response so that access response headers in same fashion as RestClient
8
+ class Response
9
+ attr_reader :headers
10
+
11
+ def initialize(headers)
12
+ @headers = headers
13
+ end
14
+ end
15
+
16
+ # Base HTTP exception class
17
+ class HttpException < RuntimeError
18
+ attr_writer :message
19
+ attr_reader :http_code, :http_body
20
+ attr_accessor :response
21
+
22
+ def initialize(code, body, response = nil)
23
+ @http_code = code
24
+ @http_body = body
25
+ @response = response
26
+ end
27
+
28
+ def inspect
29
+ "#{message}: #{http_body}"
30
+ end
31
+
32
+ def to_s
33
+ inspect
34
+ end
35
+
36
+ def message
37
+ @message || self.class.name
38
+ end
39
+
40
+ end
41
+
42
+ # Exceptions created for each status code defined in RestClient::STATUSES
43
+ # e.g., RightScale::HttpExceptions::ResourceNotFound for status code 404
44
+ module HttpExceptions
45
+
46
+ # Exception when request failed with an error code that is not managed here
47
+ class RequestFailed < HttpException
48
+
49
+ def to_s
50
+ message
51
+ end
52
+
53
+ def message
54
+ "HTTP status code #{http_code}"
55
+ end
56
+ end
57
+
58
+ # Map HTTP status codes to the corresponding exception class
59
+ HTTP_EXCEPTIONS_MAP = {}
60
+
61
+ # Create exception for given code
62
+ def self.create(code, body = "", headers = {})
63
+ if HttpExceptions::HTTP_EXCEPTIONS_MAP[code]
64
+ HttpExceptions::HTTP_EXCEPTIONS_MAP[code].new(code, body, RightScale::Response.new(headers))
65
+ else
66
+ RequestFailed.new(code, body, RightScale::Response.new(headers))
67
+ end
68
+ end
69
+
70
+ # Convert RestClient exception
71
+ def self.convert(e)
72
+ e2 = create(e.http_code, e.http_body, RightScale::Response.new((e.response && e.response.headers) || {}))
73
+ e2.message = e.message
74
+ e2
75
+ end
76
+
77
+ RestClient::STATUSES.each do |code, message|
78
+ klass = Class.new(HttpException) do
79
+ send(:define_method, :message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
80
+ end
81
+ klass_constant = const_set(message.delete(' \-\''), klass)
82
+ HTTP_EXCEPTIONS_MAP[code] = klass_constant
83
+ end
84
+
85
+ end # HttpExceptions
86
+
87
+ end # RightScale
@@ -44,3 +44,4 @@ require ::File.normalize_path('command', RIGHT_AGENT_BASE_DIR)
44
44
  require ::File.normalize_path('log', RIGHT_AGENT_BASE_DIR)
45
45
  require ::File.normalize_path('pid_file', RIGHT_AGENT_BASE_DIR)
46
46
  require ::File.normalize_path('serialize/serializable', RIGHT_AGENT_BASE_DIR)
47
+ require ::File.normalize_path('eventmachine_spawn', RIGHT_AGENT_BASE_DIR)
@@ -88,7 +88,7 @@ module RightScale
88
88
  def initialize(operation, payload, options = {})
89
89
  raise ArgumentError.new("operation is required") unless (@operation = operation)
90
90
  raise ArgumentError.new("payload is required") unless (@payload = payload)
91
- @retry_on_error = options[:retry_on_error] || false
91
+ @retry_on_error = options[:retry_on_error]
92
92
  @timeout = options[:timeout] || DEFAULT_TIMEOUT
93
93
  @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
94
94
  @retry_delay_count = options[:retry_delay_count] || DEFAULT_RETRY_DELAY_COUNT
@@ -287,7 +287,7 @@ module RightScale
287
287
  def dispatch(action, agent_name)
288
288
  # Setup the environment from config if necessary
289
289
  begin
290
- eval("#{action}_agent(agent_name)")
290
+ send("#{action}_agent", agent_name)
291
291
  rescue SystemExit
292
292
  true
293
293
  rescue SignalException
@@ -332,6 +332,12 @@ module RightScale
332
332
  def start_agent(agent_name, agent_class = Agent)
333
333
  puts "#{human_readable_name} being started"
334
334
 
335
+ if @options[:fiber_pool_size]
336
+ require 'fiber_pool'
337
+ @options[:fiber_pool] = FiberPool.new(@options[:fiber_pool_size])
338
+ EM_S.fiber_pool = @options[:fiber_pool]
339
+ end
340
+
335
341
  EM.error_handler do |e|
336
342
  Log.error("EM block execution failed with exception", e, :trace)
337
343
  Log.error("\n\n===== Exiting due to EM block exception =====\n\n")
@@ -339,7 +345,7 @@ module RightScale
339
345
  exit(1)
340
346
  end
341
347
 
342
- EM.run do
348
+ EM_S.run do
343
349
  begin
344
350
  @@agent = agent_class.start(@options)
345
351
  rescue SystemExit
@@ -364,9 +370,9 @@ module RightScale
364
370
  # (Boolean):: true if process was stopped, otherwise false
365
371
  def stop_agent(agent_name)
366
372
  res = false
367
- if pid_file = AgentConfig.pid_file(agent_name)
373
+ if (pid_file = AgentConfig.pid_file(agent_name))
368
374
  name = human_readable_name(agent_name, pid_file.identity)
369
- if pid = pid_file.read_pid[:pid]
375
+ if (pid = pid_file.read_pid[:pid])
370
376
  begin
371
377
  Process.kill('TERM', pid)
372
378
  res = true
@@ -405,7 +411,7 @@ module RightScale
405
411
  else
406
412
  puts "#{name} is not running but has a stale pid file at #{pid_file}"
407
413
  end
408
- elsif identity = AgentConfig.agent_options(agent_name)[:identity]
414
+ elsif (identity = AgentConfig.agent_options(agent_name)[:identity])
409
415
  puts "#{human_readable_name(agent_name, identity)} is not running"
410
416
  end
411
417
  res
@@ -469,7 +475,7 @@ module RightScale
469
475
  def configure_agent(action, options)
470
476
  agent_type = options[:agent_type]
471
477
  agent_name = options[:agent_name]
472
- if agent_name != agent_type && cfg = AgentConfig.load_cfg(agent_type)
478
+ if agent_name != agent_type && (cfg = AgentConfig.load_cfg(agent_type))
473
479
  base_id = (options[:base_id] || AgentIdentity.parse(cfg[:identity]).base_id.to_s).to_i
474
480
  unless (identity = AgentConfig.agent_options(agent_name)[:identity]) &&
475
481
  AgentIdentity.parse(identity).base_id == base_id
@@ -44,6 +44,7 @@
44
44
  # --reconnect-interval SEC Set number of seconds between HTTP or AMQP reconnect attempts
45
45
  # --grace-timeout SEC Set number of seconds before graceful termination times out
46
46
  # --[no-]dup-check Set whether to check for and reject duplicate requests, .e.g., due to retries
47
+ # --fiber-pool-size, -f N Set size of fiber pool
47
48
  # --options, -o KEY=VAL Set options that act as final override for any persisted configuration settings
48
49
  # --monit Generate monit configuration file
49
50
  # --test Build test deployment using default test settings
@@ -176,6 +177,10 @@ module RightScale
176
177
  options[:dup_check] = b
177
178
  end
178
179
 
180
+ opts.on('-f', '--fiber-pool-size N') do |n|
181
+ options[:fiber_pool_size] = n.to_i
182
+ end
183
+
179
184
  opts.on('--prefetch COUNT') do |count|
180
185
  options[:prefetch] = count.to_i
181
186
  end
@@ -312,6 +317,7 @@ module RightScale
312
317
  cfg[:dup_check] = options[:dup_check].nil? ? true : options[:dup_check]
313
318
  cfg[:http_proxy] = options[:http_proxy] if options[:http_proxy]
314
319
  cfg[:http_no_proxy] = options[:http_no_proxy] if options[:http_no_proxy]
320
+ cfg[:fiber_pool_size] = options[:fiber_pool_size] if options[:fiber_pool_size]
315
321
  cfg
316
322
  end
317
323
 
@@ -571,7 +571,9 @@ module RightScale
571
571
  true
572
572
  end
573
573
 
574
- # Send request via HTTP and then immediately handle response
574
+ # Send request via HTTP
575
+ # Use next_tick for asynchronous response and to ensure
576
+ # that the request is sent using the main EM reactor thread
575
577
  #
576
578
  # === Parameters
577
579
  # kind(Symbol):: Kind of request: :send_push or :send_request
@@ -583,6 +585,33 @@ module RightScale
583
585
  # === Return
584
586
  # true:: Always return true
585
587
  def http_send(kind, target, packet, received_at, callback)
588
+ if @options[:async_response]
589
+ EM_S.next_tick do
590
+ begin
591
+ http_send_once(kind, target, packet, received_at, callback)
592
+ rescue Exception => e
593
+ Log.error("Failed sending or handling response for #{packet.trace} #{packet.type}", e, :trace)
594
+ @exception_stats.track("request", e)
595
+ end
596
+ end
597
+ else
598
+ http_send_once(kind, target, packet, received_at, callback)
599
+ end
600
+ true
601
+ end
602
+
603
+ # Send request via HTTP and then immediately handle response
604
+ #
605
+ # === Parameters
606
+ # kind(Symbol):: Kind of request: :send_push or :send_request
607
+ # target(Hash|String|nil):: Target for request
608
+ # packet(Push|Request):: Request packet to send
609
+ # received_at(Time):: Time when request received
610
+ # callback(Proc|nil):: Block used to process response
611
+ #
612
+ # === Return
613
+ # true:: Always return true
614
+ def http_send_once(kind, target, packet, received_at, callback)
586
615
  begin
587
616
  method = packet.class.name.split("::").last.downcase
588
617
  result = success_result(@agent.client.send(method, packet.type, packet.payload, target, packet.token))
@@ -619,11 +648,7 @@ module RightScale
619
648
  result = Result.new(packet.token, @identity, result, from = packet.target)
620
649
  result.received_at = received_at.to_f
621
650
  @pending_requests[packet.token] = PendingRequest.new(kind, received_at, callback) if callback
622
- if @options[:async_response]
623
- EM.next_tick { handle_response(result) }
624
- else
625
- handle_response(result)
626
- end
651
+ handle_response(result)
627
652
  end
628
653
  true
629
654
  end
@@ -25,8 +25,8 @@ require 'rbconfig'
25
25
 
26
26
  Gem::Specification.new do |spec|
27
27
  spec.name = 'right_agent'
28
- spec.version = '2.0.8'
29
- spec.date = '2014-03-03'
28
+ spec.version = '2.1.0'
29
+ spec.date = '2014-04-01'
30
30
  spec.authors = ['Lee Kirchhoff', 'Raphael Simon', 'Tony Spataro', 'Scott Messier']
31
31
  spec.email = 'lee@rightscale.com'
32
32
  spec.homepage = 'https://github.com/rightscale/right_agent'
@@ -42,7 +42,6 @@ describe RightScale::Agent do
42
42
  flexmock(EM).should_receive(:next_tick).and_yield
43
43
  flexmock(EM).should_receive(:add_timer).and_yield
44
44
  @timer = flexmock("timer")
45
- flexmock(EM::Timer).should_receive(:new).and_return(@timer)
46
45
  flexmock(EM::PeriodicTimer).should_receive(:new).and_return(@timer)
47
46
  @timer.should_receive(:cancel)
48
47
  @broker = flexmock("broker", :subscribe => ["b1"], :publish => ["b1"], :prefetch => true,
@@ -249,11 +248,9 @@ describe RightScale::Agent do
249
248
  @log = flexmock(RightScale::Log)
250
249
  @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
251
250
  flexmock(EM).should_receive(:next_tick).and_yield
252
- flexmock(EM).should_receive(:add_timer).and_yield
253
- @timer = flexmock("timer")
254
- flexmock(EM::Timer).should_receive(:new).and_return(@timer).by_default
255
- @timer.should_receive(:cancel).by_default
256
- @periodic_timer = flexmock("timer")
251
+ flexmock(EM).should_receive(:add_timer).with(0, Proc).and_yield.by_default
252
+ flexmock(EM).should_receive(:add_timer).with(1, Proc).and_yield.by_default
253
+ @periodic_timer = flexmock("periodic timer")
257
254
  flexmock(EM::PeriodicTimer).should_receive(:new).and_return(@periodic_timer)
258
255
  @periodic_timer.should_receive(:cancel).by_default
259
256
  @broker_id = "rs-broker-123-1"
@@ -521,6 +518,12 @@ describe RightScale::Agent do
521
518
 
522
519
  describe "Terminating" do
523
520
 
521
+ before(:each) do
522
+ @timer = flexmock("timer")
523
+ flexmock(EM::Timer).should_receive(:new).and_return(@timer).by_default
524
+ @timer.should_receive(:cancel).by_default
525
+ end
526
+
524
527
  it "should log error for abnormal termination" do
525
528
  run_in_em do
526
529
  @agent = RightScale::Agent.new(:user => "me", :identity => @identity)
@@ -32,29 +32,37 @@ describe RightScale::BalancedHttpClient do
32
32
  @log = flexmock(RightScale::Log)
33
33
  @log.should_receive(:error).by_default.and_return { |m| raise RightScale::Log.format(*m) }
34
34
  @log.should_receive(:warning).by_default.and_return { |m| raise RightScale::Log.format(*m) }
35
+ @options = {}
35
36
  @url = "http://my.com"
36
37
  @urls = [@url]
38
+ @host = @url
37
39
  @path = "/foo/bar"
38
40
  @balancer = flexmock("balancer")
41
+ flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer).by_default
42
+ @later = (@now = Time.now)
43
+ @tick = 0.01
44
+ flexmock(Time).should_receive(:now).and_return { @later += @tick }
39
45
  end
40
46
 
41
47
  context :initialize do
42
- ['HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', 'ALL_PROXY'].each do |proxy|
43
- it "initializes use of proxy if #{proxy} defined in environment" do
44
- ENV[proxy] = "https://my.proxy.com"
45
- flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer)
46
- flexmock(RestClient).should_receive(:proxy=).with("https://my.proxy.com").once
47
- RightScale::BalancedHttpClient.new(@urls)
48
- ENV.delete(proxy)
49
- end
48
+ it "initializes parameter filter list" do
49
+ client = RightScale::BalancedHttpClient.new(@urls)
50
+ client.instance_variable_get(:@filter_params).should == []
51
+ end
50
52
 
51
- it "prepends scheme to proxy address if #{proxy} defined in environment" do
52
- ENV[proxy] = "1.2.3.4"
53
- flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer)
54
- flexmock(RestClient).should_receive(:proxy=).with("http://1.2.3.4").once
55
- RightScale::BalancedHttpClient.new(@urls)
56
- ENV.delete(proxy)
57
- end
53
+ it "stores specified parameter filters" do
54
+ client = RightScale::BalancedHttpClient.new(@urls, :filter_params => [:secret])
55
+ client.instance_variable_get(:@filter_params).should == ["secret"]
56
+ end
57
+
58
+ it "creates blocking HTTP client" do
59
+ client = RightScale::BalancedHttpClient.new(@urls)
60
+ client.instance_variable_get(:@http_client).should be_a RightScale::BlockingClient
61
+ end
62
+
63
+ it "creates non-blocking HTTP client if specified" do
64
+ client = RightScale::BalancedHttpClient.new(@urls, :non_blocking => true)
65
+ client.instance_variable_get(:@http_client).should be_a RightScale::NonBlockingClient
58
66
  end
59
67
 
60
68
  it "creates request balancer with health checker" do
@@ -73,24 +81,28 @@ describe RightScale::BalancedHttpClient do
73
81
  context :check_health do
74
82
  before(:each) do
75
83
  @urls = ["http://my0.com", @url]
76
- @http_client = flexmock("http client")
77
- flexmock(RightSupport::Net::HTTPClient).should_receive(:new).and_return(@http_client).by_default
78
84
  @client = RightScale::BalancedHttpClient.new(@urls)
85
+ @health_check_proc = @client.instance_variable_get(:@http_client).health_check_proc
79
86
  end
80
87
 
81
88
  it "calls health check proc using first URL" do
82
- @http_client.should_receive(:get).with("http://my0.com/health-check", Hash).once
89
+ flexmock(@health_check_proc).should_receive(:call).with("http://my0.com").once
83
90
  @client.check_health
84
91
  end
85
92
 
86
93
  it "calls health check proc using specified URL" do
87
- @http_client.should_receive(:get).with("http://my1.com/health-check", Hash).once
88
- @client.check_health("http://my1.com")
94
+ flexmock(@health_check_proc).should_receive(:call).with("http://my1.com/api").once
95
+ @client.check_health("http://my1.com/api")
96
+ end
97
+
98
+ it "returns result of health check" do
99
+ flexmock(@health_check_proc).should_receive(:call).with("http://my0.com").and_return("ok").once
100
+ @client.check_health.should == "ok"
89
101
  end
90
102
 
91
103
  RightScale::BalancedHttpClient::RETRY_STATUS_CODES.each do |code|
92
104
  it "raises NotResponding for #{code}" do
93
- @http_client.should_receive(:get).and_raise(RestExceptionMock.new(code))
105
+ flexmock(@health_check_proc).should_receive(:call).and_raise(RightScale::HttpExceptions.create(code))
94
106
  lambda { @client.check_health("http://my1.com") }.should raise_error(RightScale::BalancedHttpClient::NotResponding)
95
107
  end
96
108
  end
@@ -98,248 +110,244 @@ describe RightScale::BalancedHttpClient do
98
110
 
99
111
  context :request do
100
112
  before(:each) do
101
- @result = {:out => 123}
102
- @json_result = JSON.dump(@result)
103
- @json_decoded_result = {"out" => 123}
104
- @response = flexmock("response")
105
- @response.should_receive(:code).and_return(200).by_default
106
- @response.should_receive(:body).and_return(@json_result).by_default
107
- @response.should_receive(:headers).and_return({:status => "200 OK"}).by_default
113
+ @params = {}
114
+ @result = {"out" => 123}
115
+ @body = JSON.dump({:out => 123})
116
+ @headers = {:status => "200 OK"}
117
+ @response = [@result, 200, @body, @headers]
118
+ @balancer.should_receive(:request).and_yield(@url).and_return(@response).by_default
108
119
  flexmock(RightSupport::Data::UUID).should_receive(:generate).and_return("random uuid").by_default
109
- @balancer.should_receive(:request).and_yield(@url).by_default
110
- flexmock(RightSupport::Net::RequestBalancer).should_receive(:new).and_return(@balancer).by_default
111
- @http_client = flexmock("http client")
112
- flexmock(RightSupport::Net::HTTPClient).should_receive(:new).and_return(@http_client).by_default
113
- @client = RightScale::BalancedHttpClient.new(@urls)
120
+ @client = RightScale::BalancedHttpClient.new(@urls, :filter_params => [:secret])
121
+ @http_client = @client.instance_variable_get(:@http_client)
122
+ flexmock(@http_client).should_receive(:request).and_return(@response).by_default
114
123
  end
115
124
 
116
- context "with no options" do
117
- before(:each) do
118
- @options = {}
119
- @http_client.should_receive(:get).with("#{@url}#{@path}", on { |a| @options = a }).and_return(nil)
120
- @client.send(:request, :get, @path).should be_nil
121
- end
122
-
123
- it "sets default open and request timeouts" do
124
- @options[:open_timeout].should == RightScale::BalancedHttpClient::DEFAULT_OPEN_TIMEOUT
125
- @options[:timeout].should == RightScale::BalancedHttpClient::DEFAULT_REQUEST_TIMEOUT
126
- end
127
-
128
- it "sets random request uuid in header" do
129
- @options[:headers]["X-Request-Lineage-Uuid"] == "random uuid"
130
- end
125
+ it "uses specified request UUID" do
126
+ @log.should_receive(:info).with("Requesting POST <my uuid> /foo/bar").once
127
+ @log.should_receive(:info).with("Completed <my uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes")
128
+ @client.request(:post, @path, @params, :request_uuid => "my uuid")
129
+ end
131
130
 
132
- it "sets response to be JSON-encoded" do
133
- @options[:headers][:accept] == "application/json"
134
- end
131
+ it "generates request UUID if none specified" do
132
+ @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
133
+ @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
134
+ @client.request(:post, @path)
135
+ end
135
136
 
136
- it "does not set API version" do
137
- @options[:headers]["X-API-Version"].should be_nil
138
- end
137
+ it "logs request before sending it" do
138
+ @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
139
+ flexmock(@http_client).should_receive(:request).and_raise(RuntimeError).once
140
+ @client.request(:post, @path) rescue nil
139
141
  end
140
142
 
141
- context "with options" do
142
- before(:each) do
143
- @options = {}
144
- create_options = {
145
- :request_uuid => "my uuid",
146
- :api_version => "1.0" }
147
- request_options = {
148
- :open_timeout => 1,
149
- :request_timeout => 2,
150
- :headers => {"Authorization" => "Bearer <session>"} }
151
- @client = RightScale::BalancedHttpClient.new(@urls, create_options)
152
- @http_client.should_receive(:get).with("#{@url}#{@path}", on { |a| @options = a }).and_return(nil)
153
- @client.send(:request, :get, @path, nil, request_options).should be_nil
154
- end
143
+ it "logs using specified log level" do
144
+ @params = {:some => "data"}
145
+ @log.should_receive(:debug).with("Requesting POST <random uuid> /foo/bar {:some=>\"data\"}").once
146
+ @log.should_receive(:debug).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes | {\"out\"=>123}").once
147
+ @client.request(:post, @path, @params, :log_level => :debug)
148
+ end
155
149
 
156
- it "sets open and request timeouts" do
157
- @options[:open_timeout].should == 1
158
- @options[:timeout].should == 2
159
- end
150
+ it "logs using configured log level if none specified" do
151
+ @params = {:some => "data"}
152
+ @log.should_receive(:level).and_return(:debug)
153
+ @log.should_receive(:debug).with("Requesting POST <random uuid> /foo/bar {:some=>\"data\"}").once
154
+ @log.should_receive(:debug).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes | {\"out\"=>123}").once
155
+ @client.request(:post, @path, @params)
156
+ end
160
157
 
161
- it "sets request uuid in header" do
162
- @options[:headers]["X-Request-Lineage-Uuid"] == "my uuid"
163
- end
158
+ it "appends specified filter parameters to list" do
159
+ @params = {:some => "data", :secret => "data", :other => "data"}
160
+ @log.should_receive(:debug).with("Requesting POST <random uuid> /foo/bar {:some=>\"data\", :secret=>\"<hidden>\", :other=>\"<hidden>\"}").once
161
+ @log.should_receive(:debug).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes | {\"out\"=>123}").once
162
+ @client.request(:post, @path, @params, :log_level => :debug, :filter_params => [:other])
163
+ end
164
164
 
165
- it "sets API version in header" do
166
- @options[:headers]["X-API-Version"].should == "1.0"
167
- end
165
+ it "removes user and password from host when logging" do
166
+ flexmock(@client).should_receive(:log_success).with(@result, 200, @body, @headers, "http://my.com", @path, "random uuid", Time, :info).once
167
+ @balancer.should_receive(:request).and_yield("http://111:secret@my.com").and_return(@response).once
168
+ @client.request(:post, @path)
169
+ end
168
170
 
169
- it "uses headers for setting authorization in header" do
170
- @options[:headers]["Authorization"].should == "Bearer <session>"
171
- end
171
+ it "logs successful completion of request" do
172
+ @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar")
173
+ @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
174
+ @client.request(:post, @path, @params, @options)
172
175
  end
173
176
 
174
- [:get, :delete].each do |verb|
175
- context "with #{verb.inspect}" do
176
- it "uses form-encoded query option for parameters" do
177
- params = {:id => 10}
178
- @http_client.should_receive(verb).with("#{@url}#{@path}", on { |a| a[:query] == params &&
179
- a[:payload].nil? }).and_return(nil)
180
- @client.send(:request, verb, @path, params).should be_nil
181
- end
182
- end
177
+ it "returns result of request" do
178
+ @client.request(:post, @path, @params, @options).should == @result
183
179
  end
184
180
 
185
- [:post, :put].each do |verb|
186
- context "with #{verb.inspect}" do
187
- it "uses JSON-encoded payload options for parameters" do
188
- payload = {:pay => "load"}
189
- json_payload = JSON.dump(payload)
190
- @http_client.should_receive(verb).with("#{@url}#{@path}", on { |a| a[:payload] == json_payload &&
191
- a[:query].nil? }).and_return(nil).once
192
- @client.send(:request, verb, @path, payload).should be_nil
193
- end
194
- end
181
+ it "handles poll requests separately" do
182
+ flexmock(@client).should_receive(:poll_request).and_return(@response).once
183
+ flexmock(@client).should_receive(:rest_request).and_return(@response).never
184
+ @client.request(:poll, @path, @params, @options).should == @result
195
185
  end
196
186
 
197
- context "health check proc" do
198
- it "removes user and password from URL when checking health" do
199
- @url = "http://me:pass@my.com"
200
- @client = RightScale::BalancedHttpClient.new(@url, :health_check_path => "/health-check")
201
- @http_client.should_receive(:get).with("http://my.com/health-check", Hash).once
202
- @client.instance_variable_get(:@health_check_proc).call(@url)
203
- end
187
+ it "handles NoResult response from balancer" do
188
+ no_result = RightSupport::Net::NoResult.new("no result", {})
189
+ @log.should_receive(:error).with(/Failed <random uuid>.*#{@path}/).once
190
+ @balancer.should_receive(:request).and_yield(@url).once
191
+ flexmock(@http_client).should_receive(:request).and_raise(no_result).once
192
+ flexmock(@client).should_receive(:handle_no_result).with(RightSupport::Net::NoResult, @url, Proc).and_yield(no_result).once
193
+ @client.request(:get, @path)
194
+ end
204
195
 
205
- it "uses default path if none specified" do
206
- @client = RightScale::BalancedHttpClient.new(@url)
207
- @http_client.should_receive(:get).with("http://my.com/health-check", Hash).once
208
- @client.instance_variable_get(:@health_check_proc).call(@url)
209
- end
196
+ it "converts RestClient exception to HttpException and raises it" do
197
+ bad_request = RestClient::Exceptions::EXCEPTIONS_MAP[400].new(nil, 400)
198
+ @log.should_receive(:error).with(/Failed <random uuid>.*#{@path}/).once
199
+ flexmock(@http_client).should_receive(:request).and_raise(bad_request).once
200
+ lambda { @client.request(:get, @path) }.should raise_error(RightScale::HttpExceptions::BadRequest)
201
+ end
210
202
 
211
- it "appends health check path to any existing path" do
212
- @url = "http://my.com/foo"
213
- @client = RightScale::BalancedHttpClient.new(@url)
214
- @http_client.should_receive(:get).with("http://my.com/foo/health-check", Hash).once
215
- @client.instance_variable_get(:@health_check_proc).call(@url)
216
- end
203
+ it "logs and re-raises unexpected exception" do
204
+ @log.should_receive(:error).with(/Failed <random uuid>.*#{@path}/).once
205
+ flexmock(@http_client).should_receive(:request).and_raise(RuntimeError).once
206
+ lambda { @client.request(:get, @path) }.should raise_error(RuntimeError)
207
+ end
217
208
 
218
- it "uses fixed timeout values" do
219
- @client = RightScale::BalancedHttpClient.new(@url, :open_timeout => 5, :request_timeout => 30)
220
- @http_client.should_receive(:get).with(String, {:open_timeout => 2, :timeout => 5}).once
221
- @client.instance_variable_get(:@health_check_proc).call(@url)
222
- end
209
+ it "passes back host used with user and password removed" do
210
+ flexmock(@client).should_receive(:log_success).with(@result, 200, @body, @headers, "http://my.com", @path, "random uuid", Time, :info).once
211
+ @balancer.should_receive(:request).and_yield("http://111:secret@my.com").and_return(@response).once
212
+ @client.request(:post, @path)
213
+ end
214
+ end
223
215
 
224
- it "sets API version if specified" do
225
- @client = RightScale::BalancedHttpClient.new(@url, :api_version => "2.0")
226
- @http_client.should_receive(:get).with(String, hsh(:headers => {"X-API-Version" => "2.0"})).once
227
- @client.instance_variable_get(:@health_check_proc).call(@url)
228
- end
216
+ context :request_headers do
217
+ before(:each) do
218
+ @request_uuid = "my uuid"
219
+ @client = RightScale::BalancedHttpClient.new(@urls)
229
220
  end
230
221
 
231
- it "sends request using HTTPClient via balancer" do
232
- @http_client.should_receive(:post).with("#{@url}#{@path}", Hash).and_return(nil).once
233
- @balancer.should_receive(:request).and_yield(@url).once
234
- @client.send(:request, :post, @path).should be_nil
222
+ it "sets request uuid in header" do
223
+ headers = @client.send(:request_headers, @request_uuid, @options)
224
+ headers["X-Request-Lineage-Uuid"].should == "my uuid"
235
225
  end
236
226
 
237
- it "returns location header for 201 response" do
238
- @response.should_receive(:code).and_return(201).once
239
- @response.should_receive(:body).and_return("").once
240
- @response.should_receive(:headers).and_return({:status => "201 Created", :location => "/href"}).once
241
- @balancer.should_receive(:request).and_return(@response).once
242
- @client.send(:request, :get, @path).should == "/href"
227
+ it "sets response to be JSON-encoded" do
228
+ headers = @client.send(:request_headers, @request_uuid, @options)
229
+ headers[:accept] == "application/json"
243
230
  end
244
231
 
245
- it "returns JSON decoded response" do
246
- @response.should_receive(:body).and_return(@json_result).once
247
- @balancer.should_receive(:request).and_return(@response).once
248
- @client.send(:request, :get, @path).should == @json_decoded_result
232
+ it "sets API version" do
233
+ @client = RightScale::BalancedHttpClient.new(@urls, :api_version => "2.0")
234
+ headers = @client.send(:request_headers, @request_uuid, @options)
235
+ headers["X-API-Version"].should == "2.0"
249
236
  end
250
237
 
251
- it "returns nil if response is empty" do
252
- @response.should_receive(:body).and_return("").once
253
- @balancer.should_receive(:request).and_return(@response).once
254
- @client.send(:request, :get, @path).should be_nil
238
+ it "does not set API version if not defined" do
239
+ headers = @client.send(:request_headers, @request_uuid, @options)
240
+ headers.has_key?("X-API-Version").should be false
255
241
  end
256
242
 
257
- it "returns nil if response is nil" do
258
- @balancer.should_receive(:request).and_return(nil).once
259
- @client.send(:request, :get, @path).should be_nil
243
+ it "sets any optionally specified headers" do
244
+ @options = {:headers => {"Authorization" => "Bearer <session>"}}
245
+ headers = @client.send(:request_headers, @request_uuid, @options)
246
+ headers["Authorization"].should == "Bearer <session>"
260
247
  end
261
248
 
262
- it "returns nil if response status indicates no content" do
263
- @response.should_receive(:code).and_return(204).once
264
- @balancer.should_receive(:request).and_return(@response).once
265
- @client.send(:request, :get, @path).should be_nil
249
+ it "sets debug option if in debug mode" do
250
+ @log.should_receive(:level).and_return(:debug)
251
+ headers = @client.send(:request_headers, @request_uuid, @options)
252
+ headers["X-DEBUG"].should be true
266
253
  end
254
+ end
267
255
 
268
- it "handles NoResult response from balancer" do
269
- no_result = RightSupport::Net::NoResult.new("no result", {})
270
- @log.should_receive(:error).with(/Failed <random uuid>.*#{@path}/).once
271
- @balancer.should_receive(:request).and_yield(@url).once
272
- flexmock(RightSupport::Net::HTTPClient).should_receive(:new).and_raise(no_result).once
273
- flexmock(@client).should_receive(:handle_no_result).with(RightSupport::Net::NoResult, @url, Proc).and_yield(no_result).once
274
- @client.send(:request, :get, @path)
256
+ context "requesting" do
257
+ before(:each) do
258
+ @tick = 1
259
+ @used = {}
260
+ @result = {"out" => 123}
261
+ @body = JSON.dump({:out => 123})
262
+ @headers = {:status => "200 OK"}
263
+ @response = [@result, 200, @body, @headers]
264
+ @balancer.should_receive(:request).and_yield(@url).and_return(@response).by_default
265
+ @client = RightScale::BalancedHttpClient.new(@urls)
266
+ @http_client = @client.instance_variable_get(:@http_client)
267
+ flexmock(@http_client).should_receive(:request).and_return(@response).by_default
268
+ @connect_options = {}
269
+ @request_options = {:open_timeout => 2, :request_timeout => 5}
275
270
  end
276
271
 
277
- it "reports and re-raises unexpected exception" do
278
- @log.should_receive(:error).with(/Failed <random uuid>.*#{@path}/).once
279
- @balancer.should_receive(:request).and_raise(RuntimeError).once
280
- lambda { @client.send(:request, :get, @path) }.should raise_error(RuntimeError)
272
+ context :rest_request do
273
+ it "make requests using request balancer" do
274
+ flexmock(@http_client).should_receive(:request).with(:post, @path, @url, Hash, Hash).and_return(@response).once
275
+ @balancer.should_receive(:request).and_yield(@url).once
276
+ @client.send(:rest_request, :post, @path, @connect_options, @request_options, @used)
277
+ end
278
+
279
+ it "uses request balancer to make request" do
280
+ flexmock(@http_client).should_receive(:request).with(:post, @path, @url, Hash, Hash).and_return(@response).once
281
+ @balancer.should_receive(:request).and_yield(@url).and_return(@response).once
282
+ @client.send(:rest_request, :post, @path, @connect_options, @request_options, @used)
283
+ end
284
+
285
+ it "passes back host used with user and password removed" do
286
+ @balancer.should_receive(:request).and_yield("http://111:secret@my.com").and_return(@response).once
287
+ @client.send(:rest_request, :post, @path, @connect_options, @request_options, @used)
288
+ @used[:host].should == "http://my.com"
289
+ end
290
+
291
+ it "returns result and response info" do
292
+ @balancer.should_receive(:request).and_yield(@url).and_return(@response).once
293
+ @client.send(:rest_request, :post, @path, @connect_options, @request_options, @used).should == @response
294
+ end
281
295
  end
282
296
 
283
- context "when logging" do
297
+ context :poll_request do
284
298
  before(:each) do
285
- now = Time.now
286
- flexmock(Time).should_receive(:now).and_return(now, now + 0.01)
299
+ @started_at = @now
300
+ @request_timeout = 30
301
+ @stop_at = @started_at + @request_timeout
302
+ @connection = {:host => @host, :expires_at => @later + 5}
287
303
  end
288
304
 
289
- it "logs request and response" do
290
- @http_client.should_receive(:post).and_return(@response)
291
- @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
292
- @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
293
- @client.send(:request, :post, @path)
305
+ it "makes get request if there is no existing connection" do
306
+ @http_client.instance_variable_get(:@connections)[@path].should be nil
307
+ flexmock(@http_client).should_receive(:request).with(:get, @path, @url, Hash, Hash).and_return(@response).once
308
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used)
294
309
  end
295
310
 
296
- it "logs using specified log level" do
297
- @http_client.should_receive(:post).and_return(@response)
298
- @log.should_receive(:debug).with("Requesting POST <random uuid> /foo/bar").once
299
- @log.should_receive(:debug).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
300
- @client.send(:request, :post, @path, {}, :log_level => :debug)
311
+ it "makes get request if there is connection but it has expired" do
312
+ @connection = {:host => @host, :expires_at => @now}
313
+ @http_client.instance_variable_get(:@connections)[@path] = @connection
314
+ flexmock(@http_client).should_receive(:request).with(:get, @path, @url, Hash, Hash).and_return(@response).once
315
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used)
301
316
  end
302
317
 
303
- it "logs response length using header :content_length if available" do
304
- @response.should_receive(:headers).and_return({:status => "200 OK", :content_length => 99})
305
- @http_client.should_receive(:post).and_return(@response)
306
- @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
307
- @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 99 bytes").once
308
- @client.send(:request, :post, @path)
318
+ it "returns after get request if request has timed out" do
319
+ @response = [nil, 200, nil, @headers]
320
+ @balancer.should_receive(:request).and_return(@response).once
321
+ @client.send(:poll_request, @path, @connect_options, @request_options, 1, @started_at, @used).should == @response
309
322
  end
310
323
 
311
- it "omits user and password from logged host" do
312
- url = "http://111:secret@my.com"
313
- @client = RightScale::BalancedHttpClient.new([url])
314
- @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
315
- @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
316
- @http_client.should_receive(:post).with("#{url}#{@path}", Hash).and_return(@response).once
317
- @balancer.should_receive(:request).and_yield(url).once
318
- @client.send(:request, :post, @path)
324
+ it "makes poll request after get request if result is nil and there is an unexpired connection" do
325
+ get_response = [nil, 200, nil, @headers]
326
+ @balancer.should_receive(:request).and_return do
327
+ @http_client.instance_variable_get(:@connections)[@path] = @connection
328
+ get_response
329
+ end.once
330
+ flexmock(@http_client).should_receive(:poll).with(@connection, @request_options, @stop_at).and_return(@response).once
331
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used).should == @response
319
332
  end
320
333
 
321
- context "when filtering" do
322
- before(:each) do
323
- @client = RightScale::BalancedHttpClient.new(@urls, :filter_params => [:secret])
324
- @params = {:public => "data", "secret" => "data", "very secret" => "data"}
325
- @options = {:filter_params => ["very secret"]}
326
- @filtered_params = "{:public=>\"data\", \"secret\"=>\"<hidden>\", \"very secret\"=>\"<hidden>\"}"
327
- end
334
+ it "makes poll request without get request if there is an unexpired connection" do
335
+ @connection[:expires_at] = @later + 10
336
+ @http_client.instance_variable_get(:@connections)[@path] = @connection
337
+ flexmock(@http_client).should_receive(:poll).with(@connection, @request_options, @stop_at).and_return(@response).once
338
+ flexmock(@http_client).should_receive(:request).never
339
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used).should == @response
340
+ end
328
341
 
329
- it "logs request with filtered params if in debug mode" do
330
- @log.should_receive(:level).and_return(:debug)
331
- @http_client.should_receive(:post).and_return(@response)
332
- @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar #{@filtered_params}").once
333
- @log.should_receive(:info).with("Completed <random uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes | {\"out\"=>123}").once
334
- @client.send(:request, :post, @path, @params, @options)
335
- end
342
+ it "returns result and response info" do
343
+ @balancer.should_receive(:request).and_yield(@url).and_return(@response).once
344
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used).should == @response
345
+ end
336
346
 
337
- it "logs response failure including filtered params" do
338
- @http_client.should_receive(:post).and_raise(RestExceptionMock.new(400, "bad data")).once
339
- @log.should_receive(:info).with("Requesting POST <random uuid> /foo/bar").once
340
- @log.should_receive(:error).with("Failed <random uuid> in 10ms | 400 [http://my.com/foo/bar #{@filtered_params}] | 400 Bad Request: bad data").once
341
- lambda { @client.send(:request, :post, @path, @params, @options) }.should raise_error(RuntimeError)
342
- end
347
+ it "passes back host used" do
348
+ @balancer.should_receive(:request).and_yield("http://111:secret@my.com").and_return(@response).once
349
+ @client.send(:poll_request, @path, @connect_options, @request_options, @request_timeout, @started_at, @used)
350
+ @used[:host].should == "http://my.com"
343
351
  end
344
352
  end
345
353
  end
@@ -364,8 +372,8 @@ describe RightScale::BalancedHttpClient do
364
372
  end
365
373
 
366
374
  it "uses last exception stored in NoResult details" do
367
- gateway_timeout = RestExceptionMock.new(504, "server timeout")
368
- bad_request = RestExceptionMock.new(400, "bad data")
375
+ gateway_timeout = RightScale::HttpExceptions.create(504, "server timeout")
376
+ bad_request = RightScale::HttpExceptions.create(400, "bad data")
369
377
  @no_result = RightSupport::Net::NoResult.new("no result", {@url => gateway_timeout, @url => bad_request})
370
378
  lambda { @client.send(:handle_no_result, @no_result, @url, &@proc) }.should raise_error(bad_request)
371
379
  @yielded.should == bad_request
@@ -378,7 +386,7 @@ describe RightScale::BalancedHttpClient do
378
386
  end
379
387
 
380
388
  it "uses http_body in raised NotResponding exception if status code is 504" do
381
- gateway_timeout = RestExceptionMock.new(504, "server timeout")
389
+ gateway_timeout = RightScale::HttpExceptions.create(504, "server timeout")
382
390
  @no_result = RightSupport::Net::NoResult.new("no result", {@url => gateway_timeout})
383
391
  lambda { @client.send(:handle_no_result, @no_result, @url, &@proc) }.should \
384
392
  raise_error(RightScale::BalancedHttpClient::NotResponding, "server timeout")
@@ -386,7 +394,7 @@ describe RightScale::BalancedHttpClient do
386
394
  end
387
395
 
388
396
  it "uses raise NotResponding if status code is 504 and http_body is nil or empty" do
389
- gateway_timeout = RestExceptionMock.new(504, "")
397
+ gateway_timeout = RightScale::HttpExceptions.create(504, "")
390
398
  @no_result = RightSupport::Net::NoResult.new("no result", {@url => gateway_timeout})
391
399
  lambda { @client.send(:handle_no_result, @no_result, @url, &@proc) }.should \
392
400
  raise_error(RightScale::BalancedHttpClient::NotResponding, "http://my.com not responding")
@@ -395,7 +403,7 @@ describe RightScale::BalancedHttpClient do
395
403
 
396
404
  [502, 503].each do |code|
397
405
  it "uses server name in NotResponding exception if status code is #{code}" do
398
- e = RestExceptionMock.new(code)
406
+ e = RightScale::HttpExceptions.create(code)
399
407
  @no_result = RightSupport::Net::NoResult.new("no result", {@url => e})
400
408
  lambda { @client.send(:handle_no_result, @no_result, @url, &@proc) }.should \
401
409
  raise_error(RightScale::BalancedHttpClient::NotResponding, "http://my.com not responding")
@@ -403,34 +411,63 @@ describe RightScale::BalancedHttpClient do
403
411
  end
404
412
 
405
413
  it "raises last exception in details if not retryable" do
406
- bad_request = RestExceptionMock.new(400, "bad data")
414
+ bad_request = RightScale::HttpExceptions.create(400, "bad data")
407
415
  @no_result = RightSupport::Net::NoResult.new("no result", {@url => bad_request})
408
416
  lambda { @client.send(:handle_no_result, @no_result, @url, &@proc) }.should raise_error(bad_request)
409
417
  @yielded.should == bad_request
410
418
  end
411
419
  end
412
420
 
413
- context :report_failure do
421
+ context :log_success do
422
+ before(:each) do
423
+ @result = {"out" => 123}
424
+ @body = JSON.dump({:out => 123})
425
+ @headers = {:status => "200 OK"}
426
+ @response = [@result, 200, @body, @headers]
427
+ @client = RightScale::BalancedHttpClient.new(@urls)
428
+ end
429
+
430
+ it "logs response length using header :content_length if available" do
431
+ @headers.merge!(:content_length => 99)
432
+ @log.should_receive(:info).with("Completed <uuid> in 10ms | 200 [http://my.com/foo/bar] | 99 bytes").once
433
+ @client.send(:log_success, @result, 200, @body, @headers, "http://my.com", @path, "uuid", @now, :info).should be true
434
+ end
435
+
436
+ it "logs response length using response body size of :content_lenght not available" do
437
+ @log.should_receive(:info).with("Completed <uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes").once
438
+ @client.send(:log_success, @result, 200, @body, @headers, "http://my.com", @path, "uuid", @now, :info).should be true
439
+ end
440
+
441
+ it "logs duration based on request start time" do
442
+ @log.should_receive(:info).with("Completed <uuid> in 20ms | 200 [http://my.com/foo/bar] | 11 bytes").once
443
+ @client.send(:log_success, @result, 200, @body, @headers, "http://my.com", @path, "uuid", @now - 0.01, :info).should be true
444
+ end
445
+
446
+ it "log result if log level option set to :debug" do
447
+ @log.should_receive(:debug).with("Completed <uuid> in 10ms | 200 [http://my.com/foo/bar] | 11 bytes | {\"out\"=>123}").once
448
+ @client.send(:log_success, @result, 200, @body, @headers, "http://my.com", @path, "uuid", @now, :debug).should be true
449
+ end
450
+ end
451
+
452
+ context :log_failure do
414
453
  before(:each) do
415
- @started_at = Time.now
416
- flexmock(Time).should_receive(:now).and_return(@started_at + 0.01)
417
454
  @client = RightScale::BalancedHttpClient.new(@urls)
418
455
  end
419
456
 
420
457
  it "logs exception" do
421
- exception = RestExceptionMock.new(400, "bad data")
458
+ exception = RightScale::HttpExceptions.create(400, "bad data")
422
459
  @log.should_receive(:error).with("Failed <uuid> in 10ms | 400 [http://my.com/foo/bar \"params\"] | 400 Bad Request: bad data").once
423
- @client.send(:report_failure, @url, @path, "params", [], "uuid", @started_at, exception)
460
+ @client.send(:log_failure, @url, @path, "params", [], "uuid", @now, :info, exception).should be true
424
461
  end
425
462
 
426
463
  it "logs error string" do
427
464
  @log.should_receive(:error).with("Failed <uuid> in 10ms | nil [http://my.com/foo/bar \"params\"] | bad data").once
428
- @client.send(:report_failure, @url, @path, "params", [], "uuid", @started_at, "bad data")
465
+ @client.send(:log_failure, @url, @path, "params", [], "uuid", @now, :info, "bad data").should be true
429
466
  end
430
467
 
431
- it "filters params" do
468
+ it "filters parameters" do
432
469
  @log.should_receive(:error).with("Failed <uuid> in 10ms | nil [http://my.com/foo/bar {:secret=>\"<hidden>\"}] | bad data").once
433
- @client.send(:report_failure, @url, @path, {:secret => "data"}, ["secret"], "uuid", @started_at, "bad data")
470
+ @client.send(:log_failure, @url, @path, {:secret => "data"}, ["secret"], "uuid", @now, :info, "bad data").should be true
434
471
  end
435
472
  end
436
473
 
@@ -443,22 +480,21 @@ describe RightScale::BalancedHttpClient do
443
480
 
444
481
  context "in info mode with no host" do
445
482
  it "generates text containing path" do
446
- text = @client.send(:log_text, @path, {:value => 123}, [])
483
+ text = @client.send(:log_text, @path, {:value => 123}, [], :info)
447
484
  text.should == "/foo/bar"
448
485
  end
449
486
  end
450
487
 
451
488
  context "in info mode with host" do
452
489
  it "generates text containing host and path" do
453
- text = @client.send(:log_text, @path, {:value => 123}, [], @url)
490
+ text = @client.send(:log_text, @path, {:value => 123}, [], :info, @url)
454
491
  text.should == "[http://my.com/foo/bar]"
455
492
  end
456
493
  end
457
494
 
458
495
  context "and in debug mode" do
459
- it "generates text containing containing host, path, and filtered params" do
460
- @log.should_receive(:level).and_return(:debug)
461
- text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], @url)
496
+ it "generates text containing containing host, path, and filtered parameters" do
497
+ text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], :debug, @url)
462
498
  text.should == "[http://my.com/foo/bar {:some=>\"data\", :secret=>\"<hidden>\"}]"
463
499
  end
464
500
  end
@@ -466,13 +502,13 @@ describe RightScale::BalancedHttpClient do
466
502
 
467
503
  context "when exception" do
468
504
  it "includes params regardless of mode" do
469
- text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], @url, "failed")
505
+ text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], :info, @url, "failed")
470
506
  text.should == "[http://my.com/foo/bar {:some=>\"data\", :secret=>\"<hidden>\"}] | failed"
471
507
  end
472
508
 
473
509
  it "includes exception text" do
474
- exception = RestExceptionMock.new(400, "bad data")
475
- text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], @url, exception)
510
+ exception = RightScale::HttpExceptions.create(400, "bad data")
511
+ text = @client.send(:log_text, @path, {:some => "data", :secret => "data"}, ["secret"], :info, @url, exception)
476
512
  text.should == "[http://my.com/foo/bar {:some=>\"data\", :secret=>\"<hidden>\"}] | 400 Bad Request: bad data"
477
513
  end
478
514
  end
@@ -489,7 +525,7 @@ describe RightScale::BalancedHttpClient do
489
525
  @client.send(:filter, params, filter).should == {:some => 1, "secret" => "<hidden>"}
490
526
  end
491
527
 
492
- it "converts param names to string before comparing to filter list" do
528
+ it "converts parameter names to string before comparing to filter list" do
493
529
  filter = ["secret", "very secret"]
494
530
  params = {:some => 1, :secret => "data", "very secret" => "data"}
495
531
  @client.send(:filter, params, filter).should == {:some => 1, :secret => "<hidden>", "very secret" => "<hidden>"}
@@ -527,41 +563,110 @@ describe RightScale::BalancedHttpClient do
527
563
  end
528
564
  end
529
565
 
566
+ context :format do
567
+ before(:each) do
568
+ @client = RightScale::BalancedHttpClient
569
+ end
570
+
571
+ it "converts param to key=value format" do
572
+ @client.format({:foo => "bar"}).should == "foo=bar"
573
+ end
574
+
575
+ it "escapes param value" do
576
+ @client.format({:path => "/foo/bar"}).should == "path=%2Ffoo%2Fbar"
577
+ end
578
+
579
+ it "breaks arrays into multiple params" do
580
+ params = {:paths => ["/foo/bar", "/foo/bar2"]}
581
+ @client.format(params).should == "paths[]=%2Ffoo%2Fbar&paths[]=%2Ffoo%2Fbar2"
582
+ end
583
+
584
+ it "separates params with '&'" do
585
+ params = {:foo => 111, :paths => ["/foo/bar", "/foo/bar2"], :bar => 999}
586
+ response = @client.format(params)
587
+ response.split("&").sort.should == ["bar=999", "foo=111", "paths[]=%2Ffoo%2Fbar", "paths[]=%2Ffoo%2Fbar2"]
588
+ end
589
+ end
590
+
591
+ context :process_response do
592
+ before(:each) do
593
+ @result = {"out" => 123}
594
+ @body = JSON.dump({:out => 123})
595
+ @client = RightScale::BalancedHttpClient
596
+ end
597
+
598
+ it "returns location header for 201 response" do
599
+ @client.response(201, "", {:status => "201 Created", :location => "/href"}, false).should == "/href"
600
+ end
601
+
602
+ it "returns body without decoding by default" do
603
+ @client.response(200, @body, {:status => "200 OK"}, false).should == @body
604
+ end
605
+
606
+ it "returns JSON decoded response if decoding requested" do
607
+ @client.response(200, @body, {:status => "200 OK"}, true).should == @result
608
+ end
609
+
610
+ it "returns nil if JSON decoded response is empty" do
611
+ @client.response(200, "null", {:status => "200 OK"}, true).should be nil
612
+ end
613
+
614
+ it "returns nil if response is empty" do
615
+ @client.response(204, nil, {:status => "204 No Content"}, false).should be nil
616
+ end
617
+
618
+ it "returns nil if response is nil" do
619
+ @client.response(200, nil, {:status => "200 OK"}, false).should be nil
620
+ end
621
+
622
+ it "returns nil if body is empty" do
623
+ @client.response(200, "", {:status => "200 OK"}, false).should be nil
624
+ end
625
+
626
+ it "raises exception if response code indicates failure" do
627
+ lambda { @client.response(400, nil, {:status => "400 Bad Request"}, false) }.should raise_error(RightScale::HttpExceptions::BadRequest)
628
+ end
629
+ end
630
+
530
631
  context :exception_text do
632
+ before(:each) do
633
+ @client = RightScale::BalancedHttpClient
634
+ end
635
+
531
636
  context "when string exception" do
532
637
  it "adds exception text" do
533
- RightScale::BalancedHttpClient.exception_text("failed").should == "failed"
638
+ @client.exception_text("failed").should == "failed"
534
639
  end
535
640
  end
536
641
 
537
642
  context "when REST exception" do
538
643
  it "adds exception code/type and any http_body" do
539
- exception = RestExceptionMock.new(400, "bad data")
540
- RightScale::BalancedHttpClient.exception_text(exception).should == "400 Bad Request: bad data"
644
+ exception = RightScale::HttpExceptions.create(400, "bad data")
645
+ @client.exception_text(exception).should == "400 Bad Request: bad data"
541
646
  end
542
647
 
543
648
  it "adds exception code/type but omits http_body if it is html" do
544
- exception = RestExceptionMock.new(400, "<html> bad </html>")
545
- RightScale::BalancedHttpClient.exception_text(exception).should == "400 Bad Request"
649
+ exception = RightScale::HttpExceptions.create(400, "<html> bad </html>")
650
+ @client.exception_text(exception).should == "400 Bad Request"
546
651
  end
547
652
 
548
653
  it "adds exception code/type and omits http_body if it is blank" do
549
- exception = RestExceptionMock.new(400)
550
- RightScale::BalancedHttpClient.exception_text(exception).should == "400 Bad Request"
654
+ exception = RightScale::HttpExceptions.create(400, "")
655
+ @client.exception_text(exception).should == "400 Bad Request"
551
656
  end
552
657
  end
553
658
 
554
659
  context "when NoResult exception" do
555
660
  it "adds exception class and message" do
556
661
  exception = RightSupport::Net::NoResult.new("no result")
557
- RightScale::BalancedHttpClient.exception_text(exception).should == "RightSupport::Net::NoResult: no result"
662
+ @client.exception_text(exception).should == "RightSupport::Net::NoResult: no result"
558
663
  end
559
664
  end
560
665
 
561
666
  context "when non-REST, non-NoResult exception" do
562
667
  it "adds exception class and message" do
563
668
  exception = ArgumentError.new("bad arg")
564
- RightScale::BalancedHttpClient.exception_text(exception).should == "ArgumentError: bad arg"
669
+ @client.exception_text(exception).should == "ArgumentError: bad arg"
565
670
  end
566
671
  end
567
672
 
@@ -569,7 +674,7 @@ describe RightScale::BalancedHttpClient do
569
674
  it "adds exception class, message, and backtrace" do
570
675
  exception = ArgumentError.new("bad arg")
571
676
  flexmock(exception).should_receive(:backtrace).and_return(["line 1", "line 2"])
572
- RightScale::BalancedHttpClient.exception_text(exception).should == "ArgumentError: bad arg in\nline 1\nline 2"
677
+ @client.exception_text(exception).should == "ArgumentError: bad arg in\nline 1\nline 2"
573
678
  end
574
679
  end
575
680
  end