right_agent 1.0.1 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/README.rdoc +10 -8
  2. data/Rakefile +31 -5
  3. data/lib/right_agent.rb +6 -1
  4. data/lib/right_agent/actor.rb +4 -20
  5. data/lib/right_agent/actors/agent_manager.rb +1 -1
  6. data/lib/right_agent/agent.rb +357 -144
  7. data/lib/right_agent/agent_config.rb +7 -6
  8. data/lib/right_agent/agent_identity.rb +13 -11
  9. data/lib/right_agent/agent_tag_manager.rb +60 -64
  10. data/{spec/results_mock.rb → lib/right_agent/clients.rb} +10 -24
  11. data/lib/right_agent/clients/api_client.rb +383 -0
  12. data/lib/right_agent/clients/auth_client.rb +247 -0
  13. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  14. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  15. data/lib/right_agent/clients/right_http_client.rb +279 -0
  16. data/lib/right_agent/clients/router_client.rb +493 -0
  17. data/lib/right_agent/command/command_io.rb +4 -4
  18. data/lib/right_agent/command/command_parser.rb +2 -2
  19. data/lib/right_agent/command/command_runner.rb +1 -1
  20. data/lib/right_agent/connectivity_checker.rb +179 -0
  21. data/lib/right_agent/core_payload_types/secure_document_location.rb +2 -2
  22. data/lib/right_agent/dispatcher.rb +12 -10
  23. data/lib/right_agent/enrollment_result.rb +16 -12
  24. data/lib/right_agent/exceptions.rb +34 -20
  25. data/lib/right_agent/history.rb +10 -5
  26. data/lib/right_agent/log.rb +5 -5
  27. data/lib/right_agent/minimal.rb +1 -0
  28. data/lib/right_agent/multiplexer.rb +1 -1
  29. data/lib/right_agent/offline_handler.rb +270 -0
  30. data/lib/right_agent/packets.rb +7 -7
  31. data/lib/right_agent/payload_formatter.rb +1 -1
  32. data/lib/right_agent/pending_requests.rb +128 -0
  33. data/lib/right_agent/platform.rb +1 -1
  34. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  35. data/lib/right_agent/{idempotent_request.rb → retryable_request.rb} +7 -7
  36. data/lib/right_agent/scripts/agent_controller.rb +28 -26
  37. data/lib/right_agent/scripts/agent_deployer.rb +37 -22
  38. data/lib/right_agent/scripts/common_parser.rb +10 -3
  39. data/lib/right_agent/secure_identity.rb +1 -1
  40. data/lib/right_agent/sender.rb +299 -785
  41. data/lib/right_agent/serialize/secure_serializer.rb +3 -1
  42. data/lib/right_agent/serialize/secure_serializer_initializer.rb +2 -2
  43. data/lib/right_agent/serialize/serializable.rb +8 -3
  44. data/right_agent.gemspec +49 -18
  45. data/spec/agent_config_spec.rb +7 -7
  46. data/spec/agent_identity_spec.rb +7 -4
  47. data/spec/agent_spec.rb +43 -7
  48. data/spec/agent_tag_manager_spec.rb +72 -83
  49. data/spec/clients/api_client_spec.rb +423 -0
  50. data/spec/clients/auth_client_spec.rb +272 -0
  51. data/spec/clients/balanced_http_client_spec.rb +576 -0
  52. data/spec/clients/base_retry_client_spec.rb +635 -0
  53. data/spec/clients/router_client_spec.rb +594 -0
  54. data/spec/clients/spec_helper.rb +111 -0
  55. data/spec/command/command_io_spec.rb +1 -1
  56. data/spec/command/command_parser_spec.rb +1 -1
  57. data/spec/connectivity_checker_spec.rb +83 -0
  58. data/spec/dispatcher_spec.rb +3 -2
  59. data/spec/enrollment_result_spec.rb +2 -2
  60. data/spec/history_spec.rb +51 -39
  61. data/spec/offline_handler_spec.rb +340 -0
  62. data/spec/pending_requests_spec.rb +136 -0
  63. data/spec/{idempotent_request_spec.rb → retryable_request_spec.rb} +73 -73
  64. data/spec/sender_spec.rb +835 -1052
  65. data/spec/serialize/secure_serializer_spec.rb +3 -2
  66. data/spec/spec_helper.rb +54 -1
  67. metadata +71 -12
