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.
- data/LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +113 -0
- data/lib/right_agent.rb +59 -0
- data/lib/right_agent/actor.rb +182 -0
- data/lib/right_agent/actor_registry.rb +76 -0
- data/lib/right_agent/actors/agent_manager.rb +232 -0
- data/lib/right_agent/agent.rb +1149 -0
- data/lib/right_agent/agent_config.rb +480 -0
- data/lib/right_agent/agent_identity.rb +210 -0
- data/lib/right_agent/agent_tag_manager.rb +237 -0
- data/lib/right_agent/audit_formatter.rb +107 -0
- data/lib/right_agent/clients.rb +31 -0
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command.rb +30 -0
- data/lib/right_agent/command/agent_manager_commands.rb +150 -0
- data/lib/right_agent/command/command_client.rb +136 -0
- data/lib/right_agent/command/command_constants.rb +33 -0
- data/lib/right_agent/command/command_io.rb +126 -0
- data/lib/right_agent/command/command_parser.rb +87 -0
- data/lib/right_agent/command/command_runner.rb +118 -0
- data/lib/right_agent/command/command_serializer.rb +63 -0
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/console.rb +65 -0
- data/lib/right_agent/core_payload_types.rb +44 -0
- data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
- data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
- data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
- data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
- data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
- data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
- data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
- data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
- data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
- data/lib/right_agent/core_payload_types/login_user.rb +79 -0
- data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
- data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
- data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
- data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
- data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
- data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
- data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
- data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
- data/lib/right_agent/daemonize.rb +35 -0
- data/lib/right_agent/dispatched_cache.rb +109 -0
- data/lib/right_agent/dispatcher.rb +272 -0
- data/lib/right_agent/enrollment_result.rb +221 -0
- data/lib/right_agent/exceptions.rb +87 -0
- data/lib/right_agent/history.rb +145 -0
- data/lib/right_agent/log.rb +460 -0
- data/lib/right_agent/minimal.rb +46 -0
- data/lib/right_agent/monkey_patches.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
- data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
- data/lib/right_agent/multiplexer.rb +102 -0
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/operation_result.rb +300 -0
- data/lib/right_agent/packets.rb +673 -0
- data/lib/right_agent/payload_formatter.rb +104 -0
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/pid_file.rb +159 -0
- data/lib/right_agent/platform.rb +770 -0
- data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
- data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
- data/lib/right_agent/platform/unix/platform.rb +226 -0
- data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
- data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
- data/lib/right_agent/platform/windows/platform.rb +1808 -0
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/retryable_request.rb +195 -0
- data/lib/right_agent/scripts/agent_controller.rb +543 -0
- data/lib/right_agent/scripts/agent_deployer.rb +400 -0
- data/lib/right_agent/scripts/common_parser.rb +160 -0
- data/lib/right_agent/scripts/log_level_manager.rb +192 -0
- data/lib/right_agent/scripts/stats_manager.rb +268 -0
- data/lib/right_agent/scripts/usage.rb +58 -0
- data/lib/right_agent/secure_identity.rb +92 -0
- data/lib/right_agent/security.rb +32 -0
- data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
- data/lib/right_agent/security/certificate.rb +102 -0
- data/lib/right_agent/security/certificate_cache.rb +89 -0
- data/lib/right_agent/security/distinguished_name.rb +56 -0
- data/lib/right_agent/security/encrypted_document.rb +83 -0
- data/lib/right_agent/security/rsa_key_pair.rb +76 -0
- data/lib/right_agent/security/signature.rb +86 -0
- data/lib/right_agent/security/static_certificate_store.rb +85 -0
- data/lib/right_agent/sender.rb +792 -0
- data/lib/right_agent/serialize.rb +29 -0
- data/lib/right_agent/serialize/message_pack.rb +107 -0
- data/lib/right_agent/serialize/secure_serializer.rb +151 -0
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
- data/lib/right_agent/serialize/serializable.rb +151 -0
- data/lib/right_agent/serialize/serializer.rb +159 -0
- data/lib/right_agent/subprocess.rb +38 -0
- data/lib/right_agent/tracer.rb +124 -0
- data/right_agent.gemspec +101 -0
- data/spec/actor_registry_spec.rb +80 -0
- data/spec/actor_spec.rb +162 -0
- data/spec/agent_config_spec.rb +235 -0
- data/spec/agent_identity_spec.rb +78 -0
- data/spec/agent_spec.rb +734 -0
- data/spec/agent_tag_manager_spec.rb +319 -0
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/agent_manager_commands_spec.rb +51 -0
- data/spec/command/command_io_spec.rb +93 -0
- data/spec/command/command_parser_spec.rb +79 -0
- data/spec/command/command_runner_spec.rb +107 -0
- data/spec/command/command_serializer_spec.rb +51 -0
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
- data/spec/core_payload_types/dev_repository_spec.rb +33 -0
- data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
- data/spec/core_payload_types/login_user_spec.rb +102 -0
- data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
- data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
- data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
- data/spec/core_payload_types/spec_helper.rb +23 -0
- data/spec/dispatched_cache_spec.rb +136 -0
- data/spec/dispatcher_spec.rb +324 -0
- data/spec/enrollment_result_spec.rb +53 -0
- data/spec/history_spec.rb +246 -0
- data/spec/log_spec.rb +192 -0
- data/spec/monkey_patches/eventmachine_spec.rb +62 -0
- data/spec/multiplexer_spec.rb +48 -0
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/operation_result_spec.rb +208 -0
- data/spec/packets_spec.rb +461 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/platform/spec_helper.rb +216 -0
- data/spec/platform/unix/darwin/platform_spec.rb +181 -0
- data/spec/platform/unix/linux/platform_spec.rb +540 -0
- data/spec/platform/unix/spec_helper.rb +149 -0
- data/spec/platform/windows/mingw/platform_spec.rb +222 -0
- data/spec/platform/windows/mswin/platform_spec.rb +259 -0
- data/spec/platform/windows/spec_helper.rb +720 -0
- data/spec/retryable_request_spec.rb +306 -0
- data/spec/secure_identity_spec.rb +50 -0
- data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
- data/spec/security/certificate_cache_spec.rb +71 -0
- data/spec/security/certificate_spec.rb +49 -0
- data/spec/security/distinguished_name_spec.rb +46 -0
- data/spec/security/encrypted_document_spec.rb +55 -0
- data/spec/security/rsa_key_pair_spec.rb +55 -0
- data/spec/security/signature_spec.rb +66 -0
- data/spec/security/static_certificate_store_spec.rb +58 -0
- data/spec/sender_spec.rb +1045 -0
- data/spec/serialize/message_pack_spec.rb +131 -0
- data/spec/serialize/secure_serializer_spec.rb +132 -0
- data/spec/serialize/serializable_spec.rb +90 -0
- data/spec/serialize/serializer_spec.rb +197 -0
- data/spec/spec.opts +2 -0
- data/spec/spec.win32.opts +1 -0
- data/spec/spec_helper.rb +130 -0
- data/spec/tracer_spec.rb +114 -0
- 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
|