right_agent 2.0.7-x86-mingw32

Sign up to get free protection for your applications and to get access to all the features.
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,76 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ class ActorRegistry
26
+
27
+ # (Hash) Actors that are registered; key is actor prefix and value is actor
28
+ attr_reader :actors
29
+
30
+ # Initialize registry
31
+ def initialize
32
+ @actors = {}
33
+ end
34
+
35
+ # Register as an actor
36
+ #
37
+ # === Parameters
38
+ # actor(Actor):: Actor to be registered
39
+ # prefix(String):: Prefix used in request to identify actor
40
+ #
41
+ # === Return
42
+ # (Actor):: Actor registered
43
+ #
44
+ # === Raises
45
+ # ArgumentError if actor is not an Actor
46
+ def register(actor, prefix)
47
+ raise ArgumentError, "#{actor.inspect} is not a RightScale::Actor subclass instance" unless RightScale::Actor === actor
48
+ log_msg = "[actor] #{actor.class.to_s}"
49
+ log_msg += ", prefix #{prefix}" if prefix && !prefix.empty?
50
+ Log.info(log_msg)
51
+ prefix ||= actor.class.default_prefix
52
+ @actors[prefix.to_s] = actor
53
+ end
54
+
55
+ # Retrieve services provided by all of the registered actors
56
+ #
57
+ # === Return
58
+ # (Array):: List of unique /prefix/method path strings
59
+ def services
60
+ @actors.map { |prefix, actor| actor.class.provides_for(prefix) }.flatten.uniq
61
+ end
62
+
63
+ # Retrieve actor by prefix
64
+ #
65
+ # === Parameters
66
+ # prefix(String):: Prefix identifying actor
67
+ #
68
+ # === Return
69
+ # (Actor|nil):: Retrieved actor, or nil if unknown
70
+ def actor_for(prefix)
71
+ @actors[prefix]
72
+ end
73
+
74
+ end # ActorRegistry
75
+
76
+ end # RightScale
@@ -0,0 +1,232 @@
1
+ #
2
+ # Copyright (c) 2009-2011 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
+ require 'socket'
24
+
25
+ # Generic actor for all agents to provide basic agent management services
26
+ class AgentManager
27
+
28
+ include RightScale::Actor
29
+ include RightScale::OperationResultHelper
30
+
31
+ on_exception { |meth, deliverable, e| RightScale::ExceptionMailer.deliver_notification(meth, deliverable, e) }
32
+
33
+ expose_idempotent :ping, :stats, :profile, :set_log_level, :connect, :disconnect, :connect_failed
34
+ expose_non_idempotent :execute, :terminate
35
+
36
+ # Valid log levels
37
+ LEVELS = [:debug, :info, :warn, :error, :fatal]
38
+
39
+ # Initialize broker
40
+ #
41
+ # === Parameters
42
+ # agent(RightScale::Agent):: This agent
43
+ def initialize(agent)
44
+ @agent = agent
45
+ end
46
+
47
+ # Always return success along with identity, protocol version, and broker information
48
+ # Used for troubleshooting
49
+ #
50
+ # === Return
51
+ # (RightScale::OperationResult):: Always returns success
52
+ def ping(_)
53
+ success_result(:identity => @agent.options[:identity],
54
+ :hostname => Socket.gethostname,
55
+ :version => RightScale::AgentConfig.protocol_version,
56
+ :client => @agent.client.status,
57
+ :time => Time.now.to_i)
58
+ end
59
+
60
+ # Retrieve statistics about agent operation
61
+ #
62
+ # === Parameters:
63
+ # options(Hash):: Request options:
64
+ # :reset(Boolean):: Whether to reset the statistics after getting the current ones
65
+ #
66
+ # === Return
67
+ # (RightScale::OperationResult):: Always returns success
68
+ def stats(options)
69
+ @agent.stats(RightScale::SerializationHelper.symbolize_keys(options || {}))
70
+ end
71
+
72
+ # Profile memory use
73
+ #
74
+ # === Parameters
75
+ # options(Hash):: Request options
76
+ # :start(Boolean):: Whether to start profiling
77
+ # :stats(Boolean):: Whether to display profile statistics to stdout
78
+ # :reset(Boolean):: Whether to reset profile statistics when after displaying them
79
+ # :stop(Boolean):: Whether to stop profiling
80
+ #
81
+ # === Return
82
+ # (OperationResult):: Empty success result or error result with message
83
+ def profile(options)
84
+ return error_result("The memprof gem is not available for profiling. Please install memprof 0.3 manually") unless require_succeeds?('memprof')
85
+
86
+ options = RightScale::SerializationHelper.symbolize_keys(options || {})
87
+ if options[:start]
88
+ RightScale::Log.info("[profile] Start")
89
+ $stderr.puts "[profile] Start at #{Time.now}"
90
+ Memprof.start
91
+ @profiling = true
92
+ end
93
+
94
+ if options[:stats]
95
+ return error_result("Profiling has not yet been started") unless @profiling
96
+ RightScale::Log.info("[profile] GC start")
97
+ $stderr.puts "[profile] GC at #{Time.now}"
98
+ GC.start
99
+ RightScale::Log.info("[profile] Display stats to stderr")
100
+ $stderr.puts "[profile] Stats at #{Time.now}#{options[:reset] ? ' with reset' : ''}"
101
+ options[:reset] ? Memprof.stats! : Memprof.stats
102
+ end
103
+
104
+ if options[:stop]
105
+ return error_result("Profiling has not yet been started") unless @profiling
106
+ RightScale::Log.info("[profile] Stop")
107
+ $stderr.puts "[profile] Stop at #{Time.now}"
108
+ Memprof.stop
109
+ @profiling = false
110
+ end
111
+ success_result
112
+ end
113
+
114
+ # Change log level of agent
115
+ #
116
+ # === Parameter
117
+ # level(Symbol|String):: One of :debug, :info, :warn, :error, :fatal
118
+ #
119
+ # === Return
120
+ # (RightScale::OperationResult):: Success if level was changed, error otherwise
121
+ def set_log_level(level)
122
+ level = level.to_sym if level.is_a?(String)
123
+ if LEVELS.include?(level)
124
+ RightScale::Log.level = level
125
+ success_result
126
+ else
127
+ error_result("Invalid log level '#{level.to_s}'")
128
+ end
129
+ end
130
+
131
+ # Eval given code in context of agent
132
+ #
133
+ # === Parameter
134
+ # code(String):: Code to be eval'd
135
+ #
136
+ # === Return
137
+ # (RightScale::OperationResult):: Success with result if code didn't raise an exception,
138
+ # otherwise failure with exception message
139
+ def execute(code)
140
+ begin
141
+ success_result(self.instance_eval(code))
142
+ rescue Exception => e
143
+ error_result(e.message + " at\n " + e.backtrace.join("\n "))
144
+ end
145
+ end
146
+
147
+ # Connect agent to an additional broker or reconnect it if connection has failed
148
+ # Assumes agent already has credentials on this broker and identity queue exists
149
+ #
150
+ # === Parameters
151
+ # options(Hash):: Connect options:
152
+ # :host(String):: Host name of broker
153
+ # :port(Integer):: Port number of broker
154
+ # :id(Integer):: Small unique id associated with this broker for use in forming alias
155
+ # :priority(Integer|nil):: Priority position of this broker in list for use
156
+ # by this agent with nil meaning add to end of list
157
+ # :force(Boolean):: Reconnect even if already connected
158
+ #
159
+ # === Return
160
+ # res(RightScale::OperationResult):: Success unless exception is raised
161
+ def connect(options)
162
+ options = RightScale::SerializationHelper.symbolize_keys(options)
163
+ res = success_result
164
+ begin
165
+ if error = @agent.connect(options[:host], options[:port], options[:id], options[:priority], options[:force])
166
+ res = error_result(error)
167
+ end
168
+ rescue Exception => e
169
+ res = error_result("Failed to connect to broker", e)
170
+ end
171
+ res
172
+ end
173
+
174
+ # Disconnect agent from a broker
175
+ #
176
+ # === Parameters
177
+ # options(Hash):: Connect options:
178
+ # :host(String):: Host name of broker
179
+ # :port(Integer):: Port number of broker
180
+ # :remove(Boolean):: Remove broker from configuration in addition to disconnecting it
181
+ #
182
+ # === Return
183
+ # res(RightScale::OperationResult):: Success unless exception is raised
184
+ def disconnect(options)
185
+ options = RightScale::SerializationHelper.symbolize_keys(options)
186
+ res = success_result
187
+ begin
188
+ if error = @agent.disconnect(options[:host], options[:port], options[:remove])
189
+ res = error_result(error)
190
+ end
191
+ rescue Exception => e
192
+ res = error_result("Failed to disconnect from broker", e)
193
+ end
194
+ res
195
+ end
196
+
197
+ # Declare one or more broker connections unusable because connection setup has failed
198
+ #
199
+ # === Parameters
200
+ # options(Hash):: Failure options:
201
+ # :brokers(Array):: Identity of brokers
202
+ #
203
+ # === Return
204
+ # res(RightScale::OperationResult):: Success unless exception is raised
205
+ def connect_failed(options)
206
+ options = RightScale::SerializationHelper.symbolize_keys(options)
207
+ res = success_result
208
+ begin
209
+ if error = @agent.connect_failed(options[:brokers])
210
+ res = error_result(error)
211
+ end
212
+ rescue Exception => e
213
+ res = error_result("Failed to notify agent that brokers #{options[:brokers]} are unusable", e)
214
+ end
215
+ res
216
+ end
217
+
218
+ # Terminate self
219
+ #
220
+ # === Parameters
221
+ # options(Hash):: Terminate options
222
+ #
223
+ # === Return
224
+ # true
225
+ def terminate(options = nil)
226
+ RightScale::CommandRunner.stop
227
+ # Delay terminate a bit to give request message a chance to be ack'd and reply to be sent
228
+ EM.add_timer(1) { @agent.terminate }
229
+ true
230
+ end
231
+
232
+ end
@@ -0,0 +1,1149 @@
1
+ #
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'socket'
24
+
25
+ module RightScale
26
+
27
+ # Agent for receiving requests via RightNet and acting upon them
28
+ # by dispatching to a registered actor to perform
29
+ # See load_actors for details on how the agent specific environment is loaded
30
+ # Operates in either HTTP or AMQP mode for RightNet communication
31
+ class Agent
32
+
33
+ include ConsoleHelper
34
+ include DaemonizeHelper
35
+
36
+ # (String) Identity of this agent
37
+ attr_reader :identity
38
+
39
+ # (Hash) Configuration options applied to the agent
40
+ attr_reader :options
41
+
42
+ # (Hash) Dispatcher for each queue for messages received via AMQP
43
+ attr_reader :dispatchers
44
+
45
+ # (ActorRegistry) Registry for this agents actors
46
+ attr_reader :registry
47
+
48
+ # (RightHttpClient|RightAMQP::HABrokerClient) Client for accessing RightNet/RightApi
49
+ attr_reader :client
50
+
51
+ # (Symbol) RightNet communication mode: :http or :amqp
52
+ attr_reader :mode
53
+
54
+ # (String) Name of AMQP queue to which requests are to be published
55
+ attr_reader :request_queue
56
+
57
+ # (Array) Tag strings published by agent
58
+ attr_accessor :tags
59
+
60
+ # (Proc) Callback procedure for exceptions
61
+ attr_reader :exception_callback
62
+
63
+ # Default option settings for the agent
64
+ DEFAULT_OPTIONS = {
65
+ :user => 'agent',
66
+ :pass => 'testing',
67
+ :vhost => '/right_net',
68
+ :secure => true,
69
+ :log_level => :info,
70
+ :daemonize => false,
71
+ :console => false,
72
+ :root_dir => Dir.pwd,
73
+ :mode => :amqp,
74
+ :time_to_live => 0,
75
+ :retry_interval => nil,
76
+ :retry_timeout => nil,
77
+ :connect_timeout => 60,
78
+ :reconnect_interval => 60,
79
+ :offline_queueing => false,
80
+ :ping_interval => 0,
81
+ :check_interval => 5 * 60,
82
+ :grace_timeout => 30,
83
+ :prefetch => 1,
84
+ :heartbeat => 0
85
+ }
86
+
87
+ # Maximum abnormal termination delay for slowing crash cycling
88
+ MAX_ABNORMAL_TERMINATE_DELAY = 60 * 60
89
+
90
+ # Block to be activated when finish terminating
91
+ TERMINATE_BLOCK = lambda { EM.stop if EM.reactor_running? }
92
+
93
+ # Initializes a new agent and establishes an HTTP or AMQP RightNet connection
94
+ # This must be used inside an EM.run block unless the EventMachine reactor
95
+ # was already started by the server that this application runs on
96
+ #
97
+ # === Parameters
98
+ # opts(Hash):: Configuration options:
99
+ # :identity(String):: Identity of this agent; no default
100
+ # :agent_name(String):: Local name for this agent
101
+ # :root_dir(String):: Application root for this agent containing subdirectories actors, certs, and init;
102
+ # defaults to current working directory
103
+ # :pid_dir(String):: Path to the directory where the agent stores its process id file (only if daemonized);
104
+ # defaults to the current working directory
105
+ # :log_dir(String):: Log directory path; defaults to the platform specific log directory
106
+ # :log_level(Symbol):: The verbosity of logging -- :debug, :info, :warn, :error or :fatal
107
+ # :actors(Array):: List of actors to load
108
+ # :console(Boolean):: true indicates to start interactive console
109
+ # :daemonize(Boolean):: true indicates to daemonize
110
+ # :retry_interval(Numeric):: Number of seconds between request retries
111
+ # :retry_timeout(Numeric):: Maximum number of seconds to retry request before give up
112
+ # :time_to_live(Integer):: Number of seconds before a request expires and is to be ignored
113
+ # by the receiver, 0 means never expire; defaults to 0
114
+ # :connect_timeout(Integer):: Number of seconds to wait for an AMQP broker connection to be established
115
+ # :reconnect_interval(Integer):: Number of seconds between AMQP broker reconnect attempts
116
+ # :offline_queueing(Boolean):: Whether to queue request if currently not connected to RightNet,
117
+ # also requires agent invocation of Sender initialize_offline_queue and start_offline_queue methods,
118
+ # as well as enable_offline_mode and disable_offline_mode as connection status changes
119
+ # :ping_interval(Integer):: Minimum number of seconds since last message receipt to ping the RightNet
120
+ # router to check connectivity; defaults to 0 meaning do not ping
121
+ # :check_interval(Integer):: Number of seconds between publishing stats and checking for AMQP broker
122
+ # connections that failed during agent launch and then attempting to reconnect
123
+ # :heartbeat(Integer):: Number of seconds between AMQP connection heartbeats used to keep
124
+ # connection alive (e.g., when AMQP broker is behind a firewall), nil or 0 means disable
125
+ # :grace_timeout(Integer):: Maximum number of seconds to wait after last request received before
126
+ # terminating regardless of whether there are still unfinished requests
127
+ # :dup_check(Boolean):: Whether to check for and reject duplicate requests, e.g., due to retries
128
+ # or redelivery by AMQP broker after server failure
129
+ # :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for this agent
130
+ # before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
131
+ # if the agent crashes. Value 0 means unlimited prefetch.
132
+ # :exception_callback(Proc):: Callback with following parameters that is activated on exception events:
133
+ # exception(Exception):: Exception
134
+ # message(Packet):: Message being processed
135
+ # agent(Agent):: Reference to agent
136
+ # :ready_callback(Proc):: Called once agent is connected to AMQP broker and ready for service (no argument)
137
+ # :restart_callback(Proc):: Called on each restart vote with votes being initiated by offline queue
138
+ # exceeding MAX_QUEUED_REQUESTS or by repeated failures to access RightNet when online (no argument)
139
+ # :services(Symbol):: List of services provided by this agent; defaults to all methods exposed by actors
140
+ # :secure(Boolean):: true indicates to use security features of RabbitMQ to restrict agents to themselves
141
+ # :mode(Symbol):: RightNet communication mode: :http or :amqp; defaults to :amqp
142
+ # :api_url(String):: Domain name for HTTP access to RightApi server
143
+ # :account_id(Integer):: Identifier for account owning this agent
144
+ # :shard_id(Integer):: Identifier for database shard in which this agent is operating
145
+ # :vhost(String):: AMQP broker virtual host
146
+ # :user(String):: AMQP broker user
147
+ # :pass(String):: AMQP broker password
148
+ # :host(String):: Comma-separated list of AMQP broker hosts; if only one, it is reapplied
149
+ # to successive ports; if none; defaults to 'localhost'
150
+ # :port(Integer):: Comma-separated list of AMQP broker ports corresponding to hosts; if only one,
151
+ # it is incremented and applied to successive hosts; if none, defaults to AMQP::HOST
152
+ #
153
+ # On start config.yml is read, so it is common to specify options in the YAML file. However, when both
154
+ # Ruby code options and YAML file specify options, Ruby code options take precedence.
155
+ #
156
+ # === Return
157
+ # agent(Agent):: New agent
158
+ def self.start(opts = {})
159
+ agent = new(opts)
160
+ agent.run
161
+ agent
162
+ end
163
+
164
+ # Initialize the new agent
165
+ #
166
+ # === Parameters
167
+ # opts(Hash):: Configuration options per start method above
168
+ #
169
+ # === Return
170
+ # true:: Always return true
171
+ def initialize(opts)
172
+ set_configuration(opts)
173
+ @tags = []
174
+ @tags << opts[:tag] if opts[:tag]
175
+ @tags.flatten!
176
+ @status_callbacks = []
177
+ @options.freeze
178
+ @last_stat_reset_time = Time.now
179
+ reset_agent_stats
180
+ true
181
+ end
182
+
183
+ # Put the agent in service
184
+ # This requires making a RightNet connection via HTTP or AMQP
185
+ # and other initialization like loading actors
186
+ #
187
+ # === Return
188
+ # true:: Always return true
189
+ def run
190
+ Log.init(@identity, @options[:log_path], :print => true)
191
+ Log.level = @options[:log_level] if @options[:log_level]
192
+ RightSupport::Log::Mixin.default_logger = Log
193
+ @history.update("start")
194
+ now = Time.now
195
+ Log.info("[start] Agent #{@identity} starting; time: #{now.utc}; utc_offset: #{now.utc_offset}")
196
+ Log.debug("Start options:")
197
+ log_opts = @options.inject([]) do |t, (k, v)|
198
+ t << "- #{k}: #{k.to_s =~ /pass/ ? '****' : (v.respond_to?(:each) ? v.inspect : v)}"
199
+ end
200
+ log_opts.each { |l| Log.debug(l) }
201
+
202
+ begin
203
+ # Capture process id in file after optional daemonize
204
+ pid_file = PidFile.new(@identity)
205
+ pid_file.check
206
+ daemonize(@identity, @options) if @options[:daemonize]
207
+ pid_file.write
208
+ at_exit { pid_file.remove }
209
+
210
+ if @mode == :http
211
+ # HTTP is being used for RightNet communication instead of AMQP
212
+ # The code loaded with the actors specific to this application
213
+ # is responsible to call setup_http at the appropriate time
214
+ start_service
215
+ else
216
+ # Initiate AMQP broker connection, wait for connection before proceeding
217
+ # otherwise messages published on failed connection will be lost
218
+ @client = RightAMQP::HABrokerClient.new(Serializer.new(:secure), @options)
219
+ @queues.each { |s| @remaining_queue_setup[s] = @client.all }
220
+ @client.connection_status(:one_off => @options[:connect_timeout]) do |status|
221
+ if status == :connected
222
+ # Need to give EM (on Windows) a chance to respond to the AMQP handshake
223
+ # before doing anything interesting to prevent AMQP handshake from
224
+ # timing-out; delay post-connected activity a second
225
+ EM.add_timer(1) { start_service }
226
+ elsif status == :failed
227
+ terminate("failed to connect to any brokers during startup")
228
+ elsif status == :timeout
229
+ terminate("failed to connect to any brokers after #{@options[:connect_timeout]} seconds during startup")
230
+ else
231
+ terminate("broker connect attempt failed unexpectedly with status #{status} during startup")
232
+ end
233
+ end
234
+ end
235
+ rescue SystemExit
236
+ raise
237
+ rescue PidFile::AlreadyRunning
238
+ EM.stop if EM.reactor_running?
239
+ raise
240
+ rescue Exception => e
241
+ terminate("failed startup", e)
242
+ end
243
+ true
244
+ end
245
+
246
+ # Register an actor for this agent
247
+ #
248
+ # === Parameters
249
+ # actor(Actor):: Actor to be registered
250
+ # prefix(String):: Prefix to be used in place of actor's default_prefix
251
+ #
252
+ # === Return
253
+ # (Actor):: Actor registered
254
+ def register(actor, prefix = nil)
255
+ @registry.register(actor, prefix)
256
+ end
257
+
258
+ # Resource href associated with this agent, if any
259
+ #
260
+ # @return [String, NilClass] href or nil if unknown
261
+ def self_href
262
+ @client.self_href if @client && @mode == :http
263
+ end
264
+
265
+ # Record callback to be notified of agent status changes
266
+ # Multiple callbacks are supported
267
+ #
268
+ # === Block
269
+ # optional block activated when there is a status change with parameters
270
+ # type (Symbol):: Type of client reporting status change: :auth, :api, :router, :broker
271
+ # state (Symbol):: State of client
272
+ #
273
+ # === Return
274
+ # (Hash):: Status of various clients
275
+ def status(&callback)
276
+ @status_callbacks << callback if callback
277
+ @status
278
+ end
279
+
280
+ # Connect to an additional AMQP broker or reconnect it if connection has failed
281
+ # Subscribe to identity queue on this broker
282
+ # Update config file if this is a new broker
283
+ # Assumes already has credentials on this broker and identity queue exists
284
+ #
285
+ # === Parameters
286
+ # host(String):: Host name of broker
287
+ # port(Integer):: Port number of broker
288
+ # index(Integer):: Small unique id associated with this broker for use in forming alias
289
+ # priority(Integer|nil):: Priority position of this broker in list for use
290
+ # by this agent with nil meaning add to end of list
291
+ # force(Boolean):: Reconnect even if already connected
292
+ #
293
+ # === Return
294
+ # res(String|nil):: Error message if failed, otherwise nil
295
+ def connect(host, port, index, priority = nil, force = false)
296
+ @connect_request_stats.update("connect b#{index}")
297
+ even_if = " even if already connected" if force
298
+ Log.info("Connecting to broker at host #{host.inspect} port #{port.inspect} " +
299
+ "index #{index.inspect} priority #{priority.inspect}#{even_if}")
300
+ Log.info("Current broker configuration: #{@client.status.inspect}")
301
+ res = nil
302
+ begin
303
+ @client.connect(host, port, index, priority, force) do |id|
304
+ @client.connection_status(:one_off => @options[:connect_timeout], :brokers => [id]) do |status|
305
+ begin
306
+ if status == :connected
307
+ setup_queues([id])
308
+ remaining = 0
309
+ @remaining_queue_setup.each_value { |ids| remaining += ids.size }
310
+ Log.info("[setup] Finished subscribing to queues after reconnecting to broker #{id}") if remaining == 0
311
+ unless update_configuration(:host => @client.hosts, :port => @client.ports)
312
+ Log.warning("Successfully connected to broker #{id} but failed to update config file")
313
+ end
314
+ else
315
+ Log.error("Failed to connect to broker #{id}, status #{status.inspect}")
316
+ end
317
+ rescue Exception => e
318
+ Log.error("Failed to connect to broker #{id}, status #{status.inspect}", e)
319
+ @exception_stats.track("connect", e)
320
+ end
321
+ end
322
+ end
323
+ rescue Exception => e
324
+ res = Log.format("Failed to connect to broker at host #{host.inspect} and port #{port.inspect}", e)
325
+ @exception_stats.track("connect", e)
326
+ end
327
+ Log.error(res) if res
328
+ res
329
+ end
330
+
331
+ # Disconnect from an AMQP broker and optionally remove it from the configuration
332
+ # Refuse to do so if it is the last connected broker
333
+ #
334
+ # === Parameters
335
+ # host(String):: Host name of broker
336
+ # port(Integer):: Port number of broker
337
+ # remove(Boolean):: Whether to remove broker from configuration rather than just closing it,
338
+ # defaults to false
339
+ #
340
+ # === Return
341
+ # res(String|nil):: Error message if failed, otherwise nil
342
+ def disconnect(host, port, remove = false)
343
+ and_remove = " and removing" if remove
344
+ Log.info("Disconnecting#{and_remove} broker at host #{host.inspect} port #{port.inspect}")
345
+ Log.info("Current broker configuration: #{@client.status.inspect}")
346
+ id = RightAMQP::HABrokerClient.identity(host, port)
347
+ @connect_request_stats.update("disconnect #{@client.alias_(id)}")
348
+ connected = @client.connected
349
+ res = nil
350
+ if connected.include?(id) && connected.size == 1
351
+ res = "Not disconnecting from #{id} because it is the last connected broker for this agent"
352
+ elsif @client.get(id)
353
+ begin
354
+ if remove
355
+ @client.remove(host, port) do |id|
356
+ unless update_configuration(:host => @client.hosts, :port => @client.ports)
357
+ res = "Successfully disconnected from broker #{id} but failed to update config file"
358
+ end
359
+ end
360
+ else
361
+ @client.close_one(id)
362
+ end
363
+ rescue Exception => e
364
+ res = Log.format("Failed to disconnect from broker #{id}", e)
365
+ @exception_stats.track("disconnect", e)
366
+ end
367
+ else
368
+ res = "Cannot disconnect from broker #{id} because not configured for this agent"
369
+ end
370
+ Log.error(res) if res
371
+ res
372
+ end
373
+
374
+ # There were problems while setting up service for this agent on the given AMQP brokers,
375
+ # so mark these brokers as failed if not currently connected and later, during the
376
+ # periodic status check, attempt to reconnect
377
+ #
378
+ # === Parameters
379
+ # ids(Array):: Identity of brokers
380
+ #
381
+ # === Return
382
+ # res(String|nil):: Error message if failed, otherwise nil
383
+ def connect_failed(ids)
384
+ aliases = @client.aliases(ids).join(", ")
385
+ @connect_request_stats.update("enroll failed #{aliases}")
386
+ res = nil
387
+ begin
388
+ Log.info("Received indication that service initialization for this agent for brokers #{ids.inspect} has failed")
389
+ connected = @client.connected
390
+ ignored = connected & ids
391
+ Log.info("Not marking brokers #{ignored.inspect} as unusable because currently connected") if ignored
392
+ Log.info("Current broker configuration: #{@client.status.inspect}")
393
+ @client.declare_unusable(ids - ignored)
394
+ rescue Exception => e
395
+ res = Log.format("Failed handling broker connection failure indication for #{ids.inspect}", e)
396
+ Log.error(res)
397
+ @exception_stats.track("connect failed", e)
398
+ end
399
+ res
400
+ end
401
+
402
+ # Update agent's persisted configuration
403
+ # Note that @options are frozen and therefore not updated
404
+ #
405
+ # === Parameters
406
+ # opts(Hash):: Options being updated
407
+ #
408
+ # === Return
409
+ # (Boolean):: true if successful, otherwise false
410
+ def update_configuration(opts)
411
+ if (cfg = AgentConfig.load_cfg(@agent_name))
412
+ opts.each { |k, v| cfg[k] = v }
413
+ AgentConfig.store_cfg(@agent_name, cfg)
414
+ true
415
+ else
416
+ Log.error("Could not access configuration file #{AgentConfig.cfg_file(@agent_name).inspect} for update")
417
+ false
418
+ end
419
+ rescue Exception => e
420
+ Log.error("Failed updating configuration file #{AgentConfig.cfg_file(@agent_name).inspect}", e, :trace)
421
+ false
422
+ end
423
+
424
+ # Gracefully terminate execution by allowing unfinished tasks to complete
425
+ # Immediately terminate if called a second time
426
+ # Report reason for termination if it is abnormal
427
+ #
428
+ # === Parameters
429
+ # reason(String):: Reason for abnormal termination, if any
430
+ # exception(Exception|String):: Exception or other parenthetical error information, if any
431
+ #
432
+ # === Return
433
+ # true:: Always return true
434
+ def terminate(reason = nil, exception = nil)
435
+ begin
436
+ @history.update("stop") if @history
437
+ Log.error("[stop] Terminating because #{reason}", exception, :trace) if reason
438
+ if exception.is_a?(Exception)
439
+ h = @history.analyze_service
440
+ if h[:last_crashed]
441
+ delay = [(Time.now.to_i - h[:last_crash_time]) * 2, MAX_ABNORMAL_TERMINATE_DELAY].min
442
+ Log.info("[stop] Delaying termination for #{RightSupport::Stats.elapsed(delay)} to slow crash cycling")
443
+ sleep(delay)
444
+ end
445
+ end
446
+ if @terminating || @client.nil?
447
+ @terminating = true
448
+ @termination_timer.cancel if @termination_timer
449
+ @termination_timer = nil
450
+ Log.info("[stop] Terminating immediately")
451
+ @terminate_callback.call
452
+ @history.update("graceful exit") if @history && @client.nil?
453
+ else
454
+ @terminating = true
455
+ @check_status_timer.cancel if @check_status_timer
456
+ @check_status_timer = nil
457
+ Log.info("[stop] Agent #{@identity} terminating")
458
+ stop_gracefully(@options[:grace_timeout])
459
+ end
460
+ rescue SystemExit
461
+ raise
462
+ rescue Exception => e
463
+ Log.error("Failed to terminate gracefully", e, :trace)
464
+ begin @terminate_callback.call; rescue Exception; end
465
+ end
466
+ true
467
+ end
468
+
469
+ # Retrieve statistics about agent operation
470
+ #
471
+ # === Parameters:
472
+ # options(Hash):: Request options:
473
+ # :reset(Boolean):: Whether to reset the statistics after getting the current ones
474
+ #
475
+ # === Return
476
+ # result(OperationResult):: Always returns success
477
+ def stats(options = {})
478
+ now = Time.now
479
+ reset = options[:reset]
480
+ stats = {
481
+ "name" => @agent_name,
482
+ "identity" => @identity,
483
+ "hostname" => Socket.gethostname,
484
+ "memory" => Platform.process.resident_set_size,
485
+ "version" => AgentConfig.protocol_version,
486
+ "agent stats" => agent_stats(reset),
487
+ "receive stats" => dispatcher_stats(reset),
488
+ "send stats" => @sender.stats(reset),
489
+ "last reset time" => @last_stat_reset_time.to_i,
490
+ "stat time" => now.to_i,
491
+ "service uptime" => @history.analyze_service,
492
+ "machine uptime" => Platform.shell.uptime
493
+ }
494
+ stats["revision"] = @revision if @revision
495
+ if @mode == :http
496
+ stats.merge!(@client.stats(reset))
497
+ else
498
+ stats["broker"] = @client.stats(reset)
499
+ end
500
+ result = OperationResult.success(stats)
501
+ @last_stat_reset_time = now if reset
502
+ result
503
+ end
504
+
505
+ protected
506
+
507
+ # Get request statistics
508
+ #
509
+ # === Parameters
510
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
511
+ #
512
+ # === Return
513
+ # stats(Hash):: Current statistics:
514
+ # "connect requests"(Hash|nil):: Stats about requests to update AMQP broker connections with keys "total", "percent",
515
+ # and "last" with percentage breakdown by "connects: <alias>", "disconnects: <alias>", "enroll setup failed:
516
+ # <aliases>", or nil if none
517
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
518
+ # "total"(Integer):: Total exceptions for this category
519
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
520
+ # "non-deliveries"(Hash):: AMQP message non-delivery activity stats with keys "total", "percent", "last", and "rate"
521
+ # with percentage breakdown by request type, or nil if none
522
+ # "request failures"(Hash|nil):: Request dispatch failure activity stats with keys "total", "percent", "last", and "rate"
523
+ # with percentage breakdown per failure type, or nil if none
524
+ # "response failures"(Hash|nil):: Response delivery failure activity stats with keys "total", "percent", "last", and "rate"
525
+ # with percentage breakdown per failure type, or nil if none
526
+ def agent_stats(reset = false)
527
+ stats = {
528
+ "exceptions" => @exception_stats.stats,
529
+ "request failures" => @request_failure_stats.all,
530
+ "response failures" => @response_failure_stats.all
531
+ }
532
+ unless @mode == :http
533
+ stats["connect requests"] = @connect_request_stats.all
534
+ stats["non-deliveries"] = @non_delivery_stats.all
535
+ end
536
+ reset_agent_stats if reset
537
+ stats
538
+ end
539
+
540
+ # Reset agent statistics
541
+ #
542
+ # === Return
543
+ # true:: Always return true
544
+ def reset_agent_stats
545
+ @connect_request_stats = RightSupport::Stats::Activity.new(measure_rate = false)
546
+ @non_delivery_stats = RightSupport::Stats::Activity.new
547
+ @request_failure_stats = RightSupport::Stats::Activity.new
548
+ @response_failure_stats = RightSupport::Stats::Activity.new
549
+ @exception_stats = RightSupport::Stats::Exceptions.new(self, @options[:exception_callback])
550
+ true
551
+ end
552
+
553
+ # Get dispatcher statistics
554
+ #
555
+ # === Return
556
+ # (Hash):: Current statistics
557
+ def dispatcher_stats(reset)
558
+ @dispatchers[@identity].stats(reset)
559
+ end
560
+
561
+ # Set the agent's configuration using the supplied options
562
+ #
563
+ # === Parameters
564
+ # opts(Hash):: Configuration options
565
+ #
566
+ # === Return
567
+ # true:: Always return true
568
+ def set_configuration(opts)
569
+ @options = DEFAULT_OPTIONS.clone
570
+ @options.update(opts)
571
+
572
+ AgentConfig.root_dir = @options[:root_dir]
573
+ AgentConfig.pid_dir = @options[:pid_dir]
574
+
575
+ @options[:log_path] = false
576
+ if @options[:daemonize] || @options[:log_dir]
577
+ @options[:log_path] = (@options[:log_dir] || Platform.filesystem.log_dir)
578
+ FileUtils.mkdir_p(@options[:log_path]) unless File.directory?(@options[:log_path])
579
+ end
580
+
581
+ @options[:async_response] = true unless @options.has_key?(:async_response)
582
+
583
+ @identity = @options[:identity]
584
+ parsed_identity = AgentIdentity.parse(@identity)
585
+ @agent_type = parsed_identity.agent_type
586
+ @agent_name = @options[:agent_name]
587
+ @request_queue = "request"
588
+ @request_queue << "-#{@options[:shard_id].to_i}" if @options[:shard_id].to_i != 0
589
+ @mode = @options[:mode].to_sym
590
+ @stats_routing_key = "stats.#{@agent_type}.#{parsed_identity.base_id}"
591
+ @terminate_callback = TERMINATE_BLOCK
592
+ @exception_callback = @options[:exception_callback]
593
+ @revision = revision
594
+ @queues = [@identity]
595
+ @remaining_queue_setup = {}
596
+ @history = History.new(@identity)
597
+ end
598
+
599
+ # Start service
600
+ #
601
+ # === Return
602
+ # true:: Always return true
603
+ def start_service
604
+ begin
605
+ @registry = ActorRegistry.new
606
+ @dispatchers = create_dispatchers
607
+ # Creating sender now but for HTTP mode it is not really usable until setup_http
608
+ # is called by the code loaded for this application in load_actors
609
+ @sender = create_sender
610
+ load_actors
611
+ setup_traps
612
+ setup_status
613
+ unless @mode == :http
614
+ setup_non_delivery
615
+ setup_queues
616
+ end
617
+ @history.update("run")
618
+ start_console if @options[:console] && !@options[:daemonize]
619
+ EM.next_tick { @options[:ready_callback].call } if @options[:ready_callback]
620
+ EM.defer { @client.listen(nil) { |e| handle_event(e) } } if @mode == :http
621
+
622
+ # Need to keep reconnect interval at least :connect_timeout in size,
623
+ # otherwise connection_status callback will not timeout prior to next
624
+ # reconnect attempt, which can result in repeated attempts to setup
625
+ # queues when finally do connect
626
+ setup_status_checks([@options[:check_interval], @options[:connect_timeout]].max)
627
+ rescue SystemExit
628
+ raise
629
+ rescue Exception => e
630
+ terminate("failed service startup", e)
631
+ end
632
+ true
633
+ end
634
+
635
+ # Handle events received by this agent
636
+ #
637
+ # === Parameters
638
+ # event(Hash):: Event received
639
+ #
640
+ # === Return
641
+ # nil:: Always return nil indicating no response since handled separately via notify
642
+ def handle_event(event)
643
+ if event.is_a?(Hash)
644
+ if ["Push", "Request"].include?(event[:type])
645
+ # Use next_tick to ensure that on main reactor thread
646
+ # so that any data access is thread safe
647
+ EM.next_tick do
648
+ begin
649
+ if (result = @dispatcher.dispatch(event_to_packet(event))) && event[:type] == "Request"
650
+ @client.notify(result_to_event(result), [result.to])
651
+ end
652
+ rescue Exception => e
653
+ Log.error("Failed sending response for <#{event[:uuid]}>", e, :trace)
654
+ end
655
+ end
656
+ else
657
+ Log.error("Unrecognized event type #{event[:type]} from #{event[:from]}")
658
+ end
659
+ else
660
+ Log.error("Unrecognized event: #{event.class}")
661
+ end
662
+ nil
663
+ end
664
+
665
+ # Convert event hash to packet
666
+ #
667
+ # === Parameters
668
+ # event(Hash):: Event to be converted
669
+ #
670
+ # === Return
671
+ # (Push|Request):: Packet
672
+ def event_to_packet(event)
673
+ packet = nil
674
+ case event[:type]
675
+ when "Push"
676
+ packet = RightScale::Push.new(event[:path], event[:data], {:from => event[:from], :token => event[:uuid]})
677
+ packet.expires_at = event[:expires_at].to_i if event.has_key?(:expires_at)
678
+ when "Request"
679
+ options = {:from => event[:from], :token => event[:uuid], :reply_to => event[:reply_to], :tries => event[:tries]}
680
+ packet = RightScale::Request.new(event[:path], event[:data], options)
681
+ packet.expires_at = event[:expires_at].to_i if event.has_key?(:expires_at)
682
+ end
683
+ packet
684
+ end
685
+
686
+ # Convert result packet to event
687
+ #
688
+ # === Parameters
689
+ # result(Result):: Event to be converted
690
+ #
691
+ # === Return
692
+ # (Hash):: Event
693
+ def result_to_event(result)
694
+ { :type => "Result",
695
+ :from => result.from,
696
+ :data => {
697
+ :result => result.results,
698
+ :duration => result.duration,
699
+ :request_uuid => result.token,
700
+ :request_from => result.request_from } }
701
+ end
702
+
703
+ # Create dispatcher per queue for use in handling incoming requests
704
+ #
705
+ # === Return
706
+ # [Hash]:: Dispatchers with queue name as key
707
+ def create_dispatchers
708
+ cache = DispatchedCache.new(@identity) if @options[:dup_check]
709
+ @dispatcher = Dispatcher.new(self, cache)
710
+ @queues.inject({}) { |dispatchers, queue| dispatchers[queue] = @dispatcher; dispatchers }
711
+ end
712
+
713
+ # Create manager for outgoing requests
714
+ #
715
+ # === Return
716
+ # (Sender):: New sender
717
+ def create_sender
718
+ Sender.new(self)
719
+ end
720
+
721
+ # Load the ruby code for the actors
722
+ #
723
+ # === Return
724
+ # true:: Always return true
725
+ def load_actors
726
+ # Load agent's configured actors
727
+ actors = (@options[:actors] || []).clone
728
+ Log.info("[setup] Agent #{@identity} with actors #{actors.inspect}")
729
+ actors_dirs = AgentConfig.actors_dirs
730
+ actors_dirs.each do |dir|
731
+ Dir["#{dir}/*.rb"].each do |file|
732
+ actor = File.basename(file, ".rb")
733
+ next if actors && !actors.include?(actor)
734
+ Log.info("[setup] loading actor #{file}")
735
+ require file
736
+ actors.delete(actor)
737
+ end
738
+ end
739
+ Log.error("Actors #{actors.inspect} not found in #{actors_dirs.inspect}") unless actors.empty?
740
+
741
+ # Perform agent-specific initialization including actor creation and registration
742
+ if (init_file = AgentConfig.init_file)
743
+ Log.info("[setup] initializing agent from #{init_file}")
744
+ instance_eval(File.read(init_file), init_file)
745
+ else
746
+ Log.error("No agent init.rb file found in init directory of #{AgentConfig.root_dir.inspect}")
747
+ end
748
+ true
749
+ end
750
+
751
+ # Create client for HTTP-based RightNet communication
752
+ # The code loaded with the actors specific to this application
753
+ # is responsible for calling this function
754
+ #
755
+ # === Parameters
756
+ # auth_client(AuthClient):: Authorization client to be used by this agent
757
+ #
758
+ # === Return
759
+ # true:: Always return true
760
+ def setup_http(auth_client)
761
+ @auth_client = auth_client
762
+ if @mode == :http
763
+ RightHttpClient.init(@auth_client, @options.merge(:retry_enabled => true))
764
+ @client = RightHttpClient.instance
765
+ end
766
+ true
767
+ end
768
+
769
+ # Setup signal traps
770
+ #
771
+ # === Return
772
+ # true:: Always return true
773
+ def setup_traps
774
+ ['INT', 'TERM'].each do |sig|
775
+ old = trap(sig) do
776
+ EM.next_tick do
777
+ begin
778
+ terminate do
779
+ TERMINATE_BLOCK.call
780
+ old.call if old.is_a? Proc
781
+ end
782
+ rescue Exception => e
783
+ Log.error("Failed in termination", e, :trace)
784
+ end
785
+ end
786
+ end
787
+ end
788
+ true
789
+ end
790
+
791
+ # Setup client status collection
792
+ #
793
+ # === Return
794
+ # true:: Always return true
795
+ def setup_status
796
+ @status = {}
797
+ if @mode == :http
798
+ @status = @client.status { |type, state| update_status(type, state) }.dup
799
+ else
800
+ @client.connection_status { |state| update_status(:broker, state) }
801
+ @status[:broker] = :connected
802
+ @status[:auth] = @auth_client.status { |type, state| update_status(type, state) } if @auth_client
803
+ end
804
+ true
805
+ end
806
+
807
+ # Setup non-delivery handler
808
+ #
809
+ # === Return
810
+ # true:: Always return true
811
+ def setup_non_delivery
812
+ @client.non_delivery do |reason, type, token, from, to|
813
+ begin
814
+ @non_delivery_stats.update(type)
815
+ reason = case reason
816
+ when "NO_ROUTE" then OperationResult::NO_ROUTE_TO_TARGET
817
+ when "NO_CONSUMERS" then OperationResult::TARGET_NOT_CONNECTED
818
+ else reason.to_s
819
+ end
820
+ result = Result.new(token, from, OperationResult.non_delivery(reason), to)
821
+ @sender.handle_response(result)
822
+ rescue Exception => e
823
+ Log.error("Failed handling non-delivery for <#{token}>", e, :trace)
824
+ @exception_stats.track("message return", e)
825
+ end
826
+ end
827
+ end
828
+
829
+ # Setup the queues on the specified brokers for this agent
830
+ # Do the setup regardless of whether remaining setup is empty since may be reconnecting
831
+ #
832
+ # === Parameters
833
+ # ids(Array):: Identity of brokers for which to subscribe, defaults to all usable
834
+ #
835
+ # === Return
836
+ # true:: Always return true
837
+ def setup_queues(ids = nil)
838
+ @queues.each { |q| @remaining_queue_setup[q] -= setup_queue(q, ids) }
839
+ true
840
+ end
841
+
842
+ # Setup queue for this agent
843
+ #
844
+ # === Parameters
845
+ # name(String):: Queue name
846
+ # ids(Array):: Identity of brokers for which to subscribe, defaults to all usable
847
+ #
848
+ # === Return
849
+ # (Array):: Identity of brokers to which subscribe submitted (although may still fail)
850
+ def setup_queue(name, ids = nil)
851
+ queue = {:name => name, :options => {:durable => true, :no_declare => @options[:secure]}}
852
+ filter = [:from, :tags, :tries, :persistent]
853
+ options = {:ack => true, Push => filter, Request => filter, Result => [:from], :brokers => ids}
854
+ @client.subscribe(queue, nil, options) { |_, packet, header| handle_packet(name, packet, header) }
855
+ end
856
+
857
+ # Handle packet from queue
858
+ #
859
+ # === Parameters
860
+ # queue(String):: Name of queue from which message was received
861
+ # packet(Packet):: Packet received
862
+ # header(AMQP::Frame::Header):: Packet header containing ack control
863
+ #
864
+ # === Return
865
+ # true:: Always return true
866
+ def handle_packet(queue, packet, header)
867
+ begin
868
+ # Continue to dispatch/ack requests even when terminating otherwise will block results
869
+ # Ideally would reject requests when terminating but broker client does not yet support that
870
+ case packet
871
+ when Push, Request then dispatch_request(packet, queue)
872
+ when Result then deliver_response(packet)
873
+ end
874
+ @sender.message_received
875
+ rescue Exception => e
876
+ Log.error("#{queue} queue processing error", e, :trace)
877
+ @exception_stats.track("#{queue} queue", e, packet)
878
+ ensure
879
+ # Relying on fact that all dispatches/deliveries are synchronous and therefore
880
+ # need to have completed or failed by now, thus allowing packet acknowledgement
881
+ header.ack
882
+ end
883
+ true
884
+ end
885
+
886
+ # Dispatch request and then send response if any
887
+ #
888
+ # === Parameters
889
+ # request(Push|Request):: Packet containing request
890
+ # queue(String):: Name of queue from which message was received
891
+ #
892
+ # === Return
893
+ # true:: Always return true
894
+ def dispatch_request(request, queue)
895
+ begin
896
+ if (dispatcher = @dispatchers[queue])
897
+ if (result = dispatcher.dispatch(request))
898
+ exchange = {:type => :queue, :name => request.reply_to, :options => {:durable => true, :no_declare => @options[:secure]}}
899
+ @client.publish(exchange, result, :persistent => true, :mandatory => true, :log_filter => [:request_from, :tries, :persistent, :duration])
900
+ end
901
+ else
902
+ Log.error("Failed to dispatch request #{request.trace} from queue #{queue} because no dispatcher configured")
903
+ @request_failure_stats.update("NoConfiguredDispatcher")
904
+ end
905
+ rescue Dispatcher::DuplicateRequest
906
+ rescue RightAMQP::HABrokerClient::NoConnectedBrokers => e
907
+ Log.error("Failed to publish result of dispatched request #{request.trace} from queue #{queue}", e)
908
+ @request_failure_stats.update("NoConnectedBrokers")
909
+ rescue Exception => e
910
+ Log.error("Failed to dispatch request #{request.trace} from queue #{queue}", e, :trace)
911
+ @request_failure_stats.update(e.class.name)
912
+ @exception_stats.track("request", e)
913
+ end
914
+ true
915
+ end
916
+
917
+ # Deliver response to request sender
918
+ #
919
+ # === Parameters
920
+ # result(Result):: Packet containing response
921
+ #
922
+ # === Return
923
+ # true:: Always return true
924
+ def deliver_response(result)
925
+ begin
926
+ @sender.handle_response(result)
927
+ rescue Exception => e
928
+ Log.error("Failed to deliver response #{result.trace}", e, :trace)
929
+ @response_failure_stats.update(e.class.name)
930
+ @exception_stats.track("response", e)
931
+ end
932
+ true
933
+ end
934
+
935
+ # Finish any remaining agent setup
936
+ #
937
+ # === Return
938
+ # true:: Always return true
939
+ def finish_setup
940
+ @client.failed.each do |id|
941
+ p = {:agent_identity => @identity}
942
+ p[:host], p[:port], p[:id], p[:priority] = @client.identity_parts(id)
943
+ @sender.send_push("/registrar/connect", p)
944
+ end
945
+ true
946
+ end
947
+
948
+ # Forward status updates via callbacks
949
+ #
950
+ # === Parameters
951
+ # type (Symbol):: Type of client: :auth, :api, :router, or :broker
952
+ # state (Symbol):: State of client
953
+ #
954
+ # === Return
955
+ # true:: Always return true
956
+ def update_status(type, state)
957
+ old_state, @status[type] = @status[type], state
958
+ Log.info("Client #{type.inspect} changed state from #{old_state.inspect} to #{state.inspect}")
959
+ @status_callbacks.each do |callback|
960
+ begin
961
+ callback.call(type, state)
962
+ rescue RuntimeError => e
963
+ Log.error("Failed status callback", e)
964
+ @exception_stats.track("update status", e)
965
+ end
966
+ end
967
+ true
968
+ end
969
+
970
+ # Setup periodic status check
971
+ #
972
+ # === Parameters
973
+ # interval(Integer):: Number of seconds between status checks
974
+ #
975
+ # === Return
976
+ # true:: Always return true
977
+ def setup_status_checks(interval)
978
+ @check_status_count = 0
979
+ @check_status_brokers = @client.all unless @mode == :http
980
+ @check_status_timer = EM::PeriodicTimer.new(interval) { check_status }
981
+ true
982
+ end
983
+
984
+ # Check status of agent by finishing any queue setup, checking the status of the queues,
985
+ # and gathering/publishing current operation statistics
986
+ # Checking the status of a queue will cause the broker connection to fail if the
987
+ # queue does not exist, but a reconnect should then get initiated on the next check loop
988
+ # Although agent termination cancels the check_status_timer, this method could induce
989
+ # termination, therefore the termination status needs to be checked before each step
990
+ #
991
+ # === Return
992
+ # true:: Always return true
993
+ def check_status
994
+ begin
995
+ if @auth_client && @auth_client.mode != @mode
996
+ Log.info("Detected request to switch mode from #{@mode} to #{@auth_client.mode}")
997
+ update_status(:auth, :failed)
998
+ end
999
+ rescue Exception => e
1000
+ Log.error("Failed switching mode", e)
1001
+ @exception_stats.track("check status", e)
1002
+ end
1003
+
1004
+ begin
1005
+ finish_setup unless @terminating || @mode == :http
1006
+ rescue Exception => e
1007
+ Log.error("Failed finishing setup", e)
1008
+ @exception_stats.track("check status", e)
1009
+ end
1010
+
1011
+ begin
1012
+ @client.queue_status(@queues, timeout = @options[:check_interval] / 10) unless @terminating || @mode == :http
1013
+ rescue Exception => e
1014
+ Log.error("Failed checking queue status", e)
1015
+ @exception_stats.track("check status", e)
1016
+ end
1017
+
1018
+ begin
1019
+ publish_stats unless @terminating || @stats_routing_key.nil?
1020
+ rescue Exceptions::ConnectivityFailure => e
1021
+ Log.error("Failed publishing stats", e, :no_trace)
1022
+ rescue Exception => e
1023
+ Log.error("Failed publishing stats", e)
1024
+ @exception_stats.track("check status", e)
1025
+ end
1026
+
1027
+ begin
1028
+ check_other(@check_status_count) unless @terminating
1029
+ rescue Exception => e
1030
+ Log.error("Failed to perform other check status check", e)
1031
+ @exception_stats.track("check status", e)
1032
+ end
1033
+
1034
+ @check_status_count += 1
1035
+ true
1036
+ end
1037
+
1038
+ # Publish current stats
1039
+ #
1040
+ # === Return
1041
+ # true:: Always return true
1042
+ def publish_stats
1043
+ s = stats({}).content
1044
+ if @mode == :http
1045
+ @client.notify({:type => "Stats", :from => @identity, :data => s}, nil)
1046
+ else
1047
+ exchange = {:type => :topic, :name => "stats", :options => {:no_declare => true}}
1048
+ @client.publish(exchange, Stats.new(s, @identity), :no_log => true,
1049
+ :routing_key => @stats_routing_key, :brokers => @check_status_brokers.rotate!)
1050
+ end
1051
+ true
1052
+ end
1053
+
1054
+ # Allow derived classes to perform any other useful periodic checks
1055
+ #
1056
+ # === Parameters
1057
+ # check_status_count(Integer):: Counter that is incremented for each status check
1058
+ #
1059
+ # === Return
1060
+ # true:: Always return true
1061
+ def check_other(check_status_count)
1062
+ true
1063
+ end
1064
+
1065
+ # Store unique tags
1066
+ #
1067
+ # === Parameters
1068
+ # tags(Array):: Tags to be added
1069
+ #
1070
+ # === Return
1071
+ # @tags(Array):: Current tags
1072
+ def tag(*tags)
1073
+ tags.each {|t| @tags << t}
1074
+ @tags.uniq!
1075
+ end
1076
+
1077
+ # Gracefully stop processing
1078
+ # Close clients except for authorization
1079
+ #
1080
+ # === Parameters
1081
+ # timeout(Integer):: Maximum number of seconds to wait after last request received before
1082
+ # terminating regardless of whether there are still unfinished requests
1083
+ #
1084
+ # === Return
1085
+ # true:: Always return true
1086
+ def stop_gracefully(timeout)
1087
+ if @mode == :http
1088
+ @client.close
1089
+ else
1090
+ @client.unusable.each { |id| @client.close_one(id, propagate = false) }
1091
+ end
1092
+ finish_terminating(timeout)
1093
+ end
1094
+
1095
+ # Finish termination after all requests have been processed
1096
+ #
1097
+ # === Parameters
1098
+ # timeout(Integer):: Maximum number of seconds to wait after last request received before
1099
+ # terminating regardless of whether there are still unfinished requests
1100
+ #
1101
+ # === Return
1102
+ # true:: Always return true
1103
+ def finish_terminating(timeout)
1104
+ if @sender
1105
+ request_count, request_age = @sender.terminate
1106
+
1107
+ finish = lambda do
1108
+ request_count, request_age = @sender.terminate
1109
+ Log.info("[stop] The following #{request_count} requests initiated as recently as #{request_age} " +
1110
+ "seconds ago are being dropped:\n " + @sender.dump_requests.join("\n ")) if request_age
1111
+ if @mode == :http
1112
+ @terminate_callback.call
1113
+ else
1114
+ @client.close { @terminate_callback.call }
1115
+ end
1116
+ end
1117
+
1118
+ if (wait_time = [timeout - (request_age || timeout), 0].max) > 0
1119
+ Log.info("[stop] Termination waiting #{wait_time} seconds for completion of #{request_count} " +
1120
+ "requests initiated as recently as #{request_age} seconds ago")
1121
+ @termination_timer = EM::Timer.new(wait_time) do
1122
+ begin
1123
+ Log.info("[stop] Continuing with termination")
1124
+ finish.call
1125
+ rescue Exception => e
1126
+ Log.error("Failed while finishing termination", e, :trace)
1127
+ begin @terminate_callback.call; rescue Exception; end
1128
+ end
1129
+ end
1130
+ else
1131
+ finish.call
1132
+ end
1133
+ else
1134
+ @terminate_callback.call
1135
+ end
1136
+ @history.update("graceful exit")
1137
+ true
1138
+ end
1139
+
1140
+ # Determine current revision of software
1141
+ #
1142
+ # === Return
1143
+ # (String):: Revision of software in displayable format
1144
+ def revision
1145
+ end
1146
+
1147
+ end # Agent
1148
+
1149
+ end # RightScale