@@ -76,11 +76,11 @@ module RightScale
76
76
  # true:: Always return true
77
77
  #
78
78
  # === Raise
79
- # (RightScale::Exceptions::Argument):: If block is missing
80
- # (RightScale::Exceptions::Application):: If +listen+ has already been called and +stop+ hasn't since
81
- # (RightScale::Exceptions::Application):: If port is already bound
79
+ # (ArgumentError):: If block is missing
80
+ # (Exceptions::Application):: If +listen+ has already been called and +stop+ hasn't since
81
+ # (Exceptions::Application):: If port is already bound
82
82
  def listen(socket_port, &block)
83
- raise Exceptions::Argument, 'Missing listener block' unless block_given?
83
+ raise ArgumentError, 'Missing listener block' unless block_given?
84
84
  raise Exceptions::Application, 'Already listening' if listening
85
85
  begin
86
86
  @conn = EM.start_server('127.0.0.1', socket_port, ServerInputHandler, block)
@@ -31,9 +31,9 @@ module RightScale
31
31
  # Block that will get called back whenever a command is successfully parsed
32
32
  #
33
33
  # === Raise
34
- # (RightScale::Exceptions::Argument): If block is missing
34
+ # (ArgumentError): If block is missing
35
35
  def initialize &block
36
- raise RightScale::Exceptions::Argument, 'Missing handler block' unless block
36
+ raise ArgumentError, 'Missing handler block' unless block
37
37
  @callback = block
38
38
  @buildup = ''
39
39
  end
@@ -55,7 +55,7 @@ module RightScale
55
55
  # cmd_options[:listen_port](Integer):: Command server listen port
56
56
  #
57
57
  # === Raise
58
- # (RightScale::Exceptions::Application):: If +start+ has already been called and +stop+ hasn't since
58
+ # (Exceptions::Application):: If +start+ has already been called and +stop+ hasn't since
59
59
  def self.start(socket_port, identity, commands, fiber_pool = nil)
60
60
  cmd_options = nil
61
61
  @listen_port = socket_port
