right_agent 2.0.7-x86-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +82 -0
  3. data/Rakefile +113 -0
  4. data/lib/right_agent.rb +59 -0
  5. data/lib/right_agent/actor.rb +182 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +232 -0
  8. data/lib/right_agent/agent.rb +1149 -0
  9. data/lib/right_agent/agent_config.rb +480 -0
  10. data/lib/right_agent/agent_identity.rb +210 -0
  11. data/lib/right_agent/agent_tag_manager.rb +237 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/clients.rb +31 -0
  14. data/lib/right_agent/clients/api_client.rb +383 -0
  15. data/lib/right_agent/clients/auth_client.rb +247 -0
  16. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  17. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  18. data/lib/right_agent/clients/right_http_client.rb +279 -0
  19. data/lib/right_agent/clients/router_client.rb +493 -0
  20. data/lib/right_agent/command.rb +30 -0
  21. data/lib/right_agent/command/agent_manager_commands.rb +150 -0
  22. data/lib/right_agent/command/command_client.rb +136 -0
  23. data/lib/right_agent/command/command_constants.rb +33 -0
  24. data/lib/right_agent/command/command_io.rb +126 -0
  25. data/lib/right_agent/command/command_parser.rb +87 -0
  26. data/lib/right_agent/command/command_runner.rb +118 -0
  27. data/lib/right_agent/command/command_serializer.rb +63 -0
  28. data/lib/right_agent/connectivity_checker.rb +179 -0
  29. data/lib/right_agent/console.rb +65 -0
  30. data/lib/right_agent/core_payload_types.rb +44 -0
  31. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  32. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  33. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  34. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  35. data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
  36. data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
  37. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  38. data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
  39. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  40. data/lib/right_agent/core_payload_types/login_user.rb +79 -0
  41. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  42. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
  43. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  44. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  45. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
  46. data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
  47. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  48. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  49. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  50. data/lib/right_agent/daemonize.rb +35 -0
  51. data/lib/right_agent/dispatched_cache.rb +109 -0
  52. data/lib/right_agent/dispatcher.rb +272 -0
  53. data/lib/right_agent/enrollment_result.rb +221 -0
  54. data/lib/right_agent/exceptions.rb +87 -0
  55. data/lib/right_agent/history.rb +145 -0
  56. data/lib/right_agent/log.rb +460 -0
  57. data/lib/right_agent/minimal.rb +46 -0
  58. data/lib/right_agent/monkey_patches.rb +30 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  64. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  65. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  66. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
  67. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  68. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  69. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  70. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  71. data/lib/right_agent/multiplexer.rb +102 -0
  72. data/lib/right_agent/offline_handler.rb +270 -0
  73. data/lib/right_agent/operation_result.rb +300 -0
  74. data/lib/right_agent/packets.rb +673 -0
  75. data/lib/right_agent/payload_formatter.rb +104 -0
  76. data/lib/right_agent/pending_requests.rb +128 -0
  77. data/lib/right_agent/pid_file.rb +159 -0
  78. data/lib/right_agent/platform.rb +770 -0
  79. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  80. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  81. data/lib/right_agent/platform/unix/platform.rb +226 -0
  82. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  83. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  84. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  85. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  86. data/lib/right_agent/retryable_request.rb +195 -0
  87. data/lib/right_agent/scripts/agent_controller.rb +543 -0
  88. data/lib/right_agent/scripts/agent_deployer.rb +400 -0
  89. data/lib/right_agent/scripts/common_parser.rb +160 -0
  90. data/lib/right_agent/scripts/log_level_manager.rb +192 -0
  91. data/lib/right_agent/scripts/stats_manager.rb +268 -0
  92. data/lib/right_agent/scripts/usage.rb +58 -0
  93. data/lib/right_agent/secure_identity.rb +92 -0
  94. data/lib/right_agent/security.rb +32 -0
  95. data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
  96. data/lib/right_agent/security/certificate.rb +102 -0
  97. data/lib/right_agent/security/certificate_cache.rb +89 -0
  98. data/lib/right_agent/security/distinguished_name.rb +56 -0
  99. data/lib/right_agent/security/encrypted_document.rb +83 -0
  100. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  101. data/lib/right_agent/security/signature.rb +86 -0
  102. data/lib/right_agent/security/static_certificate_store.rb +85 -0
  103. data/lib/right_agent/sender.rb +792 -0
  104. data/lib/right_agent/serialize.rb +29 -0
  105. data/lib/right_agent/serialize/message_pack.rb +107 -0
  106. data/lib/right_agent/serialize/secure_serializer.rb +151 -0
  107. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  108. data/lib/right_agent/serialize/serializable.rb +151 -0
  109. data/lib/right_agent/serialize/serializer.rb +159 -0
  110. data/lib/right_agent/subprocess.rb +38 -0
  111. data/lib/right_agent/tracer.rb +124 -0
  112. data/right_agent.gemspec +101 -0
  113. data/spec/actor_registry_spec.rb +80 -0
  114. data/spec/actor_spec.rb +162 -0
  115. data/spec/agent_config_spec.rb +235 -0
  116. data/spec/agent_identity_spec.rb +78 -0
  117. data/spec/agent_spec.rb +734 -0
  118. data/spec/agent_tag_manager_spec.rb +319 -0
  119. data/spec/clients/api_client_spec.rb +423 -0
  120. data/spec/clients/auth_client_spec.rb +272 -0
  121. data/spec/clients/balanced_http_client_spec.rb +576 -0
  122. data/spec/clients/base_retry_client_spec.rb +635 -0
  123. data/spec/clients/router_client_spec.rb +594 -0
  124. data/spec/clients/spec_helper.rb +111 -0
  125. data/spec/command/agent_manager_commands_spec.rb +51 -0
  126. data/spec/command/command_io_spec.rb +93 -0
  127. data/spec/command/command_parser_spec.rb +79 -0
  128. data/spec/command/command_runner_spec.rb +107 -0
  129. data/spec/command/command_serializer_spec.rb +51 -0
  130. data/spec/connectivity_checker_spec.rb +83 -0
  131. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  132. data/spec/core_payload_types/dev_repository_spec.rb +33 -0
  133. data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
  134. data/spec/core_payload_types/login_user_spec.rb +102 -0
  135. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  136. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  137. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  138. data/spec/core_payload_types/spec_helper.rb +23 -0
  139. data/spec/dispatched_cache_spec.rb +136 -0
  140. data/spec/dispatcher_spec.rb +324 -0
  141. data/spec/enrollment_result_spec.rb +53 -0
  142. data/spec/history_spec.rb +246 -0
  143. data/spec/log_spec.rb +192 -0
  144. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  145. data/spec/multiplexer_spec.rb +48 -0
  146. data/spec/offline_handler_spec.rb +340 -0
  147. data/spec/operation_result_spec.rb +208 -0
  148. data/spec/packets_spec.rb +461 -0
  149. data/spec/pending_requests_spec.rb +136 -0
  150. data/spec/platform/spec_helper.rb +216 -0
  151. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  152. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  153. data/spec/platform/unix/spec_helper.rb +149 -0
  154. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  155. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  156. data/spec/platform/windows/spec_helper.rb +720 -0
  157. data/spec/retryable_request_spec.rb +306 -0
  158. data/spec/secure_identity_spec.rb +50 -0
  159. data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
  160. data/spec/security/certificate_cache_spec.rb +71 -0
  161. data/spec/security/certificate_spec.rb +49 -0
  162. data/spec/security/distinguished_name_spec.rb +46 -0
  163. data/spec/security/encrypted_document_spec.rb +55 -0
  164. data/spec/security/rsa_key_pair_spec.rb +55 -0
  165. data/spec/security/signature_spec.rb +66 -0
  166. data/spec/security/static_certificate_store_spec.rb +58 -0
  167. data/spec/sender_spec.rb +1045 -0
  168. data/spec/serialize/message_pack_spec.rb +131 -0
  169. data/spec/serialize/secure_serializer_spec.rb +132 -0
  170. data/spec/serialize/serializable_spec.rb +90 -0
  171. data/spec/serialize/serializer_spec.rb +197 -0
  172. data/spec/spec.opts +2 -0
  173. data/spec/spec.win32.opts +1 -0
  174. data/spec/spec_helper.rb +130 -0
  175. data/spec/tracer_spec.rb +114 -0
  176. metadata +447 -0
@@ -0,0 +1,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