right_agent 2.1.5 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -112,7 +112,7 @@ module RightScale
112
112
  result
113
113
  end
114
114
 
115
- # Make long-polling requests until receive data or timeout
115
+ # Make long-polling requests until receive data, hit error, or timeout
116
116
  #
117
117
  # @param [Hash] connection to server from previous request with keys :host, :path,
118
118
  # and :expires_at, with the :expires_at being adjusted on return
@@ -131,6 +131,16 @@ module RightScale
131
131
  [result, code, body, headers]
132
132
  end
133
133
 
134
+ # Close all persistent connections
135
+ #
136
+ # @param [String] reason for closing
137
+ #
138
+ # @return [TrueClass] always true
139
+ def close(reason)
140
+ @connections = {}
141
+ true
142
+ end
143
+
134
144
  protected
135
145
 
136
146
  # Make HTTP request once
@@ -123,17 +123,15 @@ module RightScale
123
123
  # Make request an then yield fiber until it completes
124
124
  fiber = Fiber.current
125
125
  connection = EM::HttpRequest.new(uri.to_s, connect_options)
126
+ # Store connection now so that close will get called if terminating or reconnecting
127
+ c = @connections[path] = {:host => host, :connection => connection, :expires_at => Time.now} if request_options[:keepalive]
126
128
  http = connection.send(verb, request_options)
127
- http.errback { fiber.resume(http.error.to_s == "Errno::ETIMEDOUT" ? 504 : 500,
128
- (http.error && http.error.to_s) || "HTTP connection failure for #{verb.to_s.upcase}") }
129
+ http.errback { @connections.delete(path); fiber.resume(*handle_error(verb, http.error)) }
129
130
  http.callback { fiber.resume(http.response_header.status, http.response, http.response_header) }
130
131
  response_code, response_body, response_headers = Fiber.yield
131
132
  response_headers = beautify_headers(response_headers) if response_headers
132
133
  result = BalancedHttpClient.response(response_code, response_body, response_headers, request_options[:head][:accept])
133
- if request_options[:keepalive]
134
- expires_at = Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT
135
- @connections[path] = {:host => host, :connection => connection, :expires_at => expires_at}
136
- end
134
+ c[:expires_at] = Time.now + BalancedHttpClient::CONNECTION_REUSE_TIMEOUT if request_options[:keepalive]
137
135
  [result, response_code, response_body, response_headers]
138
136
  end
139
137
 
@@ -159,9 +157,21 @@ module RightScale
159
157
  [result, code, body, headers]
160
158
  end
161
159
 
160
+ # Close all persistent connections
161
+ #
162
+ # @param [String] reason for closing
163
+ #
164
+ # @return [TrueClass] always true
165
+ def close(reason)
166
+ @connections.each_value { |c| c[:connection].close(reason) }
167
+ @connections = {}
168
+ true
169
+ end
170
+
162
171
  protected
163
172
 
164
- # Repeatedly make long-polling request until receive data or timeout
173
+ # Repeatedly make long-polling request until receive data, hit error, or timeout
174
+ # Treat "terminating" and "reconnecting" errors as an empty poll result
165
175
  #
166
176
  # @param [Symbol] verb for HTTP REST request
167
177
  # @param [EM:HttpRequest] connection to server from previous request
@@ -173,8 +183,7 @@ module RightScale
173
183
  # @raise [HttpException] HTTP failure with associated status code
174
184
  def poll_again(fiber, connection, request_options, stop_at)
175
185
  http = connection.send(:get, request_options)
176
- http.errback { fiber.resume(http.error.to_s == "Errno::ETIMEDOUT" ? 504 : 500,
177
- (http.error && http.error.to_s) || "HTTP connection failure for POLL") }
186
+ http.errback { fiber.resume(*handle_error("POLL", http.error)) }
178
187
  http.callback do
179
188
  code, body, headers = http.response_header.status, http.response, http.response_header