@@ -0,0 +1,179 @@
1
+ #
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Broker connectivity checker
26
+ # Checks connectivity when requested
27
+ class ConnectivityChecker
28
+
29
+ # Minimum number of seconds between restarts of the inactivity timer
30
+ MIN_RESTART_INACTIVITY_TIMER_INTERVAL = 60
31
+
32
+ # Number of seconds to wait for ping response from a RightNet router when checking connectivity
33
+ PING_TIMEOUT = 30
34
+
35
+ # Default maximum number of consecutive ping timeouts before attempt to reconnect
36
+ MAX_PING_TIMEOUTS = 3
37
+
38
+ # (EM::Timer) Timer while waiting for RightNet router ping response
39
+ attr_accessor :ping_timer
40
+
41
+ def initialize(sender, check_interval, ping_stats, exception_stats)
42
+ @sender = sender
43
+ @check_interval = check_interval
44
+ @ping_timeouts = {}
45
+ @ping_timer = nil
46
+ @ping_stats = ping_stats
47
+ @exception_stats = exception_stats
48
+ @last_received = Time.now
49
+ @message_received_callbacks = []
50
+ restart_inactivity_timer if @check_interval > 0
51
+ end
52
+
53
+ # Update the time this agent last received a request or response message
54
+ # and restart the inactivity timer thus deferring the next connectivity check
55
+ # Also forward this message receipt notification to any callbacks that have registered
56
+ #
57
+ # === Block
58
+ # Optional block without parameters that is activated when a message is received
59
+ #
60
+ # === Return
61
+ # true:: Always return true
62
+ def message_received(&callback)
63
+ if block_given?
64
+ @message_received_callbacks << callback
65
+ else
66
+ @message_received_callbacks.each { |c| c.call }
67
+ if @check_interval > 0
68
+ now = Time.now
69
+ if (now - @last_received) > MIN_RESTART_INACTIVITY_TIMER_INTERVAL
70
+ @last_received = now
71
+ restart_inactivity_timer
72
+ end
73
+ end
74
+ end
75
+ true
76
+ end
77
+
78
+ # Check whether broker connection is usable by pinging a router via that broker
79
+ # Attempt to reconnect if ping does not respond in PING_TIMEOUT seconds and
80
+ # if have reached timeout limit
81
+ # Ignore request if already checking a connection
82
+ #
83
+ # === Parameters
84
+ # id(String):: Identity of specific broker to use to send ping, defaults to any
85
+ # currently connected broker
86
+ # max_ping_timeouts(Integer):: Maximum number of ping timeouts before attempt
87
+ # to reconnect, defaults to MAX_PING_TIMEOUTS
88
+ #
89
+ # === Return
90
+ # true:: Always return true
91
+ def check(id = nil, max_ping_timeouts = MAX_PING_TIMEOUTS)
92
+ unless @terminating || @ping_timer || (id && !@sender.client.connected?(id))
93
+ @ping_id = id
94
+ @ping_timer = EM::Timer.new(PING_TIMEOUT) do
95
+ if @ping_id
96
+ begin
97
+ @ping_stats.update("timeout")
98
+ @ping_timer = nil
99
+ @ping_timeouts[@ping_id] = (@ping_timeouts[@ping_id] || 0) + 1
100
+ if @ping_timeouts[@ping_id] >= max_ping_timeouts
101
+ Log.error("Mapper ping via broker #{@ping_id} timed out after #{PING_TIMEOUT} seconds and now " +
102
+ "reached maximum of #{max_ping_timeouts} timeout#{max_ping_timeouts > 1 ? 's' : ''}, " +
103
+ "attempting to reconnect")
104
+ host, port, index, priority = @sender.client.identity_parts(@ping_id)
105
+ @sender.agent.connect(host, port, index, priority, force = true)
106
+ else
107
+ Log.warning("Mapper ping via broker #{@ping_id} timed out after #{PING_TIMEOUT} seconds")
108
+ end
109
+ rescue Exception => e
110
+ Log.error("Failed to reconnect to broker #{@ping_id}", e, :trace)
111
+ @exception_stats.track("ping timeout", e)
112
+ end
113
+ else
114
+ @ping_timer = nil
115
+ end
116
+ end
117
+
118
+ handler = lambda do |_|
119
+ begin
120
+ if @ping_timer
121
+ @ping_stats.update("success")
122
+ @ping_timer.cancel
123
+ @ping_timer = nil
124
+ @ping_timeouts[@ping_id] = 0
125
+ @ping_id = nil
126
+ end
127
+ rescue Exception => e
128
+ Log.error("Failed to cancel router ping", e, :trace)
129
+ @exception_stats.track("cancel ping", e)
130
+ end
131
+ end
132
+ request = Request.new("/router/ping", nil, {:from => @sender.identity, :token => AgentIdentity.generate})
133
+ @sender.pending_requests[request.token] = PendingRequest.new(Request, Time.now, handler)
134
+ ids = [@ping_id] if @ping_id
135
+ @ping_id = @sender.send(:publish, request, ids).first
136
+ end
137
+ true
138
+ end
139
+
140
+ # Prepare for agent termination
141
+ #
142
+ # === Return
143
+ # true:: Always return true
144
+ def terminate
145
+ @terminating = true
146
+ @check_interval = 0
147
+ if @ping_timer
148
+ @ping_timer.cancel
149
+ @ping_timer = nil
150
+ end
151
+ if @inactivity_timer
152
+ @inactivity_timer.cancel
153
+ @inactivity_timer = nil
154
+ end
155
+ true
156
+ end
157
+
158
+ protected
159
+
160
+ # Start timer that waits for inactive messaging period to end before checking connectivity
161
+ #
162
+ # === Return
163
+ # true:: Always return true
164
+ def restart_inactivity_timer
165
+ @inactivity_timer.cancel if @inactivity_timer
166
+ @inactivity_timer = EM::Timer.new(@check_interval) do
167
+ begin
168
+ check(id = nil, max_ping_timeouts = 1)
169
+ rescue Exception => e
170
+ Log.error("Failed connectivity check", e, :trace)
171
+ @exception_stats.track("check connectivity", e)
172
+ end
173
+ end
174
+ true
175
+ end
176
+
177
+ end # ConnectivityChecker
178
+
179
+ end # RightScale
@@ -40,10 +40,10 @@ module RightScale
40
40
  # (String):: Access token that should be used to fetch the document
