right_agent 2.0.7-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.
Files changed (176) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +82 -0
  3. data/Rakefile +113 -0
  4. data/lib/right_agent.rb +59 -0
  5. data/lib/right_agent/actor.rb +182 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +232 -0
  8. data/lib/right_agent/agent.rb +1149 -0
  9. data/lib/right_agent/agent_config.rb +480 -0
  10. data/lib/right_agent/agent_identity.rb +210 -0
  11. data/lib/right_agent/agent_tag_manager.rb +237 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/clients.rb +31 -0
  14. data/lib/right_agent/clients/api_client.rb +383 -0
  15. data/lib/right_agent/clients/auth_client.rb +247 -0
  16. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  17. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  18. data/lib/right_agent/clients/right_http_client.rb +279 -0
  19. data/lib/right_agent/clients/router_client.rb +493 -0
  20. data/lib/right_agent/command.rb +30 -0
  21. data/lib/right_agent/command/agent_manager_commands.rb +150 -0
  22. data/lib/right_agent/command/command_client.rb +136 -0
  23. data/lib/right_agent/command/command_constants.rb +33 -0
  24. data/lib/right_agent/command/command_io.rb +126 -0
  25. data/lib/right_agent/command/command_parser.rb +87 -0
  26. data/lib/right_agent/command/command_runner.rb +118 -0
  27. data/lib/right_agent/command/command_serializer.rb +63 -0
  28. data/lib/right_agent/connectivity_checker.rb +179 -0
  29. data/lib/right_agent/console.rb +65 -0
  30. data/lib/right_agent/core_payload_types.rb +44 -0
  31. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  32. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  33. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  34. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  35. data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
  36. data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
  37. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  38. data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
  39. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  40. data/lib/right_agent/core_payload_types/login_user.rb +79 -0
  41. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  42. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
  43. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  44. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  45. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
  46. data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
  47. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  48. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  49. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  50. data/lib/right_agent/daemonize.rb +35 -0
  51. data/lib/right_agent/dispatched_cache.rb +109 -0
  52. data/lib/right_agent/dispatcher.rb +272 -0
  53. data/lib/right_agent/enrollment_result.rb +221 -0
  54. data/lib/right_agent/exceptions.rb +87 -0
  55. data/lib/right_agent/history.rb +145 -0
  56. data/lib/right_agent/log.rb +460 -0
  57. data/lib/right_agent/minimal.rb +46 -0
  58. data/lib/right_agent/monkey_patches.rb +30 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  64. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  65. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  66. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
  67. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  68. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  69. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  70. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  71. data/lib/right_agent/multiplexer.rb +102 -0
  72. data/lib/right_agent/offline_handler.rb +270 -0
  73. data/lib/right_agent/operation_result.rb +300 -0
  74. data/lib/right_agent/packets.rb +673 -0
  75. data/lib/right_agent/payload_formatter.rb +104 -0
  76. data/lib/right_agent/pending_requests.rb +128 -0
  77. data/lib/right_agent/pid_file.rb +159 -0
  78. data/lib/right_agent/platform.rb +770 -0
  79. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  80. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  81. data/lib/right_agent/platform/unix/platform.rb +226 -0
  82. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  83. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  84. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  85. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  86. data/lib/right_agent/retryable_request.rb +195 -0
  87. data/lib/right_agent/scripts/agent_controller.rb +543 -0
  88. data/lib/right_agent/scripts/agent_deployer.rb +400 -0
  89. data/lib/right_agent/scripts/common_parser.rb +160 -0
  90. data/lib/right_agent/scripts/log_level_manager.rb +192 -0
  91. data/lib/right_agent/scripts/stats_manager.rb +268 -0
  92. data/lib/right_agent/scripts/usage.rb +58 -0
  93. data/lib/right_agent/secure_identity.rb +92 -0
  94. data/lib/right_agent/security.rb +32 -0
  95. data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
  96. data/lib/right_agent/security/certificate.rb +102 -0
  97. data/lib/right_agent/security/certificate_cache.rb +89 -0
  98. data/lib/right_agent/security/distinguished_name.rb +56 -0
  99. data/lib/right_agent/security/encrypted_document.rb +83 -0
  100. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  101. data/lib/right_agent/security/signature.rb +86 -0
  102. data/lib/right_agent/security/static_certificate_store.rb +85 -0
  103. data/lib/right_agent/sender.rb +792 -0
  104. data/lib/right_agent/serialize.rb +29 -0
  105. data/lib/right_agent/serialize/message_pack.rb +107 -0
  106. data/lib/right_agent/serialize/secure_serializer.rb +151 -0
  107. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  108. data/lib/right_agent/serialize/serializable.rb +151 -0
  109. data/lib/right_agent/serialize/serializer.rb +159 -0
  110. data/lib/right_agent/subprocess.rb +38 -0
  111. data/lib/right_agent/tracer.rb +124 -0
  112. data/right_agent.gemspec +101 -0
  113. data/spec/actor_registry_spec.rb +80 -0
  114. data/spec/actor_spec.rb +162 -0
  115. data/spec/agent_config_spec.rb +235 -0
  116. data/spec/agent_identity_spec.rb +78 -0
  117. data/spec/agent_spec.rb +734 -0
  118. data/spec/agent_tag_manager_spec.rb +319 -0
  119. data/spec/clients/api_client_spec.rb +423 -0
  120. data/spec/clients/auth_client_spec.rb +272 -0
  121. data/spec/clients/balanced_http_client_spec.rb +576 -0
  122. data/spec/clients/base_retry_client_spec.rb +635 -0
  123. data/spec/clients/router_client_spec.rb +594 -0
  124. data/spec/clients/spec_helper.rb +111 -0
  125. data/spec/command/agent_manager_commands_spec.rb +51 -0
  126. data/spec/command/command_io_spec.rb +93 -0
  127. data/spec/command/command_parser_spec.rb +79 -0
  128. data/spec/command/command_runner_spec.rb +107 -0
  129. data/spec/command/command_serializer_spec.rb +51 -0
  130. data/spec/connectivity_checker_spec.rb +83 -0
  131. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  132. data/spec/core_payload_types/dev_repository_spec.rb +33 -0
  133. data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
  134. data/spec/core_payload_types/login_user_spec.rb +102 -0
  135. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  136. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  137. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  138. data/spec/core_payload_types/spec_helper.rb +23 -0
  139. data/spec/dispatched_cache_spec.rb +136 -0
  140. data/spec/dispatcher_spec.rb +324 -0
  141. data/spec/enrollment_result_spec.rb +53 -0
  142. data/spec/history_spec.rb +246 -0
  143. data/spec/log_spec.rb +192 -0
  144. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  145. data/spec/multiplexer_spec.rb +48 -0
  146. data/spec/offline_handler_spec.rb +340 -0
  147. data/spec/operation_result_spec.rb +208 -0
  148. data/spec/packets_spec.rb +461 -0
  149. data/spec/pending_requests_spec.rb +136 -0
  150. data/spec/platform/spec_helper.rb +216 -0
  151. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  152. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  153. data/spec/platform/unix/spec_helper.rb +149 -0
  154. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  155. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  156. data/spec/platform/windows/spec_helper.rb +720 -0
  157. data/spec/retryable_request_spec.rb +306 -0
  158. data/spec/secure_identity_spec.rb +50 -0
  159. data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
  160. data/spec/security/certificate_cache_spec.rb +71 -0
  161. data/spec/security/certificate_spec.rb +49 -0
  162. data/spec/security/distinguished_name_spec.rb +46 -0
  163. data/spec/security/encrypted_document_spec.rb +55 -0
  164. data/spec/security/rsa_key_pair_spec.rb +55 -0
  165. data/spec/security/signature_spec.rb +66 -0
  166. data/spec/security/static_certificate_store_spec.rb +58 -0
  167. data/spec/sender_spec.rb +1045 -0
  168. data/spec/serialize/message_pack_spec.rb +131 -0
  169. data/spec/serialize/secure_serializer_spec.rb +132 -0
  170. data/spec/serialize/serializable_spec.rb +90 -0
  171. data/spec/serialize/serializer_spec.rb +197 -0
  172. data/spec/spec.opts +2 -0
  173. data/spec/spec.win32.opts +1 -0
  174. data/spec/spec_helper.rb +130 -0
  175. data/spec/tracer_spec.rb +114 -0
  176. metadata +447 -0