180
189
  if code == 200 && (body.nil? || body == "null") && Time.now < stop_at
@@ -186,6 +195,20 @@ module RightScale
186
195
  true
187
196
  end
188
197
 
198
+ # Handle error from request
199
+ #
200
+ # @param [Symbol] verb for HTTP REST request
201
+ # @param [Object] error result from HTTP connection
202
+ #
203
+ # @return [Array] status code and error message string
204
+ def handle_error(verb, error)
205
+ case error.to_s
206
+ when "terminating", "reconnecting" then [200, nil]
207
+ when "Errno::ETIMEDOUT" then [408, "Request timeout"]
208
+ else [500, (error && error.to_s) || "HTTP connection failure for #{verb.to_s.upcase}"]
209
+ end
210
+ end
211
+
189
212
  # Beautify response header keys so that in same form as RestClient
190
213
  #
191
214
  # @param [Hash] headers from response
@@ -93,8 +93,11 @@ module RightScale
93
93
  # Packet::GLOBAL, ones with no shard id
94
94
  # [Symbol] :selector for picking from qualified targets: :any or :all;
95
95
  # defaults to :any
96
- # @param [String, NilClass] token uniquely identifying this request;
97
- # defaults to randomly generated ID
96
+ #
97
+ # @option options [String] :request_uuid uniquely identifying this request; defaults to
98
+ # randomly generated
99
+ # @option options [Numeric] :time_to_live seconds before request expires and is to be ignored;
100
+ # non-positive value or nil means never expire
98
101
  #
99
102
  # @return [NilClass] always nil since there is no expected response to the request
100
103
  #
@@ -105,10 +108,10 @@ module RightScale
105
108
  # @raise [Exceptions::RetryableError] request failed but if retried may succeed
106
109
  # @raise [Exceptions::Terminating] closing client and terminating service
107
110
  # @raise [Exceptions::InternalServerError] internal error in server being accessed
108
- def push(type, payload = nil, target = nil, token = nil)
111
+ def push(type, payload = nil, target = nil, options = {})
109
112
  raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
110
113
  client = (@api && @api.support?(type)) ? @api : @router
111
- client.push(type, payload, target, token)
114
+ client.push(type, payload, target, options)
112
115
  end
113
116
 
114
117
  # Route a request to a single target with a response expected
@@ -127,8 +130,11 @@ module RightScale
127
130
  # [Array] :tags that must all be associated with a target for it to be selected
128
131
  # [Hash] :scope for restricting routing which may contain:
129
132
  # [Integer] :account id that agents must be associated with to be included
130
- # @param [String, NilClass] token uniquely identifying this request;
131
- # defaults to randomly generated ID
133
+ #
134
+ # @option options [String] :request_uuid uniquely identifying this request; defaults to
135
+ # randomly generated
136
+ # @option options [Numeric] :time_to_live seconds before request expires and is to be ignored;
137
+ # non-positive value or nil means never expire
132
138
  #
133
139
  # @return [Result, NilClass] response from request
134
140
  #
@@ -139,10 +145,10 @@ module RightScale
139
145
  # @raise [Exceptions::RetryableError] request failed but if retried may succeed
140
146
  # @raise [Exceptions::Terminating] closing client and terminating service
141
147
  # @raise [Exceptions::InternalServerError] internal error in server being accessed
142
- def request(type, payload = nil, target = nil, token = nil)
148
+ def request(type, payload = nil, target = nil, options = {})
143
149
  raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
144
150
  client = (@api && @api.support?(type)) ? @api : @router
145
- client.request(type, payload, target, token)
151
+ client.request(type, payload, target, options)
146
152
  end
147
153
 
148
154
  # Route event
@@ -54,7 +54,7 @@ module RightScale
54
54
  RECONNECT_INTERVAL = 2
55
55
 
56
56
  # Maximum interval between attempts to reconnect or long-poll when router is not responding