41
41
  attr_accessor :ticket
42
42
 
43
- # Array(String):: names of RightNet agents capable of providing the document.
43
+ # Array(String):: Identities of RightNet agents capable of providing the document.
44
44
  # If nil, then no target should be specified when sending RightNet requests;
45
45
  # this is used to route requests to "trusted infrastructure" nodes at the
46
- # discretion of the mapper.
46
+ # discretion of the RightNet router.
47
47
  attr_accessor :targets
48
48
 
49
49
  # Initialize fields from given arguments
@@ -25,6 +25,8 @@ module RightScale
25
25
  # Dispatching of payload to specified actor
26
26
  class Dispatcher
27
27
 
28
+ include ProtocolVersionMixin
29
+
28
30
  class InvalidRequestType < Exception; end
29
31
  class DuplicateRequest < Exception; end
30
32
 
@@ -85,10 +87,10 @@ module RightScale
85
87
  token = request.token
86
88
  actor, method, idempotent = route(request)
87
89
  received_at = @request_stats.update(method, (token if request.is_a?(Request)))
88
- if dup = duplicate?(request, method, idempotent)
90
+ if (dup = duplicate?(request, method, idempotent))
89
91
  raise DuplicateRequest.new(dup)
90
92
  end
91
- unless result = expired?(request, method)
93
+ unless (result = expired?(request, method))
92
94
  result = perform(request, actor, method, idempotent)
93
95
  end
94
96
  if request.is_a?(Request)
@@ -140,7 +142,7 @@ module RightScale
140
142
  @reject_stats = RightSupport::Stats::Activity.new
141
143
  @request_stats = RightSupport::Stats::Activity.new
142
144
  @dispatch_failure_stats = RightSupport::Stats::Activity.new
143
- @exception_stats = RightSupport::Stats::Exceptions.new(@agent)
145
+ @exception_stats = RightSupport::Stats::Exceptions.new(@agent, @agent.exception_callback)
144
146
  true
145
147
  end
146
148
 
@@ -157,10 +159,10 @@ module RightScale
157
159
  @reject_stats.update("expired (#{method})")
158
160
  Log.info("REJECT EXPIRED <#{request.token}> from #{request.from} TTL #{RightSupport::Stats.elapsed(now - expires_at)} ago")
159
161
  # For agents that do not know about non-delivery, use error result
160
- if request.recv_version < 13
161
- OperationResult.error("Could not deliver request (#{OperationResult::TTL_EXPIRATION})")
162
- else
162
+ if can_handle_non_delivery_result?(request.recv_version)
163
163
  OperationResult.non_delivery(OperationResult::TTL_EXPIRATION)
164
+ else
165
+ OperationResult.error("Could not deliver request (#{OperationResult::TTL_EXPIRATION})")
164
166
  end
165
167
  end
166
168
  end
@@ -176,11 +178,11 @@ module RightScale
176
178
  # (String|nil):: Messaging describing who already serviced request if it is a duplicate, otherwise nil
177
179
  def duplicate?(request, method, idempotent)