@@ -0,0 +1,279 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ module RightScale
25
+
26
+ # HTTP interface to RightNet router and to RightNet services in RightApi
27
+ # It is intended for use by instance agents and infrastructure servers
28
+ # The interface supports sending requests and sending/receiving events
29
+ # Events are received over a WebSocket if possible, otherwise via long-polling
30
+ # Requests to RightNet and RightApi are automatically retried to overcome connectivity failures
31
+ # A status callback is provided so that the user of this client can take action
32
+ # (e.g., queue requests) when connectivity is lost
33
+ # Health checks are sent periodically to try to recover from connectivity failures
34
+ class RightHttpClient
35
+
36
+ include RightSupport::Ruby::EasySingleton
37
+
38
+ # Initialize RightNet client
39
+ # Must be called before any other functions are usable
40
+ #
41
+ # @param [AuthClient] auth_client providing authorization session for HTTP requests
42
+ #
43
+ # @option options [Numeric] :open_timeout maximum wait for connection
44
+ # @option options [Numeric] :request_timeout maximum wait for response
45
+ # @option options [Numeric] :listen_timeout maximum wait for event when long-polling
46
+ # @option options [Numeric] :retry_timeout maximum before stop retrying
47
+ # @option options [Array] :retry_intervals between successive retries
48
+ # @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
49
+ # @option options [Boolean] :long_polling_only never attempt to create a WebSocket, always long-polling instead
50
+ # @option options [Array] :filter_params symbols or strings for names of request parameters
51
+ # whose values are to be hidden when logging
52
+ # @option options [Proc] :exception_callback for unexpected exceptions with following parameters:
53
+ # [Exception] exception raised
54
+ # [Packet, NilClass] packet being processed
55
+ # [Agent, NilClass] agent in which exception occurred
56
+ #
57
+ # @return [TrueClass] always true
58
+ #
59
+ # @raise [ArgumentError] no auth client
60
+ def init(auth_client, options = {})
61
+ raise ArgumentError, "No authorization client provided" unless auth_client.is_a?(AuthClient)
62
+ @status = {}
63
+ callback = lambda { |type, state| update_status(type, state) }
64
+ @auth = auth_client
65
+ @status[:auth] = @auth.status(&callback)
66
+ @router = RouterClient.new(@auth, options)
67
+ @status[:router] = @router.status(&callback)
68
+ if @auth.api_url
69
+ @api = ApiClient.new(@auth, options)
70
+ @status[:api] = @api.status(&callback)
71
+ end
72
+ true
73
+ end
74
+
75
+ # Route a request to a single target or multiple targets with no response expected
76
+ # Persist the request en route to reduce the chance of it being lost at the expense of some
77
+ # additional network overhead
78
+ # Enqueue the request if the target is not currently available
79
+ # Never automatically retry the request if there is the possibility of it being duplicated
80
+ # Set time-to-live to be forever
81
+ #
82
+ # @param [String] type of request as path specifying actor and action
83
+ # @param [Hash, NilClass] payload for request
84
+ # @param [String, Hash, NilClass] target for request, which may be identity of specific
85
+ # target, hash for selecting potentially multiple targets, or nil if routing solely
86
+ # using type; hash may contain:
87
+ # [Array] :tags that must all be associated with a target for it to be selected
88
+ # [Hash] :scope for restricting routing which may contain:
89
+ # [Integer] :account id that agents must be associated with to be included
90
+ # [Integer] :shard id that agents must be in to be included, or if value is
91
+ # Packet::GLOBAL, ones with no shard id
92
+ # [Symbol] :selector for picking from qualified targets: :any or :all;
93
+ # defaults to :any
94
+ # @param [String, NilClass] token uniquely identifying this request;
95
+ # defaults to randomly generated ID
96
+ #
97
+ # @return [NilClass] always nil since there is no expected response to the request
98
+ #
99
+ # @raise [RuntimeError] init was not called
100
+ # @raise [Exceptions::Unauthorized] authorization failed
101
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
102
+ # to it, or it is out of service or too busy to respond
103
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
104
+ # @raise [Exceptions::Terminating] closing client and terminating service
105
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
106
+ def push(type, payload = nil, target = nil, token = nil)
107
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
108
+ client = (@api && @api.support?(type)) ? @api : @router
109
+ client.push(type, payload, target, token)
110
+ end
111
+
112
+ # Route a request to a single target with a response expected
113
+ # Automatically retry the request if a response is not received in a reasonable amount of time
114
+ # or if there is a non-delivery response indicating the target is not currently available
115
+ # Timeout the request if a response is not received in time, typically configured to 30 sec
116
+ # Because of retries there is the possibility of duplicated requests, and these are detected and
117
+ # discarded automatically for non-idempotent actions
118
+ # Allow the request to expire per the agent's configured time-to-live, typically 1 minute
119
+ #
120
+ # @param [String] type of request as path specifying actor and action
121
+ # @param [Hash, NilClass] payload for request
122
+ # @param [String, Hash, NilClass] target for request, which may be identity of specific
123
+ # target, hash for selecting targets of which one is picked randomly, or nil if routing solely
124
+ # using type; hash may contain:
125
+ # [Array] :tags that must all be associated with a target for it to be selected
126
+ # [Hash] :scope for restricting routing which may contain:
127
+ # [Integer] :account id that agents must be associated with to be included
128
+ # @param [String, NilClass] token uniquely identifying this request;
129
+ # defaults to randomly generated ID
130
+ #
131
+ # @return [Result, NilClass] response from request
132
+ #
133
+ # @raise [RuntimeError] init was not called
134
+ # @raise [Exceptions::Unauthorized] authorization failed
135
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
136
+ # to it, or it is out of service or too busy to respond
137
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
138
+ # @raise [Exceptions::Terminating] closing client and terminating service
139
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
140
+ def request(type, payload = nil, target = nil, token = nil)
141
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
142
+ client = (@api && @api.support?(type)) ? @api : @router
143
+ client.request(type, payload, target, token)
144
+ end
145
+
146
+ # Route event
147
+ # Use WebSocket if possible
148
+ # Do not block this request even if in the process of closing
149
+ #
150
+ # @param [Hash] event to send
151
+ # @param [Array, NilClass] routing_keys as strings to assist router in delivering
152
+ # event to interested parties
153
+ #
154
+ # @return [TrueClass] always true
155
+ #
156
+ # @raise [RuntimeError] init was not called
157
+ # @raise [Exceptions::Unauthorized] authorization failed
158
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
159
+ # @raise [Exceptions::Terminating] closing client and terminating service
160
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
161
+ def notify(event, routing_keys)
162
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
163
+ @router.notify(event, routing_keys)
164
+ end
165
+
166
+ # Receive events via an HTTP WebSocket if available, otherwise via an HTTP long-polling
167
+ # This is a blocking call and therefore should be used from a thread different than
168
+ # otherwise used with this object, e.g., EM.defer thread
169
+ #
170
+ # @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
171
+ #
172
+ # @yield [event] required block called each time event received
173
+ # @yieldparam [Object] event received
174
+ #
175
+ # @return [TrueClass] always true, although normally never returns
176
+ #
177
+ # @raise [RuntimeError] init was not called
178
+ # @raise [Exceptions::Unauthorized] authorization failed
179
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
180
+ # @raise [Exceptions::Terminating] closing client and terminating service
181
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
182
+ def listen(routing_keys, &handler)
183
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
184
+ @router.listen(routing_keys, &handler)
185
+ end
186
+
187
+ # Resource href associated with the user of this client
188
+ #
189
+ # @return [String, NilClass] href or nil if unknown
190
+ def self_href
191
+ @api.self_href if @api
192
+ end
193
+
194
+ # Record callback to be notified of status changes
195
+ # Multiple callbacks are supported
196
+ #
197
+ # @yield [type, status] called when status changes (optional)
198
+ # @yieldparam [Symbol] type of client reporting status change: :auth, :api, or :router
199
+ # @yieldparam [Symbol] state of client
200
+ #
201
+ # @return [Hash] status of various clients
202
+ #
203
+ # @raise [RuntimeError] init was not called
204
+ def status(&callback)
205
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
206
+ @status_callbacks = (@status_callbacks || []) << callback if callback
207
+ @status
208
+ end
209
+
210
+ # Set callback for each successful communication excluding health checks
211
+ #
212
+ # @param [Symbol] type of server: :api or :router; defaults to all
213
+ #
214
+ # @yield [] required block executed after successful communication
215
+ #
216
+ # @return [TrueClass] always true
217
+ #
218
+ # @raise [RuntimeError] init was not called
219
+ def communicated(type = nil, &callback)
220
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
221
+ @api.communicated(&callback) if @api && [nil, :api].include?(type)
222
+ @router.communicated(&callback) if @router && [nil, :router].include?(type)
223
+ true
224
+ end
225
+
226
+ # Take any actions necessary to quiesce client interaction in preparation
227
+ # for agent termination but allow any active requests to complete
228
+ # Only router and api clients are closed, not auth client
229
+ #
230
+ # @param [Symbol] scope of close action: :receive for just closing receive side
231
+ # of client, :all for closing both receive and send side; defaults to :all
232
+ #
233
+ # @return [TrueClass] always true
234
+ def close(scope = :all)
235
+ @router.close(scope) if @router
236
+ @api.close(scope) if @api
237
+ true
238
+ end
239
+
240
+ # Current statistics for this client
241
+ #
242
+ # @param [Boolean] reset the statistics after getting the current ones
243
+ #
244
+ # @return [Hash] current statistics with keys "auth client stats", "router client stats",
245
+ # and optionally "api client stats"
246
+ #
247
+ # @raise [RuntimeError] init was not called
248
+ def stats(reset = false)
249
+ raise RuntimeError, "#{self.class.name}#init was not called" unless @auth
250
+ stats = {}
251
+ stats["auth stats"] = @auth.stats(reset)
252
+ stats["router stats"] = @router.stats(reset)
253
+ stats["api stats"] = @api.stats(reset) if @api
254
+ stats
255
+ end
256
+
257
+ protected
258
+
259
+ # Forward status updates via callbacks
260
+ #
261
+ # @param [Symbol] type of client: :auth, :api, or :router
262
+ # @param [Symbol] state of client
263
+ #
264
+ # @return [Hash] status of various clients
265
+ def update_status(type, state)
266
+ @status[type] = state
267
+ @status_callbacks.each do |callback|
268
+ begin
269
+ callback.call(type, state)
270
+ rescue RuntimeError => e
271
+ Log.error("Failed status callback", e)
272
+ end
273
+ end
274
+ @status
275
+ end
276
+
277
+ end # RightHttpClient
278
+
279
+ end # RightScale
@@ -0,0 +1,493 @@
1
+ #--
2
+ # Copyright (c) 2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'faye/websocket'
25
+
26
+ # Monkey patch WebSocket close method so that can specify status code and reason
27
+ # Valid status codes are defined in RFC6455 section 7.4.1
28
+ module Faye
29
+ class WebSocket
30
+ module API
31
+ def close(code = nil, reason = nil)
32
+ @ready_state = CLOSING if @ready_state == OPEN
33
+ @driver.close(reason, code)
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module RightScale
40
+
41
+ # HTTP interface to RightNet router
42
+ class RouterClient < BaseRetryClient
43
+
44
+ # RightNet router API version for use in X-API-Version header
45
+ API_VERSION = "2.0"
46
+
47
+ # Initial interval between attempts to make a WebSocket connection
48
+ CONNECT_INTERVAL = 30
49
+
50
+ # Maximum interval between attempts to make a WebSocket connection
51
+ MAX_CONNECT_INTERVAL = 60 * 60 * 24
52
+
53
+ # Initial interval between attempts to reconnect or long-poll when router is not responding
54
+ RECONNECT_INTERVAL = 2
55
+
56
+ # Maximum interval between attempts to reconnect or long-poll when router is not responding
57
+ MAX_RECONNECT_INTERVAL = 60
58
+
59
+ # Interval between checks for lost WebSocket connection
60
+ CHECK_INTERVAL = 5
61
+
62
+ # Backoff factor for connect and reconnect intervals
63
+ BACKOFF_FACTOR = 2
64
+
65
+ # WebSocket close status codes
66
+ NORMAL_CLOSE = 1000
67
+ SHUTDOWN_CLOSE = 1001
68
+ PROTOCOL_ERROR_CLOSE = 1002
69
+ UNEXPECTED_ERROR_CLOSE = 1011
70
+
71
+ # Default time to wait for an event or to ping WebSocket
72
+ DEFAULT_LISTEN_TIMEOUT = 60
73
+
74
+ # Create RightNet router client
75
+ #
76
+ # @param [AuthClient] auth_client providing authorization session for HTTP requests
77
+ #
78
+ # @option options [Numeric] :open_timeout maximum wait for connection; defaults to DEFAULT_OPEN_TIMEOUT
79
+ # @option options [Numeric] :request_timeout maximum wait for response; defaults to DEFAULT_REQUEST_TIMEOUT
80
+ # @option options [Numeric] :listen_timeout maximum wait for event; defaults to DEFAULT_POLL_TIMEOUT
81
+ # @option options [Boolean] :long_polling_only never attempt to create a WebSocket, always long-polling instead
82
+ # @option options [Numeric] :retry_timeout maximum before stop retrying; defaults to DEFAULT_RETRY_TIMEOUT
83
+ # @option options [Array] :retry_intervals between successive retries; defaults to DEFAULT_RETRY_INTERVALS
84
+ # @option options [Boolean] :retry_enabled for requests that fail to connect or that return a retry result
85
+ # @option options [Numeric] :reconnect_interval for reconnect attempts after lose connectivity
86
+ # @option options [Proc] :exception_callback for unexpected exceptions
87
+ #
88
+ # @raise [ArgumentError] auth client does not support this client type
89
+ def initialize(auth_client, options)
90
+ init(:router, auth_client, options.merge(:server_name => "RightNet", :api_version => API_VERSION))
91
+ @options[:listen_timeout] ||= DEFAULT_LISTEN_TIMEOUT
92
+ end
93
+
94
+ # Route a request to a single target or multiple targets with no response expected
95
+ # Persist the request en route to reduce the chance of it being lost at the expense of some
96
+ # additional network overhead
97
+ # Enqueue the request if the target is not currently available
98
+ # Never automatically retry the request if there is the possibility of it being duplicated
99
+ # Set time-to-live to be forever
100
+ #
101
+ # @param [String] type of request as path specifying actor and action
102
+ # @param [Hash, NilClass] payload for request
103
+ # @param [Hash, NilClass] target for request, which may be a specific agent (using :agent_id),
104
+ # potentially multiple targets (using :tags, :scope, :selector), or nil to route solely
105
+ # using type:
106
+ # [String] :agent_id serialized identity of specific target
107
+ # [Array] :tags that must all be associated with a target for it to be selected
108
+ # [Hash] :scope for restricting routing which may contain:
109
+ # [Integer] :account id that agents must be associated with to be included
110
+ # [Integer] :shard id that agents must be in to be included, or if value is
111
+ # Packet::GLOBAL, ones with no shard id
112
+ # [Symbol] :selector for picking from qualified targets: :any or :all;
113
+ # defaults to :any
114
+ # @param [String, NilClass] token uniquely identifying this request;
115
+ # defaults to randomly generated ID
116
+ #
117
+ # @return [NilClass] always nil since there is no expected response to the request
118
+ #
119
+ # @raise [Exceptions::Unauthorized] authorization failed
120
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
121
+ # to it, or it is out of service or too busy to respond
122
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
123
+ # @raise [Exceptions::Terminating] closing client and terminating service
124
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
125
+ def push(type, payload, target, token = nil)
126
+ params = {
127
+ :type => type,
128
+ :payload => payload,
129
+ :target => target }
130
+ make_request(:post, "/push", params, type.split("/")[2], token)
131
+ end
132
+
133
+ # Route a request to a single target with a response expected
134
+ # Automatically retry the request if a response is not received in a reasonable amount of time
135
+ # or if there is a non-delivery response indicating the target is not currently available
136
+ # Timeout the request if a response is not received in time, typically configured to 30 sec
137
+ # Because of retries there is the possibility of duplicated requests, and these are detected and
138
+ # discarded automatically for non-idempotent actions
139
+ # Allow the request to expire per the agent's configured time-to-live, typically 1 minute
140
+ #
141
+ # @param [String] type of request as path specifying actor and action
142
+ # @param [Hash, NilClass] payload for request
143
+ # @param [Hash, NilClass] target for request, which may be a specific agent (using :agent_id),
144
+ # one chosen randomly from potentially multiple targets (using :tags, :scope), or nil to
145
+ # route solely using type:
146
+ # [String] :agent_id serialized identity of specific target
147
+ # [Array] :tags that must all be associated with a target for it to be selected
148
+ # [Hash] :scope for restricting routing which may contain:
149
+ # [Integer] :account id that agents must be associated with to be included
150
+ # @param [String, NilClass] token uniquely identifying this request;
151
+ # defaults to randomly generated ID
152
+ #
153
+ # @return [Result, NilClass] response from request
154
+ #
155
+ # @raise [Exceptions::Unauthorized] authorization failed
156
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
157
+ # to it, or it is out of service or too busy to respond
158
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
159
+ # @raise [Exceptions::Terminating] closing client and terminating service
160
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
161
+ def request(type, payload, target, token = nil)
162
+ params = {
163
+ :type => type,
164
+ :payload => payload,
165
+ :target => target }
166
+ make_request(:post, "/request", params, type.split("/")[2], token)
167
+ end
168
+
169
+ # Route event
170
+ # Use WebSocket if possible
171
+ # Do not block this request even if in the process of closing since used for request responses
172
+ #
173
+ # @param [Hash] event to send
174
+ # @param [Array, NilClass] routing_keys as strings to assist router in delivering
175
+ # event to interested parties
176
+ #
177
+ # @return [TrueClass] always true
178
+ #
179
+ # @raise [Exceptions::Unauthorized] authorization failed
180
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
181
+ # to it, or it is out of service or too busy to respond
182
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
183
+ # @raise [Exceptions::Terminating] closing client and terminating service
184
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
185
+ def notify(event, routing_keys)
186
+ event[:uuid] ||= RightSupport::Data::UUID.generate
187
+ event[:version] ||= AgentConfig.protocol_version
188
+ params = {:event => event}
189
+ params[:routing_keys] = routing_keys if routing_keys
190
+ if @websocket
191
+ path = event[:path] ? " #{event[:path]}" : ""
192
+ to = routing_keys ? " to #{routing_keys.inspect}" : ""
193
+ Log.info("Sending EVENT <#{event[:uuid]}> #{event[:type]}#{path}#{to}")
194
+ @websocket.send(JSON.dump(params))
195
+ else
196
+ make_request(:post, "/notify", params, "notify", event[:uuid], :filter_params => ["event"])
197
+ end
198
+ true
199
+ end
200
+
201
+ # Receive events via an HTTP WebSocket if available, otherwise via an HTTP long-polling
202
+ # This is a blocking call and therefore should be used from a thread different than
203
+ # otherwise used with this object, e.g., EM.defer thread
204
+ #
205
+ # @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
206
+ #
207
+ # @yield [event] required block called each time event received
208
+ # @yieldparam [Hash] event received
209
+ #
210
+ # @return [TrueClass] always true, although only returns when closing
211
+ #
212
+ # @raise [ArgumentError] block missing
213
+ # @raise [Exceptions::Unauthorized] authorization failed
214
+ # @raise [Exceptions::ConnectivityFailure] cannot connect to server, lost connection
215
+ # to it, or it is out of service or too busy to respond
216
+ # @raise [Exceptions::RetryableError] request failed but if retried may succeed
217
+ # @raise [Exceptions::Terminating] closing client and terminating service
218
+ # @raise [Exceptions::InternalServerError] internal error in server being accessed
219
+ def listen(routing_keys, &handler)
220
+ raise ArgumentError, "Block missing" unless block_given?
221
+
222
+ @connect_interval = CONNECT_INTERVAL
223
+ @last_connect_time = Time.now - @connect_interval
224
+ @reconnect_interval = RECONNECT_INTERVAL
225
+
226
+ uuids = nil
227
+ retries = 0
228
+ until [:closing, :closed].include?(state) do
229
+ if @websocket
230
+ @connect_interval = CONNECT_INTERVAL
231
+ @reconnect_interval = RECONNECT_INTERVAL
232
+ sleep(CHECK_INTERVAL)
233
+ next
234
+ elsif retry_connect?
235
+ @last_connect_time = Time.now
236
+ @close_code = @close_reason = nil
237
+ @stats["reconnects"].update("websocket") if (retries += 1) > 1
238
+ next if try_connect(routing_keys, &handler)
239
+ end
240
+
241
+ # Resort to long-polling if WebSocket not usable
242
+ uuids = try_long_poll(routing_keys, uuids, &handler) if @websocket.nil?
243
+ end
244
+ true
245
+ end
246
+
247
+ # Take any actions necessary to quiesce client interaction in preparation
248
+ # for agent termination but allow any active requests to complete
249
+ #
250
+ # @param [Symbol] scope of close action: :receive for just receive side
251
+ # of client, :all for both receive and send side; defaults to :all
252
+ #
253
+ # @return [TrueClass] always true
254
+ def close(scope = :all)
255
+ super
256
+ @websocket.close(SHUTDOWN_CLOSE, "Agent terminating") if @websocket
257
+ end
258
+
259
+ # Current statistics for this client
260
+ #
261
+ # @param [Boolean] reset the statistics after getting the current ones
262
+ #
263
+ # @return [Hash] current statistics
264
+ # [Hash, NilClass] "events" Activity stats or nil if none
265
+ # [Hash, NilClass] "reconnects" Activity stats or nil if none
266
+ # [Hash, NilClass] "request failures" Activity stats or nil if none
267
+ # [Hash, NilClass] "request sent" Activity stats or nil if none
268
+ # [Float, NilClass] "response time" average number of seconds to respond to a request or nil if none
269
+ # [Hash, NilClass] "exceptions" Exceptions stats or nil if none
270
+ def stats(reset = false)
271
+ events = @stats["events"].all
272
+ stats = super(reset)
273
+ stats["events"] = events
274
+ stats
275
+ end
276
+
277
+ protected
278
+
279
+ # Reset API interface statistics
280
+ #
281
+ # @return [TrueClass] always true
282
+ def reset_stats
283
+ super
284
+ @stats["events"] = RightSupport::Stats::Activity.new
285
+ true
286
+ end
287
+
288
+ # Determine whether should retry creation of WebSocket connection
289
+ # Should only retry if (1) WebSocket is enabled, (2) there is none currently,
290
+ # (3) previous closure was for acceptable reasons (normal, router shutdown,
291
+ # router inaccessible), or (4) enough time has elapsed to make another attempt
292
+ #
293
+ # @return [Boolean] true if should try, otherwise false
294
+ def retry_connect?
295
+ unless @options[:long_polling_only]
296
+ if @websocket.nil?
297
+ if (Time.now - @last_connect_time) > @connect_interval
298
+ true
299
+ elsif [NORMAL_CLOSE, SHUTDOWN_CLOSE].include?(@close_code)
300
+ true
301
+ elsif router_not_responding?
302
+ true
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ # Try to create WebSocket connection
309
+ #
310
+ # @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
311
+ #
312
+ # @yield [event] required block called each time event received
313
+ # @yieldparam [Hash] event received
314
+ #
315
+ # @return [Boolean] true if should not try long-polling, otherwise false
316
+ def try_connect(routing_keys, &handler)
317
+ begin
318
+ connect(routing_keys, &handler)
319
+ CHECK_INTERVAL.times do
320
+ # Allow for possibility of asynchronous handshake failure resulting in close
321
+ if @websocket.nil?
322
+ if router_not_responding?
323
+ sleep(backoff_reconnect_interval)
324
+ else
325
+ backoff_connect_interval
326
+ end
327
+ break
328
+ end
329
+ sleep(1)
330
+ end
331
+ @websocket.nil?
332
+ rescue Exception => e
333
+ Log.error("Failed creating WebSocket", e)
334
+ @stats["exceptions"].track("websocket", e)
335
+ backoff_connect_interval
336
+ false
337
+ end
338
+ end
339
+
340
+ # Connect to RightNet router using WebSocket for receiving events
341
+ #
342
+ # @param [Array, NilClass] routing_keys as strings to assist router in delivering
343
+ # event to interested parties
344
+ #
345
+ # @yield [event] required block called when event received
346
+ # @yieldparam [Object] event received
347
+ # @yieldreturn [Hash, NilClass] event this is response to event received,
348
+ # or nil meaning no response
349
+ #
350
+ # @return [Faye::WebSocket] WebSocket created
351
+ #
352
+ # @raise [ArgumentError] block missing
353
+ def connect(routing_keys, &handler)
354
+ raise ArgumentError, "Block missing" unless block_given?
355
+
356
+ options = {
357
+ # Limit to .auth_header here (rather than .headers) to keep WebSockets happy
358
+ :headers => {"X-API-Version" => API_VERSION}.merge(@auth_client.auth_header),
359
+ :ping => @options[:listen_timeout] }
360
+ url = URI.parse(@auth_client.router_url)
361
+ url.scheme = url.scheme == "https" ? "wss" : "ws"
362
+ url.path = url.path + "/connect"
363
+ url.query = routing_keys.map { |k| "routing_keys[]=#{CGI.escape(k)}" }.join("&") if routing_keys && routing_keys.any?
364
+ Log.info("Creating WebSocket connection to #{url.to_s}")
365
+ @websocket = Faye::WebSocket::Client.new(url.to_s, protocols = nil, options)
366
+
367
+ @websocket.onerror = lambda do |event|
368
+ Log.error("WebSocket error (#{event.data})") if event.data
369
+ end
370
+
371
+ @websocket.onclose = lambda do |event|
372
+ begin
373
+ @close_code = event.code.to_i
374
+ @close_reason = event.reason
375
+ msg = "WebSocket closed (#{event.code}"
376
+ msg << ((event.reason.nil? || event.reason.empty?) ? ")" : ": #{event.reason})")
377
+ Log.info(msg)
378
+ rescue Exception => e
379
+ Log.error("Failed closing WebSocket", e, :trace)
380
+ @stats["exceptions"].track("event", e)
381
+ end
382
+ @websocket = nil
383
+ end
384
+
385
+ @websocket.onmessage = lambda do |event|
386
+ begin
387
+ # Receive event
388
+ event = SerializationHelper.symbolize_keys(JSON.load(event.data))
389
+ Log.info("Received EVENT <#{event[:uuid]}> #{event[:type]} #{event[:path]} from #{event[:from]}")
390
+ @stats["events"].update("#{event[:type]} #{event[:path]}")
391
+
392
+ # Acknowledge event
393
+ @websocket.send(JSON.dump({:ack => event[:uuid]}))
394
+
395
+ # Send response, if any
396
+ if (result = handler.call(event))
397
+ Log.info("Sending EVENT <#{result[:uuid]}> #{result[:type]} #{result[:path]} to #{result[:from]}")
398
+ @websocket.send(JSON.dump({:event => result, :routing_keys => [event[:from]]}))
399
+ end
400
+ rescue Exception => e
401
+ Log.error("Failed handling WebSocket event", e, :trace)
402
+ @stats["exceptions"].track("event", e)
403
+ end
404
+ end
405
+
406
+ @websocket
407
+ end
408
+
409
+ # Try to make long-polling request to receive events
410
+ #
411
+ # @param [Array, NilClass] routing_keys for event sources of interest with nil meaning all
412
+ # @param [Array, NilClass] uuids for events received on previous poll
413
+ #
414
+ # @yield [event] required block called each time event received
415
+ # @yieldparam [Hash] event received
416
+ #
417
+ # @return [Array, NilClass] UUIDs of events received, or nil if none
418
+ def try_long_poll(routing_keys, uuids, &handler)
419
+ result = nil
420
+ begin
421
+ result = long_poll(routing_keys, uuids, &handler)
422
+ @reconnect_interval = RECONNECT_INTERVAL
423
+ rescue Exceptions::Unauthorized, Exceptions::ConnectivityFailure, Exceptions::RetryableError => e
424
+ Log.error("Failed long-polling", e, :no_trace)
425
+ sleep(backoff_reconnect_interval)
426
+ rescue Exception => e
427
+ Log.error("Failed long-polling", e, :trace)
428
+ @stats["exceptions"].track("long-polling", e)
429
+ sleep(backoff_reconnect_interval)
430
+ end
431
+ result
432
+ end
433
+
434
+ # Make long-polling request to receive one or more events
435
+ # Limit logging unless in debug mode
436
+ #
437
+ # @param [Array, NilClass] routing_keys as strings to assist router in delivering
438
+ # event to interested parties
439
+ # @param [Array, NilClass] ack UUIDs for events received on previous poll
440
+ #
441
+ # @yield [event] required block called for each event received
442
+ # @yieldparam [Object] event received
443
+ #
444
+ # @return [Array, NilClass] UUIDs of events received, or nil if none
445
+ #
446
+ # @raise [ArgumentError] block missing
447
+ def long_poll(routing_keys, ack, &handler)
448
+ raise ArgumentError, "Block missing" unless block_given?
449
+
450
+ params = {
451
+ :wait_time => @options[:listen_timeout] - 5,
452
+ :timestamp => Time.now.to_f }
453
+ params[:routing_keys] = routing_keys if routing_keys
454
+ params[:ack] = ack if ack && ack.any?
455
+
456
+ uuids = []
457
+ if (events = make_request(:get, "/listen", params, "listen", nil, :log_level => :debug,
458
+ :request_timeout => @options[:listen_timeout]))
459
+ events.each do |event|
460
+ event = SerializationHelper.symbolize_keys(event)
461
+ Log.info("Received EVENT <#{event[:uuid]}> #{event[:type]} #{event[:path]} from #{event[:from]}")
462
+ @stats["events"].update("#{event[:type]} #{event[:path]}")
463
+ uuids << event[:uuid]
464
+ handler.call(event)
465
+ end
466
+ end
467
+ uuids if uuids.any?
468
+ end
469
+
470
+ # Exponentially increase WebSocket connect attempt interval after failing to connect
471
+ #
472
+ # @return [Integer] new interval
473
+ def backoff_connect_interval
474
+ @connect_interval = [@connect_interval * BACKOFF_FACTOR, MAX_CONNECT_INTERVAL].min
475
+ end
476
+
477
+ # Exponentially increase reconnect attempt interval when router not responding
478
+ #
479
+ # @return [Integer] new interval
480
+ def backoff_reconnect_interval
481
+ @reconnect_interval = [@reconnect_interval * BACKOFF_FACTOR, MAX_RECONNECT_INTERVAL].min
482
+ end
483
+
484
+ # Determine whether WebSocket attempts are failing because router not responding
485
+ #
486
+ # @return [Boolean] true if router not responding, otherwise false
487
+ def router_not_responding?
488
+ @close_code == PROTOCOL_ERROR_CLOSE && @close_reason =~ /502|503/
489
+ end
490
+
491
+ end # RouterClient
492
+
493
+ end # RightScale