57
- MAX_RECONNECT_INTERVAL = 60
57
+ MAX_RECONNECT_INTERVAL = 30
58
58
 
59
59
  # Interval between checks for lost WebSocket connection
60
60
  CHECK_INTERVAL = 5
@@ -116,8 +116,11 @@ module RightScale
116
116
  # Packet::GLOBAL, ones with no shard id
117
117
  # [Symbol] :selector for picking from qualified targets: :any or :all;
118
118
  # defaults to :any
119
- # @param [String, NilClass] token uniquely identifying this request;
120
- # defaults to randomly generated ID
119
+ #
120
+ # @option options [String] :request_uuid uniquely identifying this request; defaults to
121
+ # randomly generated
122
+ # @option options [Numeric] :time_to_live seconds before request expires and is to be ignored;
123
+ # non-positive value or nil means never expire
121
124
  #
122
125
  # @return [NilClass] always nil since there is no expected response to the request
123
126
  #
@@ -127,12 +130,12 @@ module RightScale
127
130
  # @raise [Exceptions::RetryableError] request failed but if retried may succeed
128
131
  # @raise [Exceptions::Terminating] closing client and terminating service
129
132
  # @raise [Exceptions::InternalServerError] internal error in server being accessed
130
- def push(type, payload, target, token = nil)
133
+ def push(type, payload, target, options = {})
131
134
  params = {
132
135
  :type => type,
133
136
  :payload => payload,
134
137
  :target => target }
135
- make_request(:post, "/push", params, type.split("/")[2], token)
138
+ make_request(:post, "/push", params, type.split("/")[2], options)
136
139
  end
137
140
 
138
141
  # Route a request to a single target with a response expected
@@ -152,8 +155,11 @@ module RightScale
152
155
  # [Array] :tags that must all be associated with a target for it to be selected
153
156
  # [Hash] :scope for restricting routing which may contain:
154
157
  # [Integer] :account id that agents must be associated with to be included
155
- # @param [String, NilClass] token uniquely identifying this request;
156
- # defaults to randomly generated ID
158
+ #
159
+ # @option options [String] :request_uuid uniquely identifying this request; defaults to
160
+ # randomly generated
161
+ # @option options [Numeric] :time_to_live seconds before request expires and is to be ignored;
162
+ # non-positive value or nil means never expire
157
163
  #
158
164
  # @return [Result, NilClass] response from request
159
165
  #
@@ -163,12 +169,12 @@ module RightScale
163
169
  # @raise [Exceptions::RetryableError] request failed but if retried may succeed
164
170
  # @raise [Exceptions::Terminating] closing client and terminating service
165
171
  # @raise [Exceptions::InternalServerError] internal error in server being accessed
166
- def request(type, payload, target, token = nil)
172
+ def request(type, payload, target, options = {})
167
173
  params = {
168
174
  :type => type,
169
175
  :payload => payload,
170
176
  :target => target }
171
- make_request(:post, "/request", params, type.split("/")[2], token)
177
+ make_request(:post, "/request", params, type.split("/")[2], options)
172
178
  end
173
179
 
174
180
  # Route event
@@ -198,7 +204,7 @@ module RightScale
198
204
  Log.info("Sending EVENT <#{event[:uuid]}> #{event[:type]}#{path}#{to}")
199
205
  @websocket.send(JSON.dump(params))
200
206
  else
201
- make_request(:post, "/notify", params, "notify", event[:uuid], :filter_params => ["event"])
207
+ make_request(:post, "/notify", params, "notify", :request_uuid => event[:uuid], :filter_params => ["event"])
202
208
  end
203
209
  true
204
210
  end
@@ -344,11 +350,32 @@ module RightScale
344
350
  @listen_interval = CHECK_INTERVAL
345
351
  end
346
352
 