178
180
  if !idempotent && @dispatched_cache
179
- if serviced_by = @dispatched_cache.serviced_by(request.token)
181
+ if (serviced_by = @dispatched_cache.serviced_by(request.token))
180
182
  from_retry = ""
181
183
  else
182
184
  from_retry = "retry "
183
- request.tries.each { |t| break if serviced_by = @dispatched_cache.serviced_by(t) }
185
+ request.tries.each { |t| break if (serviced_by = @dispatched_cache.serviced_by(t)) }
184
186
  end
185
187
  if serviced_by
186
188
  @reject_stats.update("#{from_retry}duplicate (#{method})")
@@ -225,9 +227,9 @@ module RightScale
225
227
  def perform(request, actor, method, idempotent)
226
228
  @dispatched_cache.store(request.token) if @dispatched_cache && !idempotent
227
229
  if actor.method(method).arity.abs == 1
228
- actor.__send__(method, request.payload)
230
+ actor.send(method, request.payload)
229
231
  else
230
- actor.__send__(method, request.payload, request)
232
+ actor.send(method, request.payload, request)
231
233
  end
232
234
  rescue Exception => e
233
235
  @dispatch_failure_stats.update("#{request.type}->#{e.class.name}")
@@ -6,7 +6,7 @@ require 'openssl'
6
6
  require 'base64'
7
7
  require 'json'
8
8
 
9
- # A response to a RightNet enrollment request containing the mapper's X509 cert,
9
+ # A response to a RightNet enrollment request containing the router's X509 cert,
10
10
  # the instance (or other) agent's identity cert, and the agent's private key.
11
11
  # Responses are encrypted using a secret key shared between the instance and
12
12
  # the Certifying Authority (aka the RightScale core site) and integrity-protected
@@ -14,35 +14,39 @@ require 'json'
14
14
  module RightScale
15
15
 
16
16
  class EnrollmentResult
17
+
18
+ include ProtocolVersionMixin
19
+
17
20
  # Versions 5 and above use an identical format for the enrollment result
18
21
  SUPPORTED_VERSIONS = 5..AgentConfig.protocol_version
19
22
 
20
23
  class IntegrityFailure < Exception; end
21
24
  class VersionError < Exception; end
22
25
 
23
- attr_reader :r_s_version, :timestamp, :mapper_cert, :id_cert, :id_key
26
+ attr_reader :r_s_version, :timestamp, :router_cert, :id_cert, :id_key
24
27
 
25
28
  # Create a new instance of this class
26
29
  #
27
30
  # === Parameters
28
31
  # timestamp(Time):: Timestamp associated with this result
29
- # mapper_cert(String):: Arbitrary string
32
+ # router_cert(String):: Arbitrary string
30
33
  # id_cert(String):: Arbitrary string
31
34
  # id_key(String):: Arbitrary string
32
35
  # secret(String):: Shared secret with which the result is encrypted
33
36
  #
34
- def initialize(r_s_version, timestamp, mapper_cert, id_cert, id_key, secret)
37
+ def initialize(r_s_version, timestamp, router_cert, id_cert, id_key, secret)
35
38
  @r_s_version = r_s_version
36
39
  @timestamp = timestamp.utc
37
- @mapper_cert = mapper_cert
40
+ @router_cert = router_cert
38
41
  @id_cert = id_cert
39
42
  @id_key = id_key
40
- @serializer = Serializer.new((:json if r_s_version < 12))
43
+ @serializer = Serializer.new(can_handle_msgpack_result?(r_s_version) ? :msgpack : :json)
41
44
 
45
+ cert_name = can_handle_http?(r_s_version) ? 'router_cert' : 'mapper_cert'
42
46
  msg = @serializer.dump({
43
- 'mapper_cert' => @mapper_cert.to_s,
44
- 'id_cert' => @id_cert,
45
- 'id_key' => @id_key
47
+ cert_name => @router_cert.to_s,
48
+ 'id_cert' => @id_cert,
49
+ 'id_key' => @id_key
46
50
  })
