right_agent 2.0.8-x86-mingw32 → 2.1.0-x86-mingw32

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.
@@ -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