347
- # Loop using next_tick or timer
353
+ listen_loop_wait(Time.now, @listen_interval, routing_keys, &handler)
354
+ end
355
+
356
+ # Wait specified interval before next listen loop
357
+ # Continue waiting if interval changes while waiting
358
+ #
359
+ # @param [Time] started_at time when first started waiting
360
+ # @param [Numeric] interval to wait
361
+ # @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
362
+ #
363
+ # @yield [event] required block called each time event received
364
+ # @yieldparam [Hash] event received
365
+ #
366
+ # @return [TrueClass] always true
367
+ def listen_loop_wait(started_at, interval, routing_keys, &handler)
348
368
  if @listen_interval == 0
349
369
  EM_S.next_tick { listen_loop(routing_keys, &handler) }
350
370
  else
351
- @listen_timer = EM_S::Timer.new(@listen_interval) { listen_loop(routing_keys, &handler) }
371
+ @listen_timer = EM_S::Timer.new(interval) do
372
+ remaining = @listen_interval - (Time.now - started_at)
373
+ if remaining > 0
374
+ listen_loop_wait(started_at, remaining, routing_keys, &handler)
375
+ else
376
+ listen_loop(routing_keys, &handler)
377
+ end
378
+ end
352
379
  end
353
380
  true
354
381
  end
@@ -555,7 +582,7 @@ module RightScale
555
582
  :poll_timeout => @options[:listen_timeout] }
556
583
 
557
584
  event_uuids = []
558
- events = make_request(:poll, "/listen", params, "listen", nil, options)
585
+ events = make_request(:poll, "/listen", params, "listen", options)
559
586
  if events
560
587
  events.each do |event|
561
588
  event = SerializationHelper.symbolize_keys(event)
@@ -610,7 +637,7 @@ module RightScale
610
637
  #
611
638
  # @return [Boolean] true if router not responding, otherwise false
612
639
  def router_not_responding?
613
- @close_code == PROTOCOL_ERROR_CLOSE && @close_reason =~ /502|503/
640
+ @close_code == PROTOCOL_ERROR_CLOSE && @close_reason =~ /408|502|503/
614
641
  end
615
642
 
616
643
  end # RouterClient
@@ -82,6 +82,7 @@ module RightScale
82
82
  @pending += 1
83
83
  command = options.dup
84
84
  command[:verbose] = verbose
85
+ command[:timeout] = timeout
85
86
  command[:cookie] = @cookie
86
87
  EM.next_tick { EM.connect('127.0.0.1', @socket_port, ConnectionHandler, command, self, response_handler) }
87
88
  EM.add_timer(timeout) { EM.stop; raise 'Timed out waiting for agent reply' } if manage_em
@@ -69,7 +69,12 @@ module RightScale
69
69
 
70
70
  # Convert RestClient exception
71
71
  def self.convert(e)
72
- e2 = create(e.http_code, e.http_body, RightScale::Response.new((e.response && e.response.headers) || {}))
72
+ e2 = if e.is_a?(RestClient::RequestTimeout)
73
+ # Special case RequestTimeout because http_code and http_body is typically nil given no actual response
74
+ create(408)
75
+ else
76
+ create(e.http_code, e.http_body, RightScale::Response.new((e.response && e.response.headers) || {}))
77
+ end
73
78
  e2.message = e.message
74
79
  e2
75
80
  end
@@ -44,12 +44,12 @@ begin
44
44
  @@win32_kill.call(sig, *pids)
45
45
  end
46
46
 
47
- # implements getpgid() for Windws
47
+ # implements getpgid() for Windows
48
48
  def self.getpgid(pid)
49
49
  # FIX: we currently only use this to check if the process is running.
50
50
  # it is possible to get the parent process id for a process in Windows if
51
51
  # we actually need this info.
52
- return Process.kill(0, pid).contains?(pid) ? 0 : -1
52
+ return Process.kill(0, pid).include?(pid) ? 0 : -1
53
53
  rescue
54
54
  raise Errno::ESRCH