47
51
 
48
52
  key = EnrollmentResult.derive_key(secret, @timestamp.to_i.to_s)
@@ -90,7 +94,7 @@ module RightScale
90
94
  # true|false:: Whether the objects' pertinent fields are identical
91
95
  #
92
96
  def ==(o)
93
- self.mapper_cert == o.mapper_cert &&
97
+ self.router_cert == o.router_cert &&
94
98
  self.id_cert == o.id_cert &&
95
99
  self.id_key == o.id_key &&
96
100
  self.timestamp.to_i == o.timestamp.to_i
@@ -164,11 +168,11 @@ module RightScale
164
168
  raise IntegrityFailure.new("MAC mismatch: expected #{my_mac}, got #{mac}") unless (mac == my_mac)
165
169
 
166
170
  msg = serializer.load(plaintext)
167
- mapper_cert = msg['mapper_cert']
171
+ router_cert = msg['router_cert'] || msg['mapper_cert']
168
172
  id_cert = msg['id_cert']
169
173
  id_key = msg['id_key']
170
174
 
171
- self.new(r_s_version, timestamp, mapper_cert, id_cert, id_key, secret)
175
+ self.new(r_s_version, timestamp, router_cert, id_cert, id_key, secret)
172
176
  end
173
177
 
174
178
  protected
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 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
@@ -21,21 +21,22 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
 
23
23
  module RightScale
24
+
24
25
  class Exceptions
25
- # Capability not currently supported
26
- class NotSupported < Exception; end
27
26
 
28
- # Internal application error
29
- class Application < RuntimeError
27
+ # Base exception for use in nesting exceptions
28
+ class NestedException < StandardError
30
29
  attr_reader :nested_exception
30
+
31
+ # Exception message and optional nested exception or string
31
32
  def initialize(message, nested_exception = nil)
32
33
  @nested_exception = nested_exception
33
34
  super(message)
34
35
  end
35
36
  end
36
37
 
37
- # Invalid command or method argument
38
- class Argument < RuntimeError; end
38
+ # Internal application error
39
+ class Application < StandardError; end
39
40
 
40
41
  # Agent command IO error
41
42
  class IO < RuntimeError; end
@@ -43,30 +44,43 @@ module RightScale
43
44
  # Agent compute platform error
44
45
  class PlatformError < StandardError; end
45
46
 
46
- # Cannot connect or lost connection to external resource
47
- class ConnectivityFailure < RuntimeError
48
- attr_reader :nested_exception
47
+ # Terminating service
48
+ class Terminating < RuntimeError; end
49
+
50
+ # Not authorized to make request
51
+ class Unauthorized < NestedException
49
52
  def initialize(message, nested_exception = nil)
50
- @nested_exception = nested_exception
51
- super(message)
53
+ super(message, nested_exception)
54
+ end
55
+ end
56
+
57
+ # 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)
52
61
  end
53
62
  end
54
63
 
55
64
  # Request failed but potentially will succeed if retried
56
- class RetryableError < RuntimeError
57
- attr_reader :nested_exception
65
+ class RetryableError < NestedException
58
66
  def initialize(message, nested_exception = nil)
59
- @nested_exception = nested_exception
60
- super(message)
67
+ super(message, nested_exception)
61
68
  end
62
69
  end
63
70
 
64
71
  # Database query failed
65
- class QueryFailure < RuntimeError
66
- attr_reader :nested_exception
72
+ class QueryFailure < NestedException
67
73
  def initialize(message, nested_exception = nil)
68
- @nested_exception = nested_exception
69
- super(message)
74
+ super(message, nested_exception)
75
+ end
76
+ end
77
+
78
+ # Error internal to specified server
79
+ class InternalServerError < NestedException
80
+ attr_reader :server
81
+ def initialize(message, server, nested_exception = nil)
82
+ @server = server
83
+ super(message, nested_exception)
70
84
  end
71
85
  end
72
86
  end