55
55
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2013 RightScale Inc
2
+ # Copyright (c) 2009-2014 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -27,7 +27,7 @@ module RightScale
27
27
  class OfflineHandler
28
28
 
29
29
  # Maximum seconds to wait before starting flushing offline queue when disabling offline mode
30
- MAX_QUEUE_FLUSH_DELAY = 2 * 60
30
+ MAX_QUEUE_FLUSH_DELAY = 60
31
31
 
32
32
  # Maximum number of offline queued requests before triggering restart vote
33
33
  MAX_QUEUED_REQUESTS = 100
@@ -161,12 +161,23 @@ module RightScale
161
161
  # Queue given request in memory
162
162
  #
163
163
  # === Parameters
164
- # request(Hash):: Request to be stored
164
+ # kind(Symbol):: Kind of request: :send_push or :send_request
165
+ # type(String):: Dispatch route for the request; typically identifies actor and action
166
+ # payload(Object):: Data to be sent with marshalling en route
167
+ # target(Hash|NilClass):: Target for request
168
+ # token(String):: Token uniquely identifying request
169
+ # expires_at(Integer):: Time in seconds in Unix-epoch when this request expires and
170
+ # is to be ignored by the receiver; value 0 means never expire
171
+ #
172
+ # === Block
173
+ # Optional block used to process response asynchronously with the following parameter:
174
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR
165
175
  #
166
176
  # === Return
167
177
  # true:: Always return true
168
- def queue_request(kind, type, payload, target, callback)
169
- request = {:kind => kind, :type => type, :payload => payload, :target => target, :callback => callback}
178
+ def queue_request(kind, type, payload, target, token, expires_at, &callback)
179
+ request = {:kind => kind, :type => type, :payload => payload, :target => target,
180
+ :token => token, :expires_at => expires_at, :callback => callback}
170
181
  Log.info("[offline] Queuing request: #{request.inspect}")
171
182
  vote_to_restart if (@restart_vote_count += 1) >= MAX_QUEUED_REQUESTS
172
183
  if @state == :initializing
@@ -190,7 +201,7 @@ module RightScale
190
201
 
191
202
  protected
192
203
 
193
- # Send any requests that were queued while in offline mode
204
+ # Send any requests that were queued while in offline mode and have not yet timed out
194
205
  # Do this asynchronously to allow for agents to respond to requests
195
206
  # Once all in-memory requests have been flushed, switch off offline mode
196
207
  #
@@ -204,10 +215,12 @@ module RightScale
204
215
  Log.info("[offline] Starting to flush request queue of size #{@queue.size}") unless again || @mode == :initializing
205
216
  if @queue.any?
206
217
  r = @queue.shift
207
- if r[:callback]
208
- Sender.instance.send(r[:kind], r[:type], r[:payload], r[:target]) { |result| r[:callback].call(result) }
218
+ options = {:token => r[:token]}
219
+ if r[:expires_at] != 0 && (options[:time_to_live] = r[:expires_at] - Time.now.to_i) <= 0
220
+ Log.info("[offline] Dropping queued request <#{r[:token]}> because it expired " +
221
+ "#{(-options[:time_to_live]).round} sec ago")
209
222
  else
210
- Sender.instance.send(r[:kind], r[:type], r[:payload], r[:target])
223
+ Sender.instance.send(r[:kind], r[:type], r[:payload], r[:target], options, &r[:callback])
211
224
  end
212
225
  end
213
226
  if @queue.empty?
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2014 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -74,13 +74,13 @@ module RightScale
74
74
  # options(Hash):: Request options
75
75
  # :targets(Array):: Target agent identities from which to randomly choose one
76
76
  # :retry_on_error(Boolean):: Whether request should be retried if recipient returned an error
77
- # :retry_delay(Fixnum):: Number of seconds delay before initial retry with -1 meaning no delay,
77
+ # :retry_delay(Numeric):: Number of seconds delay before initial retry with -1 meaning no delay,
78
78
  # defaults to DEFAULT_RETRY_DELAY
79
- # :retry_delay_count(Fixnum):: Minimum number of retries at initial :retry_delay value before
79
+ # :retry_delay_count(Numeric):: Minimum number of retries at initial :retry_delay value before
80
80
  # increasing delay exponentially and decreasing this count exponentially, defaults to
81
81
  # DEFAULT_RETRY_DELAY_COUNT
82
- # :max_retry_delay(Fixnum):: Maximum number of seconds of retry delay, defaults to DEFAULT_MAX_RETRY_DELAY
83
- # :timeout(Fixnum):: Number of seconds with no response before error callback gets called, with
82
+ # :max_retry_delay(Numeric):: Maximum number of seconds of retry delay, defaults to DEFAULT_MAX_RETRY_DELAY
83
+ # :timeout(Numeric):: Number of seconds with no response before error callback gets called, with
84
84
  # -1 meaning never, defaults to DEFAULT_TIMEOUT
85
85
  #
86
86
  # === Raises
@@ -90,6 +90,7 @@ module RightScale
90
90
  raise ArgumentError.new("payload is required") unless (@payload = payload)
91
91
  @retry_on_error = options[:retry_on_error]
92
92
  @timeout = options[:timeout] || DEFAULT_TIMEOUT
93
+ @expires_at = Time.now.to_i + @timeout if @timeout > 0
93
94
  @retry_delay = options[:retry_delay] || DEFAULT_RETRY_DELAY
94
95
  @retry_delay_count = options[:retry_delay_count] || DEFAULT_RETRY_DELAY_COUNT
95
96
  @max_retry_delay = options[:max_retry_delay] || DEFAULT_MAX_RETRY_DELAY
@@ -105,13 +106,18 @@ module RightScale
105
106
  # === Return
106
107
  # true:: Always return true
107
108
  def run
108
- Sender.instance.send_request(@operation, @payload, retrieve_target(@targets)) { |r| handle_response(r) }
109
- if @cancel_timer.nil? && @timeout > 0
110
- @cancel_timer = EM::Timer.new(@timeout) do
111
- msg = "Request #{@operation} timed out after #{@timeout} seconds"
112
- Log.info(msg)
113
- cancel(msg)
114
- end
109
+ cancel = Proc.new do
110
+ msg = "Request #{@operation} timed out after #{@timeout} seconds"
111
+ Log.info(msg)
112
+ cancel(msg)
113
+ end
114
+
115
+ options = {}
116
+ if @expires_at.nil? || (options[:time_to_live] = @expires_at - Time.now.to_i) > 0
117
+ Sender.instance.send_request(@operation, @payload, retrieve_target(@targets), options) { |r| handle_response(r) }
118
+ @cancel_timer = EM::Timer.new(@timeout) { cancel.call } if @cancel_timer.nil? && @timeout > 0
119
+ else
120
+ cancel.call
115
121
  end
116
122
  true
117
123
  end
@@ -404,9 +404,15 @@ module RightScale
404
404
  pgid = Process.getpgid(pid) rescue -1
405
405
  name = human_readable_name(agent_name, pid_file.identity)
406
406
  if pgid != -1
407
- psdata = `ps up #{pid}`.split("\n").last.split
408
- memory = (psdata[5].to_i / 1024)
409
- puts "#{name} is alive, using #{memory}MB of memory"
407
+ message = "#{name} is alive"
408
+ unless RightScale::Platform.windows?
409
+ # Windows Platform code currently does not support retrieving memory usage
410
+ # information for another process, so only include it for linux
411
+ psdata = `ps up #{pid}`.split("\n").last.split
412
+ memory = (psdata[5].to_i / 1024)
413
+ message << ", using #{memory}MB of memory"
414
+ end
415
+ puts message
410
416
  res = true
411
417
  else
412
418
  puts "#{name} is not running but has a stale pid file at #{pid_file}"