right_agent 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +78 -0
  3. data/Rakefile +86 -0
  4. data/lib/right_agent.rb +66 -0
  5. data/lib/right_agent/actor.rb +163 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +189 -0
  8. data/lib/right_agent/agent.rb +735 -0
  9. data/lib/right_agent/agent_config.rb +403 -0
  10. data/lib/right_agent/agent_identity.rb +209 -0
  11. data/lib/right_agent/agent_tags_manager.rb +213 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/broker_client.rb +683 -0
  14. data/lib/right_agent/command.rb +30 -0
  15. data/lib/right_agent/command/agent_manager_commands.rb +134 -0
  16. data/lib/right_agent/command/command_client.rb +136 -0
  17. data/lib/right_agent/command/command_constants.rb +42 -0
  18. data/lib/right_agent/command/command_io.rb +128 -0
  19. data/lib/right_agent/command/command_parser.rb +87 -0
  20. data/lib/right_agent/command/command_runner.rb +105 -0
  21. data/lib/right_agent/command/command_serializer.rb +63 -0
  22. data/lib/right_agent/console.rb +65 -0
  23. data/lib/right_agent/core_payload_types.rb +42 -0
  24. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  25. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  26. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  27. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  28. data/lib/right_agent/core_payload_types/dev_repositories.rb +90 -0
  29. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  30. data/lib/right_agent/core_payload_types/executable_bundle.rb +138 -0
  31. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  32. data/lib/right_agent/core_payload_types/login_user.rb +62 -0
  33. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  34. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +60 -0
  35. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  36. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  37. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +73 -0
  38. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  39. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  40. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  41. data/lib/right_agent/daemonize.rb +35 -0
  42. data/lib/right_agent/dispatcher.rb +348 -0
  43. data/lib/right_agent/enrollment_result.rb +217 -0
  44. data/lib/right_agent/exceptions.rb +30 -0
  45. data/lib/right_agent/ha_broker_client.rb +1278 -0
  46. data/lib/right_agent/idempotent_request.rb +140 -0
  47. data/lib/right_agent/log.rb +418 -0
  48. data/lib/right_agent/monkey_patches.rb +29 -0
  49. data/lib/right_agent/monkey_patches/amqp_patch.rb +274 -0
  50. data/lib/right_agent/monkey_patches/ruby_patch.rb +49 -0
  51. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  52. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  53. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  54. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  55. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  56. data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +46 -0
  57. data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +107 -0
  58. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +90 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  64. data/lib/right_agent/multiplexer.rb +91 -0
  65. data/lib/right_agent/operation_result.rb +270 -0
  66. data/lib/right_agent/packets.rb +637 -0
  67. data/lib/right_agent/payload_formatter.rb +104 -0
  68. data/lib/right_agent/pid_file.rb +159 -0
  69. data/lib/right_agent/platform.rb +319 -0
  70. data/lib/right_agent/platform/darwin.rb +227 -0
  71. data/lib/right_agent/platform/linux.rb +268 -0
  72. data/lib/right_agent/platform/windows.rb +1204 -0
  73. data/lib/right_agent/scripts/agent_controller.rb +522 -0
  74. data/lib/right_agent/scripts/agent_deployer.rb +379 -0
  75. data/lib/right_agent/scripts/common_parser.rb +153 -0
  76. data/lib/right_agent/scripts/log_level_manager.rb +193 -0
  77. data/lib/right_agent/scripts/stats_manager.rb +256 -0
  78. data/lib/right_agent/scripts/usage.rb +58 -0
  79. data/lib/right_agent/secure_identity.rb +92 -0
  80. data/lib/right_agent/security.rb +32 -0
  81. data/lib/right_agent/security/cached_certificate_store_proxy.rb +63 -0
  82. data/lib/right_agent/security/certificate.rb +102 -0
  83. data/lib/right_agent/security/certificate_cache.rb +89 -0
  84. data/lib/right_agent/security/distinguished_name.rb +56 -0
  85. data/lib/right_agent/security/encrypted_document.rb +84 -0
  86. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  87. data/lib/right_agent/security/signature.rb +86 -0
  88. data/lib/right_agent/security/static_certificate_store.rb +69 -0
  89. data/lib/right_agent/sender.rb +937 -0
  90. data/lib/right_agent/serialize.rb +29 -0
  91. data/lib/right_agent/serialize/message_pack.rb +102 -0
  92. data/lib/right_agent/serialize/secure_serializer.rb +131 -0
  93. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  94. data/lib/right_agent/serialize/serializable.rb +135 -0
  95. data/lib/right_agent/serialize/serializer.rb +149 -0
  96. data/lib/right_agent/stats_helper.rb +731 -0
  97. data/lib/right_agent/subprocess.rb +38 -0
  98. data/lib/right_agent/tracer.rb +124 -0
  99. data/right_agent.gemspec +60 -0
  100. data/spec/actor_registry_spec.rb +81 -0
  101. data/spec/actor_spec.rb +99 -0
  102. data/spec/agent_config_spec.rb +226 -0
  103. data/spec/agent_identity_spec.rb +75 -0
  104. data/spec/agent_spec.rb +571 -0
  105. data/spec/broker_client_spec.rb +961 -0
  106. data/spec/command/agent_manager_commands_spec.rb +51 -0
  107. data/spec/command/command_io_spec.rb +93 -0
  108. data/spec/command/command_parser_spec.rb +79 -0
  109. data/spec/command/command_runner_spec.rb +72 -0
  110. data/spec/command/command_serializer_spec.rb +51 -0
  111. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  112. data/spec/core_payload_types/executable_bundle_spec.rb +59 -0
  113. data/spec/core_payload_types/login_user_spec.rb +98 -0
  114. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  115. data/spec/core_payload_types/spec_helper.rb +23 -0
  116. data/spec/dispatcher_spec.rb +372 -0
  117. data/spec/enrollment_result_spec.rb +53 -0
  118. data/spec/ha_broker_client_spec.rb +1673 -0
  119. data/spec/idempotent_request_spec.rb +136 -0
  120. data/spec/log_spec.rb +177 -0
  121. data/spec/monkey_patches/amqp_patch_spec.rb +100 -0
  122. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  123. data/spec/monkey_patches/string_patch_spec.rb +99 -0
  124. data/spec/multiplexer_spec.rb +48 -0
  125. data/spec/operation_result_spec.rb +171 -0
  126. data/spec/packets_spec.rb +418 -0
  127. data/spec/platform/platform_spec.rb +60 -0
  128. data/spec/results_mock.rb +45 -0
  129. data/spec/secure_identity_spec.rb +50 -0
  130. data/spec/security/cached_certificate_store_proxy_spec.rb +56 -0
  131. data/spec/security/certificate_cache_spec.rb +71 -0
  132. data/spec/security/certificate_spec.rb +49 -0
  133. data/spec/security/distinguished_name_spec.rb +46 -0
  134. data/spec/security/encrypted_document_spec.rb +55 -0
  135. data/spec/security/rsa_key_pair_spec.rb +55 -0
  136. data/spec/security/signature_spec.rb +66 -0
  137. data/spec/security/static_certificate_store_spec.rb +52 -0
  138. data/spec/sender_spec.rb +887 -0
  139. data/spec/serialize/message_pack_spec.rb +131 -0
  140. data/spec/serialize/secure_serializer_spec.rb +102 -0
  141. data/spec/serialize/serializable_spec.rb +90 -0
  142. data/spec/serialize/serializer_spec.rb +174 -0
  143. data/spec/spec.opts +2 -0
  144. data/spec/spec_helper.rb +77 -0
  145. data/spec/stats_helper_spec.rb +681 -0
  146. data/spec/tracer_spec.rb +114 -0
  147. metadata +320 -0
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ require 'openssl'
6
+ require 'base64'
7
+ require 'json'
8
+
9
+ # A response to a RightNet enrollment request containing the mapper's X509 cert,
10
+ # the instance (or other) agent's identity cert, and the agent's private key.
11
+ # Responses are encrypted using a secret key shared between the instance and
12
+ # the Certifying Authority (aka the RightScale core site) and integrity-protected
13
+ # using a similar key.
14
+ module RightScale
15
+
16
+ class EnrollmentResult
17
+ # Versions 5 and above use an identical format for the enrollment result
18
+ SUPPORTED_VERSIONS = 5..AgentConfig.protocol_version
19
+
20
+ class IntegrityFailure < Exception; end
21
+ class VersionError < Exception; end
22
+
23
+ attr_reader :r_s_version, :timestamp, :mapper_cert, :id_cert, :id_key
24
+
25
+ # Create a new instance of this class
26
+ #
27
+ # === Parameters
28
+ # timestamp(Time):: Timestamp associated with this result
29
+ # mapper_cert(String):: Arbitrary string
30
+ # id_cert(String):: Arbitrary string
31
+ # id_key(String):: Arbitrary string
32
+ # secret(String):: Shared secret with which the result is encrypted
33
+ #
34
+ def initialize(r_s_version, timestamp, mapper_cert, id_cert, id_key, secret)
35
+ @r_s_version = r_s_version
36
+ @timestamp = timestamp.utc
37
+ @mapper_cert = mapper_cert
38
+ @id_cert = id_cert
39
+ @id_key = id_key
40
+ @serializer = Serializer.new((:json if r_s_version < 12))
41
+
42
+ msg = @serializer.dump({
43
+ 'mapper_cert' => @mapper_cert.to_s,
44
+ 'id_cert' => @id_cert,
45
+ 'id_key' => @id_key
46
+ })
47
+
48
+ key = EnrollmentResult.derive_key(secret, @timestamp.to_i.to_s)
49
+ #TODO switch to new OpenSSL API once we move to Ruby 1.8.7
50
+ #cipher = OpenSSL::Cipher.new("aes-256-cbc")
51
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
52
+ cipher.encrypt
53
+ cipher.key = key
54
+ cipher.iv = @iv = cipher.random_iv
55
+
56
+ @ciphertext = cipher.update(msg) + cipher.final
57
+
58
+ key = EnrollmentResult.derive_key(secret.to_s.reverse, @timestamp.to_i.to_s)
59
+ hmac = OpenSSL::HMAC.new(key, OpenSSL::Digest::SHA1.new)
60
+ hmac.update(@ciphertext)
61
+ @mac = hmac.digest
62
+ end
63
+
64
+ # Serialize an enrollment result
65
+ #
66
+ # === Parameters
67
+ # obj(EnrollmentResult):: Object to serialize
68
+ #
69
+ # === Return
70
+ # (String):: Serialized object
71
+ #
72
+ def to_s
73
+ @serializer.dump({
74
+ 'r_s_version' => @r_s_version.to_s,
75
+ 'timestamp' => @timestamp.to_i.to_s,
76
+ 'iv' => Base64::encode64(@iv).chop,
77
+ 'ciphertext' => Base64::encode64(@ciphertext).chop,
78
+ 'mac' => Base64::encode64(@mac).chop
79
+ })
80
+ end
81
+
82
+ # Compare this object to another one. Two results are equal if they have the same
83
+ # payload (certs and keys) and timestamp. The crypto fields are not included in
84
+ # the comparison because they are mutable.
85
+ #
86
+ # === Parameters
87
+ # o(EnrollmentResult):: Object to compare against
88
+ #
89
+ # === Return
90
+ # true|false:: Whether the objects' pertinent fields are identical
91
+ #
92
+ def ==(o)
93
+ self.mapper_cert == o.mapper_cert &&
94
+ self.id_cert == o.id_cert &&
95
+ self.id_key == o.id_key &&
96
+ self.timestamp.to_i == o.timestamp.to_i
97
+ end
98
+
99
+ # Serialize an enrollment result
100
+ #
101
+ # === Parameters
102
+ # obj(EnrollmentResult):: Object to serialize
103
+ #
104
+ # === Return
105
+ # (String):: Serialized object
106
+ #
107
+ def self.dump(obj)
108
+ raise VersionError.new("Unsupported version #{obj.r_s_version}") unless SUPPORTED_VERSIONS.include?(obj.r_s_version)
109
+ obj.to_s
110
+ end
111
+
112
+ # Unserialize the MessagePack encoded enrollment result
113
+ #
114
+ # === Parameters
115
+ # string(String):: MessagePack representation of the result
116
+ # secret(String):: Shared secret with which the result is encrypted
117
+ #
118
+ # === Return
119
+ # result:: An instance of EnrollmentResult
120
+ #
121
+ # === Raise
122
+ # IntegrityFailure:: if the message has been tampered with
123
+ # VersionError:: if the specified protocol version is not locally supported
124
+ #
125
+ def self.load(string, secret)
126
+ serializer = Serializer.new
127
+ envelope = serializer.load(string)
128
+
129
+ r_s_version = envelope['r_s_version'].to_i
130
+ raise VersionError.new("Unsupported version #{r_s_version}") unless SUPPORTED_VERSIONS.include?(r_s_version)
131
+
132
+ timestamp = Time.at(envelope['timestamp'].to_i)
133
+ iv = Base64::decode64(envelope['iv'])
134
+ ciphertext = Base64::decode64(envelope['ciphertext'])
135
+ mac = Base64::decode64(envelope['mac'])
136
+
137
+ key = self.derive_key(secret, timestamp.to_i.to_s)
138
+ #TODO switch to new OpenSSL API once we move to Ruby 1.8.7
139
+ #cipher = OpenSSL::Cipher.new("aes-256-cbc")
140
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
141
+ cipher.decrypt
142
+ cipher.key = key
143
+ cipher.iv = iv
144
+
145
+ #TODO exclusively use new OpenSSL API (OpenSSL::CipherError) once we move to Ruby 1.8.7
146
+ if defined?(OpenSSL::Cipher::CipherError)
147
+ begin
148
+ plaintext = cipher.update(ciphertext) + cipher.final
149
+ rescue OpenSSL::Cipher::CipherError => e
150
+ raise IntegrityFailure.new(e.message)
151
+ end
152
+ else
153
+ begin
154
+ plaintext = cipher.update(ciphertext) + cipher.final
155
+ rescue OpenSSL::CipherError => e
156
+ raise IntegrityFailure.new(e.message)
157
+ end
158
+ end
159
+
160
+ key = self.derive_key(secret.to_s.reverse, timestamp.to_i.to_s)
161
+ hmac = OpenSSL::HMAC.new(key, OpenSSL::Digest::SHA1.new)
162
+ hmac.update(ciphertext)
163
+ my_mac = hmac.digest
164
+ raise IntegrityFailure.new("MAC mismatch: expected #{my_mac}, got #{mac}") unless (mac == my_mac)
165
+
166
+ msg = serializer.load(plaintext)
167
+ mapper_cert = msg['mapper_cert']
168
+ id_cert = msg['id_cert']
169
+ id_key = msg['id_key']
170
+
171
+ self.new(r_s_version, timestamp, mapper_cert, id_cert, id_key, secret)
172
+ end
173
+
174
+ protected
175
+
176
+ def self.derive_key(secret, salt)
177
+ begin
178
+ return OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret, salt, 2048, 32)
179
+ rescue NameError => e
180
+ return pkcs5_pbkdf2_hmac_sha1(secret, salt, 2048, 32)
181
+ end
182
+ end
183
+
184
+ def self.pkcs5_pbkdf2_hmac_sha1(pass, salt, iter, len)
185
+ ret = ''
186
+ i = 0
187
+
188
+ while len > 0
189
+ i += 1
190
+ hmac = OpenSSL::HMAC.new(pass, OpenSSL::Digest::SHA1.new)
191
+ hmac.update(salt)
192
+ hmac.update([i].pack('N'))
193
+ digtmp = hmac.digest
194
+
195
+ cplen = len > digtmp.length ? digtmp.length : len
196
+
197
+ tmp = digtmp.dup
198
+
199
+ 1.upto(iter - 1) do |j|
200
+ hmac = OpenSSL::HMAC.new(pass, OpenSSL::Digest::SHA1.new)
201
+ hmac.update(digtmp)
202
+ digtmp = hmac.digest
203
+ 0.upto(cplen - 1) do |k|
204
+ tmp[k] = (tmp[k] ^ digtmp[k]).chr
205
+ end
206
+ end
207
+
208
+ tmp.slice!((cplen)..-1) if (tmp.length > cplen)
209
+ ret << tmp
210
+ len -= tmp.length
211
+ end
212
+
213
+ ret
214
+ end
215
+
216
+ end
217
+ end
@@ -0,0 +1,30 @@
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
+ class Exceptions
25
+ class Application < RuntimeError; end
26
+ class Argument < RuntimeError; end
27
+ class IO < RuntimeError; end
28
+ class PlatformError < StandardError; end
29
+ end
30
+ end
@@ -0,0 +1,1278 @@
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
+ # Client for multiple AMQP brokers to achieve a high availability service
26
+ class HABrokerClient
27
+
28
+ include StatsHelper
29
+
30
+ class NoUserData < Exception; end
31
+ class NoBrokerHosts < Exception; end
32
+ class NoConnectedBrokers < Exception; end
33
+
34
+ # Message publishing context
35
+ class Context
36
+
37
+ # (String) Message class name in lower snake case
38
+ attr_reader :name
39
+
40
+ # (String) Request type if applicable
41
+ attr_reader :type
42
+
43
+ # (String) Original sender of message if applicable
44
+ attr_reader :from
45
+
46
+ # (String) Generated message identifier if applicable
47
+ attr_reader :token
48
+
49
+ # (Boolean) Whether the packet is one that does not have an associated response
50
+ attr_reader :one_way
51
+
52
+ # (Hash) Options used to publish message
53
+ attr_reader :options
54
+
55
+ # (Array) Identity of candidate brokers when message was published
56
+ attr_reader :brokers
57
+
58
+ # (Array) Identity of brokers that have failed to deliver message with last one at end
59
+ attr_reader :failed
60
+
61
+ # Create context
62
+ #
63
+ # === Parameters
64
+ # packet(Packet):: Packet being published
65
+ # options(Hash):: Publish options
66
+ # brokers(Array):: Identity of candidate brokers
67
+ def initialize(packet, options, brokers)
68
+ @name = (packet.respond_to?(:name) ? packet.name : packet.class.name.snake_case)
69
+ @type = (packet.type if packet.respond_to?(:type) && packet.type != packet.class)
70
+ @from = (packet.from if packet.respond_to?(:from))
71
+ @token = (packet.token if packet.respond_to?(:token))
72
+ @one_way = (packet.respond_to?(:one_way) ? packet.one_way : true)
73
+ @options = options
74
+ @brokers = brokers
75
+ @failed = []
76
+ end
77
+
78
+ # Record delivery failure
79
+ #
80
+ # === Parameters
81
+ # identity(String):: Identity of broker that failed delivery
82
+ #
83
+ # === Return
84
+ # true:: Always return true
85
+ def record_failure(identity)
86
+ @failed << identity
87
+ end
88
+
89
+ end
90
+
91
+ # Default number of seconds between reconnect attempts
92
+ RECONNECT_INTERVAL = 60
93
+
94
+ # (Array(Broker)) Priority ordered list of AMQP broker clients (exposed only for unit test purposes)
95
+ attr_accessor :brokers
96
+
97
+ # (Integer|nil) Home island identifier, or nil if unknown
98
+ attr_reader :home_island
99
+
100
+ # Create connections to all configured AMQP brokers
101
+ # The constructed broker client list is in priority order with brokers in home island first
102
+ #
103
+ # === Parameters
104
+ # serializer(Serializer):: Serializer used for marshaling packets being published; if nil,
105
+ # has same effect as setting options :no_serialize and :no_unserialize
106
+ # options(Hash):: Configuration options
107
+ # :user(String):: User name
108
+ # :pass(String):: Password
109
+ # :vhost(String):: Virtual host path name
110
+ # :insist(Boolean):: Whether to suppress redirection of connection
111
+ # :reconnect_interval(Integer):: Number of seconds between reconnect attempts, defaults to RECONNECT_INTERVAL
112
+ # :host{String):: Comma-separated list of AMQP broker host names; if only one, it is reapplied
113
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
114
+ # and a short string to be used as a broker index; the index defaults to the list index,
115
+ # e.g., "host_a:0, host_c:2"
116
+ # :port(String|Integer):: Comma-separated list of AMQP broker port numbers corresponding to :host list;
117
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
118
+ # :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for the agent
119
+ # before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
120
+ # if the agent crashes. Value 0 means unlimited prefetch.
121
+ # :islands(IslandData):: Islands with host and port settings for which connections are to be
122
+ # created (takes precedence over any specified :host and :port option)
123
+ # :home_island(Integer):: Identifier for home island for this server usable for accessing
124
+ # :islands (takes precedence over any specified :host and :port option)
125
+ # :order(Symbol):: Broker selection order when publishing a message: :random or :priority,
126
+ # defaults to :priority, value can be overridden on publish call
127
+ # :exception_callback(Proc):: Callback activated on exception events with parameters
128
+ # exception(Exception):: Exception
129
+ # message(Packet):: Message being processed
130
+ # client(HABrokerClient):: Reference to this client
131
+ # :exception_on_receive_callback(Proc):: Callback activated on a receive exception with parameters
132
+ # message(String):: Message content that caused an exception
133
+ # exception(Exception):: Exception that was raised
134
+ #
135
+ # === Raise
136
+ # ArgumentError:: If :host and :port are not matched lists or :home_island is not found
137
+ def initialize(serializer, options = {})
138
+ @options = options.dup
139
+ @options[:update_status_callback] = lambda { |b, c| update_status(b, c) }
140
+ @options[:reconnect_interval] ||= RECONNECT_INTERVAL
141
+ @connection_status = {}
142
+ @serializer = serializer
143
+ @published = Published.new
144
+ reset_stats
145
+ @select = @options[:order] || :priority
146
+ islands = @options[:islands]
147
+ if islands
148
+ islands.each_value do |i|
149
+ @brokers = connect_island(i.broker_hosts, i.broker_ports, i) if i.id == @options[:home_island]
150
+ end
151
+ @home_island = @options[:home_island]
152
+ raise ArgumentError, "Could not find home island #{@home_island}" unless @brokers
153
+ islands.each_value do |i|
154
+ @brokers += connect_island(i.broker_hosts, i.broker_ports, i) if i.id != @home_island
155
+ end
156
+ else
157
+ @brokers = connect_island(@options[:host], @options[:port])
158
+ end
159
+ @closed = false
160
+ @brokers_hash = {}
161
+ @brokers.each { |b| @brokers_hash[b.identity] = b }
162
+ return_message { |i, r, m, t, c| handle_return(i, r, m, t, c) }
163
+ end
164
+
165
+ # Parse agent user data to extract broker host and port configuration
166
+ # Agents below r_s_version 9 only support using one broker
167
+ #
168
+ # === Parameters
169
+ # user_data(String):: Agent user data in <name>=<value>&<name>=<value>&... form
170
+ # with required name RS_rn_url and optional names RS_rn_host and RS_rn_port
171
+ #
172
+ # === Return
173
+ # (Array):: Broker hosts and ports as comma-separated list in priority order in the form
174
+ # <hostname>:<index>,<hostname>:<index>,...
175
+ # <port>:<index>,<port>:<index>,... or nil if none specified
176
+ #
177
+ # === Raise
178
+ # NoUserData:: If the user data is missing
179
+ # NoBrokerHosts:: If no brokers could be extracted from the user data
180
+ def self.parse_user_data(user_data)
181
+ raise NoUserData.new("User data is missing") if user_data.nil? || user_data.empty?
182
+ hosts = ""
183
+ ports = nil
184
+ user_data.split("&").each do |data|
185
+ name, value = data.split("=")
186
+ if name == "RS_rn_url"
187
+ h = value.split("@").last.split("/").first
188
+ # Translate host name used by very old agents using only one broker
189
+ h = "broker1-1.rightscale.com" if h == "broker.rightscale.com"
190
+ hosts = h + hosts
191
+ end
192
+ if name == "RS_rn_host"
193
+ hosts << value
194
+ end
195
+ if name == "RS_rn_port"
196
+ ports = value
197
+ end
198
+ end
199
+ raise NoBrokerHosts.new("No brokers found in user data") if hosts.empty?
200
+ [hosts, ports]
201
+ end
202
+
203
+ # Parse host and port information to form list of broker address information
204
+ #
205
+ # === Parameters
206
+ # host{String):: Comma-separated list of broker host names; if only one, it is reapplied
207
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
208
+ # and a short string to be used as a broker index; the index defaults to the list index,
209
+ # e.g., "host_a:0, host_c:2"
210
+ # port(String|Integer):: Comma-separated list of broker port numbers corresponding to :host list;
211
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
212
+ #
213
+ # === Returns
214
+ # (Array(Hash)):: List of broker addresses with keys :host, :port, :index
215
+ #
216
+ # === Raise
217
+ # ArgumentError:: If host and port are not matched lists
218
+ def self.addresses(host, port)
219
+ hosts = if host && !host.empty? then host.split(/,\s*/) else [ "localhost" ] end
220
+ ports = if port && port.size > 0 then port.to_s.split(/,\s*/) else [ ::AMQP::PORT ] end
221
+ if hosts.size != ports.size && hosts.size != 1 && ports.size != 1
222
+ raise ArgumentError.new("Unmatched AMQP host/port lists -- hosts: #{host.inspect} ports: #{port.inspect}")
223
+ end
224
+ i = -1
225
+ if hosts.size > 1
226
+ hosts.map do |host|
227
+ i += 1
228
+ h = host.split(/:\s*/)
229
+ port = if ports[i] then ports[i].to_i else ports[0].to_i end
230
+ port = port.to_s.split(/:\s*/)[0]
231
+ {:host => h[0], :port => port.to_i, :index => (h[1] || i.to_s).to_i}
232
+ end
233
+ else
234
+ ports.map do |port|
235
+ i += 1
236
+ p = port.to_s.split(/:\s*/)
237
+ host = if hosts[i] then hosts[i] else hosts[0] end
238
+ host = host.split(/:\s*/)[0]
239
+ {:host => host, :port => p[0].to_i, :index => (p[1] || i.to_s).to_i}
240
+ end
241
+ end
242
+ end
243
+
244
+ # Parse host and port information to form list of broker identities
245
+ #
246
+ # === Parameters
247
+ # host{String):: Comma-separated list of broker host names; if only one, it is reapplied
248
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
249
+ # and a short string to be used as a broker index; the index defaults to the list index,
250
+ # e.g., "host_a:0, host_c:2"
251
+ # port(String|Integer):: Comma-separated list of broker port numbers corresponding to :host list;
252
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
253
+ #
254
+ # === Returns
255
+ # (Array):: Identity of each broker
256
+ #
257
+ # === Raise
258
+ # ArgumentError:: If host and port are not matched lists
259
+ def self.identities(host, port = nil)
260
+ addresses(host, port).map { |a| identity(a[:host], a[:port]) }
261
+ end
262
+
263
+ # Construct a broker serialized identity from its host and port of the form
264
+ # rs-broker-host-port, with any '-'s in host replaced by '~'
265
+ #
266
+ # === Parameters
267
+ # host{String):: IP host name or address for individual broker
268
+ # port(Integer):: TCP port number for individual broker, defaults to ::AMQP::PORT
269
+ #
270
+ # === Returns
271
+ # (String):: Broker serialized identity
272
+ def self.identity(host, port = ::AMQP::PORT)
273
+ AgentIdentity.new('rs', 'broker', port.to_i, host.gsub('-', '~')).to_s
274
+ end
275
+
276
+ # Break broker serialized identity down into individual parts if exists
277
+ #
278
+ # === Parameters
279
+ # id(Integer|String):: Broker alias or serialized identity
280
+ #
281
+ # === Return
282
+ # (Array):: Host, port, index, priority, and island_id, or all nil if broker not found
283
+ def identity_parts(id)
284
+ @brokers.each do |b|
285
+ return [b.host, b.port, b.index, priority(b.identity, b.island_id)[0], b.island_id] if b.identity == id || b.alias == id
286
+ end
287
+ [nil, nil, nil, nil, nil]
288
+ end
289
+
290
+ # Convert broker identities to aliases
291
+ #
292
+ # === Parameters
293
+ # identities(Array):: Broker identities
294
+ #
295
+ # === Return
296
+ # (Array):: Broker aliases
297
+ def aliases(identities)
298
+ identities.map { |i| alias_(i) }
299
+ end
300
+
301
+ # Convert broker serialized identity to its alias
302
+ #
303
+ # === Parameters
304
+ # identity(String):: Broker serialized identity
305
+ #
306
+ # === Return
307
+ # (String|nil):: Broker alias, or nil if not a known broker
308
+ def alias_(identity)
309
+ @brokers_hash[identity].alias rescue nil
310
+ end
311
+
312
+ # Form string of hosts and associated indices for an island
313
+ #
314
+ # === Parameters
315
+ # island_id(Integer|nil):: Island identifier, defaults to home island
316
+ #
317
+ # === Return
318
+ # (String):: Comma separated list of host:index
319
+ def hosts(island_id = nil)
320
+ in_island(island_id).map { |b| "#{b.host}:#{b.index}" }.join(",")
321
+ end
322
+
323
+ # Form string of hosts and associated indices for an island
324
+ #
325
+ # === Parameters
326
+ # island_id(Integer|nil):: Island identifier, defaults to home island
327
+ #
328
+ # === Return
329
+ # (String):: Comma separated list of host:index
330
+ def ports(island_id = nil)
331
+ in_island(island_id).map { |b| "#{b.port}:#{b.index}" }.join(",")
332
+ end
333
+
334
+ # Get broker serialized identity if client exists
335
+ #
336
+ # === Parameters
337
+ # id(Integer|String):: Broker alias or serialized identity
338
+ #
339
+ # === Return
340
+ # (String|nil):: Broker serialized identity if client found, otherwise nil
341
+ def get(id)
342
+ @brokers.each { |b| return b.identity if b.identity == id || b.alias == id }
343
+ nil
344
+ end
345
+
346
+ # Check whether connected to broker
347
+ #
348
+ # === Parameters
349
+ # identity{String):: Broker serialized identity
350
+ #
351
+ # === Return
352
+ # (Boolean):: true if connected to broker, otherwise false, or nil if broker unknown
353
+ def connected?(identity)
354
+ @brokers_hash[identity].connected? rescue nil
355
+ end
356
+
357
+ # Get serialized identity of connected brokers for an island
358
+ #
359
+ # === Parameters
360
+ # island_id(Integer|nil):: Island identifier, defaults to home island
361
+ #
362
+ # === Return
363
+ # (Array):: Serialized identity of connected brokers
364
+ def connected(island_id = nil)
365
+ in_island(island_id).inject([]) { |c, b| if b.connected? then c << b.identity else c end }
366
+ end
367
+
368
+ # Get serialized identity of connected brokers for all islands
369
+ #
370
+ # === Return
371
+ # (Array):: Serialized identity of connected brokers
372
+ def all_connected
373
+ @brokers.inject([]) { |c, b| if b.connected? then c << b.identity else c end }
374
+ end
375
+
376
+ # Get serialized identity of brokers that are usable, i.e., connecting or confirmed connected
377
+ #
378
+ # === Return
379
+ # (Array):: Serialized identity of usable brokers
380
+ def usable
381
+ each_usable.map { |b| b.identity }
382
+ end
383
+
384
+ # Get serialized identity of unusable brokers
385
+ #
386
+ # === Return
387
+ # (Array):: Serialized identity of unusable brokers
388
+ def unusable
389
+ @brokers.map { |b| b.identity } - each_usable.map { |b| b.identity }
390
+ end
391
+
392
+ # Get serialized identity of all brokers
393
+ #
394
+ # === Return
395
+ # (Array):: Serialized identity of all brokers
396
+ def all
397
+ @brokers.map { |b| b.identity }
398
+ end
399
+
400
+ # Get serialized identity of brokers in home island
401
+ #
402
+ # === Return
403
+ # (Array):: Serialized identity of brokers
404
+ def home
405
+ island
406
+ end
407
+
408
+ # Get serialized identity of brokers in given island
409
+ #
410
+ # === Parameters
411
+ # island_id(Integer|nil):: Island identifier, defaults to home island
412
+ #
413
+ # === Return
414
+ # (Array):: Serialized identity of brokers
415
+ def island(island_id = nil)
416
+ in_island(island_id).map { |b| b.identity }
417
+ end
418
+
419
+ # Get serialized identity of failed broker clients, i.e., ones that were never successfully
420
+ # connected, not ones that are just disconnected
421
+ #
422
+ # === Return
423
+ # (Array):: Serialized identity of failed broker clients
424
+ def failed
425
+ @brokers.inject([]) { |c, b| b.failed? ? c << b.identity : c }
426
+ end
427
+
428
+ # Make new connection to broker at specified address unless already connected
429
+ # or currently connecting
430
+ #
431
+ # === Parameters
432
+ # host{String):: IP host name or address for individual broker
433
+ # port(Integer):: TCP port number for individual broker
434
+ # index(Integer):: Unique index for broker within given island for use in forming alias
435
+ # priority(Integer|nil):: Priority position of this broker in list for use by this agent
436
+ # with nil or a value that would leave a gap in the list meaning add to end of list
437
+ # island(IslandData|nil):: Island containing this broker, defaults to home island
438
+ # force(Boolean):: Reconnect even if already connected
439
+ #
440
+ # === Block
441
+ # Optional block with following parameters to be called after initiating the connection
442
+ # unless already connected to this broker:
443
+ # identity(String):: Broker serialized identity
444
+ #
445
+ # === Return
446
+ # (Boolean):: true if connected, false if no connect attempt made
447
+ #
448
+ # === Raise
449
+ # Exception:: If host and port do not match an existing broker but index does
450
+ def connect(host, port, index, priority = nil, island = nil, force = false, &blk)
451
+ identity = self.class.identity(host, port)
452
+ existing = @brokers_hash[identity]
453
+ if existing && existing.usable? && !force
454
+ Log.info("Ignored request to reconnect #{identity} because already #{existing.status.to_s}")
455
+ false
456
+ else
457
+ @brokers.each do |b|
458
+ if index == b.index && (island.nil? || in_island?(b, island.id))
459
+ raise Exception, "Not allowed to change host or port of existing broker #{identity}, " +
460
+ "alias #{b.alias}, to #{host} and #{port.inspect}"
461
+ end
462
+ end unless existing
463
+
464
+ address = {:host => host, :port => port, :index => index}
465
+ broker = BrokerClient.new(identity, address, @serializer, @exceptions, @options, island, existing)
466
+ island_id = island && island.id
467
+ p, i = priority(identity, island_id)
468
+ if priority && priority < p
469
+ @brokers.insert(i + priority, broker)
470
+ elsif priority && priority > p
471
+ Log.info("Reduced priority setting for broker #{identity} from #{priority} to #{p} to avoid gap in list")
472
+ @brokers.insert(i + p, broker)
473
+ else
474
+ i += p
475
+ if @brokers[i] && @brokers[i].island_id == island_id
476
+ @brokers[i].close
477
+ @brokers[i] = broker
478
+ elsif @brokers[i]
479
+ @brokers.insert(i, broker)
480
+ else
481
+ @brokers[i] = broker
482
+ end
483
+ end
484
+ @brokers_hash[identity] = broker
485
+ yield broker.identity if block_given?
486
+ true
487
+ end
488
+ end
489
+
490
+ # Connect to any brokers in islands for which not currently connected
491
+ # Remove any broker clients for islands in which they are no longer configured
492
+ #
493
+ # === Parameters
494
+ # islands(Array):: List of islands as IslandData object
495
+ #
496
+ # === Return
497
+ # identities(Array):: Identity of newly connected brokers
498
+ def connect_update(islands)
499
+ old = all
500
+ new = []
501
+ islands.each_value do |i|
502
+ priority = 0
503
+ self.class.addresses(i.broker_hosts, i.broker_ports).each do |a|
504
+ identity = self.class.identity(a[:host], a[:port])
505
+ if @brokers_hash[identity]
506
+ old.delete(identity)
507
+ else
508
+ new << identity if connect(a[:host], a[:port], a[:index], priority, i)
509
+ end
510
+ priority += 1
511
+ end
512
+ end
513
+
514
+ old.each do |identity|
515
+ b = @brokers_hash[identity]
516
+ remove(b.host, b.port)
517
+ end
518
+ { :add => new, :remove => old, :home => home }
519
+ end
520
+
521
+ # Subscribe an AMQP queue to an AMQP exchange on all broker clients that are connected or still connecting
522
+ # Allow connecting here because subscribing may happen before all have confirmed connected
523
+ # Do not wait for confirmation from broker client that subscription is complete
524
+ # When a message is received, acknowledge, unserialize, and log it as specified
525
+ # If the message is unserialized and it is not of the right type, it is dropped after logging a warning
526
+ #
527
+ # === Parameters
528
+ # queue(Hash):: AMQP queue being subscribed with keys :name and :options,
529
+ # which are the standard AMQP ones plus
530
+ # :no_declare(Boolean):: Whether to skip declaring this queue on the broker
531
+ # to cause its creation; for use when client does not have permission to create or
532
+ # knows the queue already exists and wants to avoid declare overhead
533
+ # exchange(Hash|nil):: AMQP exchange to subscribe to with keys :type, :name, and :options,
534
+ # nil means use empty exchange by directly subscribing to queue; the :options are the
535
+ # standard AMQP ones plus
536
+ # :no_declare(Boolean):: Whether to skip declaring this exchange on the broker
537
+ # to cause its creation; for use when client does not have create permission or
538
+ # knows the exchange already exists and wants to avoid declare overhead
539
+ # options(Hash):: Subscribe options:
540
+ # :ack(Boolean):: Explicitly acknowledge received messages to AMQP
541
+ # :no_unserialize(Boolean):: Do not unserialize message, this is an escape for special
542
+ # situations like enrollment, also implicitly disables receive filtering and logging;
543
+ # this option is implicitly invoked if initialize without a serializer
544
+ # (packet class)(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info,
545
+ # only packet classes specified are accepted, others are not processed but are logged with error
546
+ # :category(String):: Packet category description to be used in error messages
547
+ # :log_data(String):: Additional data to display at end of log entry
548
+ # :no_log(Boolean):: Disable receive logging unless debug level
549
+ # :exchange2(Hash):: Additional exchange to which same queue is to be bound
550
+ # :brokers(Array):: Identity of brokers for which to subscribe, defaults to all usable if nil or empty
551
+ #
552
+ # === Block
553
+ # Block with following parameters to be called each time exchange matches a message to the queue:
554
+ # identity(String):: Serialized identity of broker delivering the message
555
+ # message(Packet|String):: Message received, which is unserialized unless :no_unserialize was specified
556
+ #
557
+ # === Return
558
+ # identities(Array):: Identity of brokers where successfully subscribed
559
+ def subscribe(queue, exchange = nil, options = {}, &blk)
560
+ identities = []
561
+ each_usable(options[:brokers]) { |b| identities << b.identity if b.subscribe(queue, exchange, options, &blk) }
562
+ Log.info("Could not subscribe to queue #{queue.inspect} on exchange #{exchange.inspect} " +
563
+ "on brokers #{each_usable(options[:brokers]).inspect} when selected #{options[:brokers].inspect} " +
564
+ "from usable #{usable.inspect}") if identities.empty?
565
+ identities
566
+ end
567
+
568
+ # Unsubscribe from the specified queues on usable broker clients
569
+ # Silently ignore unknown queues
570
+ #
571
+ # === Parameters
572
+ # queue_names(Array):: Names of queues previously subscribed to
573
+ # timeout(Integer):: Number of seconds to wait for all confirmations, defaults to no timeout
574
+ #
575
+ # === Block
576
+ # Optional block with no parameters to be called after all queues are unsubscribed
577
+ #
578
+ # === Return
579
+ # true:: Always return true
580
+ def unsubscribe(queue_names, timeout = nil, &blk)
581
+ count = each_usable.inject(0) do |c, b|
582
+ c + b.queues.inject(0) { |c, q| c + (queue_names.include?(q.name) ? 1 : 0) }
583
+ end
584
+ if count == 0
585
+ blk.call if blk
586
+ else
587
+ handler = CountedDeferrable.new(count, timeout)
588
+ handler.callback { blk.call if blk }
589
+ each_usable { |b| b.unsubscribe(queue_names) { handler.completed_one } }
590
+ end
591
+ true
592
+ end
593
+
594
+ # Declare queue or exchange object but do not subscribe to it
595
+ #
596
+ # === Parameters
597
+ # type(Symbol):: Type of object: :queue, :direct, :fanout or :topic
598
+ # name(String):: Name of object
599
+ # options(Hash):: Standard AMQP declare options plus
600
+ # :brokers(Array):: Identity of brokers for which to declare, defaults to all usable if nil or empty
601
+ #
602
+ # === Return
603
+ # identities(Array):: Identity of brokers where successfully declared
604
+ def declare(type, name, options = {})
605
+ identities = []
606
+ each_usable(options[:brokers]) { |b| identities << b.identity if b.declare(type, name, options) }
607
+ Log.info("Could not declare #{type.to_s} #{name.inspect} on brokers #{each_usable(options[:brokers]).inspect} " +
608
+ "when selected #{options[:brokers].inspect} from usable #{usable.inspect}") if identities.empty?
609
+ identities
610
+ end
611
+
612
+ # Publish message to AMQP exchange of first connected broker
613
+ # Only use home island brokers by default
614
+ #
615
+ # === Parameters
616
+ # exchange(Hash):: AMQP exchange to subscribe to with keys :type, :name, and :options,
617
+ # which are the standard AMQP ones plus
618
+ # :no_declare(Boolean):: Whether to skip declaring this exchange or queue on the broker
619
+ # to cause its creation; for use when client does not have create permission or
620
+ # knows the object already exists and wants to avoid declare overhead
621
+ # :declare(Boolean):: Whether to delete this exchange or queue from the AMQP cache
622
+ # to force it to be declared on the broker and thus be created if it does not exist
623
+ # packet(Packet):: Message to serialize and publish
624
+ # options(Hash):: Publish options -- standard AMQP ones plus
625
+ # :fanout(Boolean):: true means publish to all connected brokers
626
+ # :brokers(Array):: Identity of brokers selected for use, defaults to all home brokers
627
+ # if nil or empty
628
+ # :order(Symbol):: Broker selection order: :random or :priority,
629
+ # defaults to @select if :brokers is nil, otherwise defaults to :priority
630
+ # :no_serialize(Boolean):: Do not serialize packet because it is already serialized,
631
+ # this is an escape for special situations like enrollment, also implicitly disables
632
+ # publish logging; this option is implicitly invoked if initialize without a serializer
633
+ # :log_filter(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info
634
+ # :log_data(String):: Additional data to display at end of log entry
635
+ # :no_log(Boolean):: Disable publish logging unless debug level
636
+ #
637
+ # === Return
638
+ # identities(Array):: Identity of brokers where packet was successfully published
639
+ #
640
+ # === Raise
641
+ # NoConnectedBrokers:: If cannot find a connected broker
642
+ def publish(exchange, packet, options = {})
643
+ identities = []
644
+ no_serialize = options[:no_serialize] || @serializer.nil?
645
+ message = if no_serialize then packet else @serializer.dump(packet) end
646
+ brokers = use(options)
647
+ brokers.each do |b|
648
+ if b.publish(exchange, packet, message, options.merge(:no_serialize => no_serialize))
649
+ identities << b.identity
650
+ if options[:mandatory] && !no_serialize
651
+ context = Context.new(packet, options, brokers.map { |b| b.identity })
652
+ @published.store(message, context)
653
+ end
654
+ break unless options[:fanout]
655
+ end
656
+ end
657
+ if identities.empty?
658
+ selected = "selected " if options[:brokers]
659
+ list = aliases(brokers.map { |b| b.identity }).join(", ")
660
+ raise NoConnectedBrokers, "None of #{selected}brokers [#{list}] are usable for publishing"
661
+ end
662
+ identities
663
+ end
664
+
665
+ # Register callback to be activated when a broker returns a message that could not be delivered
666
+ # A message published with :mandatory => true is returned if the exchange does not have any associated queues
667
+ # or if all the associated queues do not have any consumers
668
+ # A message published with :immediate => true is returned for the same reasons as :mandatory plus if all
669
+ # of the queues associated with the exchange are not immediately ready to consume the message
670
+ # Remove any previously registered callback
671
+ #
672
+ # === Block
673
+ # Required block to be called when a message is returned with parameters
674
+ # identity(String):: Broker serialized identity
675
+ # reason(String):: Reason for return
676
+ # "NO_ROUTE" - queue does not exist
677
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
678
+ # all consumers are not immediately ready to consume
679
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
680
+ # message(String):: Returned serialized message
681
+ # to(String):: Queue to which message was published
682
+ # context(Context|nil):: Message publishing context, or nil if not available
683
+ #
684
+ # === Return
685
+ # true:: Always return true
686
+ def return_message(&blk)
687
+ each_usable do |b|
688
+ b.return_message do |to, reason, message|
689
+ context = @published.fetch(message)
690
+ context.record_failure(b.identity) if context
691
+ blk.call(b.identity, reason, message, to, context)
692
+ end
693
+ end
694
+ true
695
+ end
696
+
697
+ # Provide callback to be activated when a message cannot be delivered
698
+ #
699
+ # === Block
700
+ # Required block with parameters
701
+ # reason(String):: Non-delivery reason
702
+ # "NO_ROUTE" - queue does not exist
703
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
704
+ # all consumers are not immediately ready to consume
705
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
706
+ # type(String|nil):: Request type, or nil if not applicable
707
+ # token(String|nil):: Generated message identifier, or nil if not applicable
708
+ # from(String|nil):: Identity of original sender of message, or nil if not applicable
709
+ # to(String):: Queue to which message was published
710
+ #
711
+ # === Return
712
+ # true:: Always return true
713
+ def non_delivery(&blk)
714
+ @non_delivery = blk
715
+ true
716
+ end
717
+
718
+ # Delete queue in all usable brokers or all selected brokers that are usable
719
+ #
720
+ # === Parameters
721
+ # name(String):: Queue name
722
+ # options(Hash):: Queue declare options plus
723
+ # :brokers(Array):: Identity of brokers in which queue is to be deleted
724
+ #
725
+ # === Return
726
+ # identities(Array):: Identity of brokers where queue was deleted
727
+ def delete(name, options = {})
728
+ identities = []
729
+ u = usable
730
+ ((options[:brokers] || u) & u).each { |i| identities << i if (b = @brokers_hash[i]) && b.delete(name, options) }
731
+ identities
732
+ end
733
+
734
+ # Remove a broker client from the configuration
735
+ # Invoke connection status callbacks only if connection is not already disabled
736
+ # There is no check whether this is the last usable broker client
737
+ #
738
+ # === Parameters
739
+ # host{String):: IP host name or address for individual broker
740
+ # port(Integer):: TCP port number for individual broker
741
+ #
742
+ # === Block
743
+ # Optional block with following parameters to be called after removing the connection
744
+ # unless broker is not configured
745
+ # identity(String):: Broker serialized identity
746
+ #
747
+ # === Return
748
+ # identity(String|nil):: Serialized identity of broker removed, or nil if unknown
749
+ def remove(host, port, &blk)
750
+ identity = self.class.identity(host, port)
751
+ if broker = @brokers_hash[identity]
752
+ Log.info("Removing #{identity}, alias #{broker.alias} from broker list")
753
+ broker.close(propagate = true, normal = true, log = false)
754
+ @brokers_hash.delete(identity)
755
+ @brokers.reject! { |b| b.identity == identity }
756
+ yield identity if block_given?
757
+ else
758
+ Log.info("Ignored request to remove #{identity} from broker list because unknown")
759
+ identity = nil
760
+ end
761
+ identity
762
+ end
763
+
764
+ # Declare a broker client as unusable
765
+ #
766
+ # === Parameters
767
+ # identities(Array):: Identity of brokers
768
+ #
769
+ # === Return
770
+ # true:: Always return true
771
+ #
772
+ # === Raises
773
+ # Exception:: If identified broker is unknown
774
+ def declare_unusable(identities)
775
+ identities.each do |id|
776
+ broker = @brokers_hash[id]
777
+ raise Exception, "Cannot mark unknown broker #{id} unusable" unless broker
778
+ broker.close(propagate = true, normal = false, log = false)
779
+ end
780
+ end
781
+
782
+ # Close all broker client connections
783
+ #
784
+ # === Block
785
+ # Optional block with no parameters to be called after all connections are closed
786
+ #
787
+ # === Return
788
+ # true:: Always return true
789
+ def close(&blk)
790
+ if @closed
791
+ blk.call if blk
792
+ else
793
+ @closed = true
794
+ @connection_status = {}
795
+ handler = CountedDeferrable.new(@brokers.size)
796
+ handler.callback { blk.call if blk }
797
+ @brokers.each do |b|
798
+ begin
799
+ b.close(propagate = false) { handler.completed_one }
800
+ rescue Exception => e
801
+ handler.completed_one
802
+ Log.error("Failed to close broker #{b.alias}", e, :trace)
803
+ @exceptions.track("close", e)
804
+ end
805
+ end
806
+ end
807
+ true
808
+ end
809
+
810
+ # Close an individual broker client connection
811
+ #
812
+ # === Parameters
813
+ # identity(String):: Broker serialized identity
814
+ # propagate(Boolean):: Whether to propagate connection status updates
815
+ #
816
+ # === Block
817
+ # Optional block with no parameters to be called after connection closed
818
+ #
819
+ # === Return
820
+ # true:: Always return true
821
+ #
822
+ # === Raise
823
+ # Exception:: If broker unknown
824
+ def close_one(identity, propagate = true, &blk)
825
+ broker = @brokers_hash[identity]
826
+ raise Exception, "Cannot close unknown broker #{identity}" unless broker
827
+ broker.close(propagate, &blk)
828
+ true
829
+ end
830
+
831
+ # Register callback to be activated when there is a change in connection status
832
+ # Connection status change is measured individually for each island
833
+ # Can be called more than once without affecting previous callbacks
834
+ #
835
+ # === Parameters
836
+ # options(Hash):: Connection status monitoring options
837
+ # :one_off(Integer):: Seconds to wait for status change; only send update once;
838
+ # if timeout, report :timeout as the status
839
+ # :boundary(Symbol):: :any if only report change on any (0/1) boundary,
840
+ # :all if only report change on all (n-1/n) boundary, defaults to :any
841
+ # :brokers(Array):: Only report a status change for these identified brokers
842
+ #
843
+ # === Block
844
+ # Required block activated when connected count crosses a status boundary with following parameters
845
+ # status(Symbol):: Status of connection: :connected, :disconnected, or :failed, with
846
+ # :failed indicating that all selected brokers or all brokers have failed
847
+ # island_id(Integer):: Island in which there was a change (optional parameter)
848
+ #
849
+ # === Return
850
+ # id(String):: Identifier associated with connection status request
851
+ def connection_status(options = {}, &callback)
852
+ id = AgentIdentity.generate
853
+ @connection_status[id] = {:boundary => options[:boundary], :brokers => options[:brokers], :callback => callback}
854
+ if timeout = options[:one_off]
855
+ @connection_status[id][:timer] = EM::Timer.new(timeout) do
856
+ if @connection_status[id]
857
+ if @connection_status[id][:callback].arity == 2
858
+ @connection_status[id][:callback].call(:timeout, nil)
859
+ else
860
+ @connection_status[id][:callback].call(:timeout)
861
+ end
862
+ @connection_status.delete(id)
863
+ end
864
+ end
865
+ end
866
+ id
867
+ end
868
+
869
+ # Get status summary
870
+ #
871
+ # === Return
872
+ # (Array(Hash)):: Status of each configured broker with keys
873
+ # :identity(String):: Broker serialized identity
874
+ # :alias(String):: Broker alias used in logs
875
+ # :status(Symbol):: Status of connection
876
+ # :disconnects(Integer):: Number of times lost connection
877
+ # :failures(Integer):: Number of times connect failed
878
+ # :retries(Integer):: Number of attempts to connect after failure
879
+ def status
880
+ @brokers.map { |b| b.summary }
881
+ end
882
+
883
+ # Get broker client statistics
884
+ #
885
+ # === Parameters:
886
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
887
+ #
888
+ # === Return
889
+ # stats(Hash):: Broker client stats with keys
890
+ # "brokers"(Array):: Stats for each broker client in priority order
891
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
892
+ # "total"(Integer):: Total exceptions for this category
893
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
894
+ # "returns"(Hash|nil):: Message return activity stats with keys "total", "percent", "last", and "rate"
895
+ # with percentage breakdown per return reason, or nil if none
896
+ def stats(reset = false)
897
+ stats = {
898
+ "brokers" => @brokers.map { |b| b.stats },
899
+ "exceptions" => @exceptions.stats,
900
+ "returns" => @returns.all
901
+ }
902
+ reset_stats if reset
903
+ stats
904
+ end
905
+
906
+ # Reset broker client statistics
907
+ # Do not reset disconnect and failure stats because they might then be
908
+ # inconsistent with underlying connection status
909
+ #
910
+ # === Return
911
+ # true:: Always return true
912
+ def reset_stats
913
+ @returns = ActivityStats.new
914
+ @exceptions = ExceptionStats.new(self, @options[:exception_callback])
915
+ true
916
+ end
917
+
918
+ protected
919
+
920
+ # Determine whether broker is in given island
921
+ #
922
+ # === Parameters
923
+ # broker(BrokerClient):: Broker client
924
+ # island_id(Integer|nil):: Island identifier, defaults to home island
925
+ #
926
+ # === Return
927
+ # (Boolean):: true if in island, otherwise false
928
+ def in_island?(broker, island_id = nil)
929
+ (!island_id && broker.in_home_island) || (island_id && island_id == broker.island_id)
930
+ end
931
+
932
+ # Get clients for brokers in a given island
933
+ #
934
+ # === Parameters
935
+ # island_id(Integer|nil):: Island identifier, defaults to home island
936
+ #
937
+ # === Return
938
+ # (Array):: Broker clients in given island
939
+ def in_island(island_id = nil)
940
+ @brokers.select { |b| in_island?(b, island_id) }
941
+ end
942
+
943
+ # Connect to all brokers in island
944
+ #
945
+ # === Parameters
946
+ # host{String):: Comma-separated list of AMQP broker host names; if only one, it is reapplied
947
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
948
+ # and a short string to be used as a broker index; the index defaults to the list index,
949
+ # e.g., "host_a:0, host_c:2"
950
+ # port(String|Integer):: Comma-separated list of AMQP broker port numbers corresponding to :host list;
951
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
952
+ # island(IslandData|nil):: Island containing these brokers, defaults to home island
953
+ #
954
+ # === Return
955
+ # (Array):: Broker clients created
956
+ def connect_island(host, port, island = nil)
957
+ self.class.addresses(host, port).map do |a|
958
+ identity = self.class.identity(a[:host], a[:port])
959
+ BrokerClient.new(identity, a, @serializer, @exceptions, @options, island, nil)
960
+ end
961
+ end
962
+
963
+ # Determine priority of broker within island
964
+ # If broker not found, assign next available priority
965
+ #
966
+ # === Parameters
967
+ # identity(String):: Broker identity
968
+ # island_id(Integer|nil):: Island identifier, defaults to home island
969
+ #
970
+ # === Return
971
+ # (Array):: Priority and broker array index where island starts
972
+ def priority(identity, island_id = nil)
973
+ index = 0
974
+ priority = 0
975
+ found_island = false
976
+ @brokers.each do |b|
977
+ if in_island?(b, island_id)
978
+ found_island = true
979
+ break if b.identity == identity
980
+ priority += 1
981
+ elsif found_island
982
+ break
983
+ end
984
+ index += 1 unless found_island
985
+ end
986
+ [priority, index]
987
+ end
988
+
989
+ # Iterate over clients that are usable, i.e., connecting or confirmed connected
990
+ #
991
+ # === Parameters
992
+ # identities(Array):: Identity of brokers to be considered, nil or empty array means all brokers
993
+ #
994
+ # === Block
995
+ # Optional block with following parameters to be called for each usable broker client
996
+ # broker(BrokerClient):: Broker client
997
+ #
998
+ # === Return
999
+ # (Array):: Usable broker clients
1000
+ def each_usable(identities = nil)
1001
+ choices = if identities && !identities.empty?
1002
+ choices = identities.inject([]) { |c, i| if b = @brokers_hash[i] then c << b else c end }
1003
+ else
1004
+ @brokers
1005
+ end
1006
+ choices.select do |b|
1007
+ if b.usable?
1008
+ yield(b) if block_given?
1009
+ true
1010
+ end
1011
+ end
1012
+ end
1013
+
1014
+ # Select the broker clients to be used in the desired order
1015
+ # Only uses home island brokers if :brokers options not used
1016
+ #
1017
+ # === Parameters
1018
+ # options(Hash):: Selection options:
1019
+ # :brokers(Array):: Identity of brokers selected for use, defaults to all home brokers if nil or empty
1020
+ # :order(Symbol):: Broker selection order: :random or :priority,
1021
+ # defaults to @select if :brokers is nil, otherwise defaults to :priority
1022
+ #
1023
+ # === Return
1024
+ # (Array):: Allowed BrokerClients in the order to be used
1025
+ def use(options)
1026
+ choices = []
1027
+ select = options[:order]
1028
+ if options[:brokers] && !options[:brokers].empty?
1029
+ options[:brokers].each do |identity|
1030
+ if choice = @brokers_hash[identity]
1031
+ choices << choice
1032
+ else
1033
+ Log.error("Invalid broker identity #{identity.inspect}, check server configuration")
1034
+ end
1035
+ end
1036
+ else
1037
+ choices = in_island
1038
+ select ||= @select
1039
+ end
1040
+ if select == :random
1041
+ choices.sort_by { rand }
1042
+ else
1043
+ choices
1044
+ end
1045
+ end
1046
+
1047
+ # Callback from broker client with connection status update
1048
+ # Makes client callback with :connected or :disconnected status if boundary crossed
1049
+ # for given island, or with :failed if all selected brokers or all brokers have failed
1050
+ #
1051
+ # === Parameters
1052
+ # broker(BrokerClient):: Broker client reporting status update
1053
+ # connected_before(Boolean):: Whether client was connected before this update
1054
+ #
1055
+ # === Return
1056
+ # true:: Always return true
1057
+ def update_status(broker, connected_before)
1058
+ after = connected(broker.island_id)
1059
+ before = after.clone
1060
+ before.delete(broker.identity) if broker.connected? && !connected_before
1061
+ before.push(broker.identity) if !broker.connected? && connected_before
1062
+ unless before == after
1063
+ island = broker.in_home_island ? " in home island" : " in island #{broker.island_alias}" if broker.island_id
1064
+ Log.info("[status] Broker #{broker.alias} is now #{broker.status}, " +
1065
+ "connected brokers#{island}: [#{aliases(after).join(", ")}]")
1066
+ end
1067
+ @connection_status.reject! do |k, v|
1068
+ reject = false
1069
+ if v[:brokers].nil? || v[:brokers].include?(broker.identity)
1070
+ b, a, n, f = if v[:brokers].nil?
1071
+ [before, after, in_island(broker.island_id).size, all]
1072
+ else
1073
+ [before & v[:brokers], after & v[:brokers], (island(broker.island_id) & v[:brokers]).size, v[:brokers]]
1074
+ end
1075
+ update = if v[:boundary] == :all
1076
+ if b.size < n && a.size == n
1077
+ :connected
1078
+ elsif b.size == n && a.size < n
1079
+ :disconnected
1080
+ elsif (f - failed).empty?
1081
+ :failed
1082
+ end
1083
+ else
1084
+ if b.size == 0 && a.size > 0
1085
+ :connected
1086
+ elsif b.size > 0 && a.size == 0
1087
+ :disconnected
1088
+ elsif (f - failed).empty?
1089
+ :failed
1090
+ end
1091
+ end
1092
+ if update
1093
+ if v[:callback].arity == 2
1094
+ v[:callback].call(update, broker.island_id)
1095
+ else
1096
+ v[:callback].call(update)
1097
+ end
1098
+ if v[:timer]
1099
+ v[:timer].cancel
1100
+ reject = true
1101
+ end
1102
+ end
1103
+ end
1104
+ reject
1105
+ end
1106
+ true
1107
+ end
1108
+
1109
+ # Handle message returned by broker because it could not deliver it
1110
+ # If agent still active, resend using another broker
1111
+ # If this is last usable broker and persistent is enabled, allow message to be queued
1112
+ # on next send even if the queue has no consumers so there is a chance of message
1113
+ # eventually being delivered
1114
+ # If persistent or one-way request and all usable brokers have failed, try one more time
1115
+ # without mandatory flag to give message opportunity to be queued
1116
+ # If there are no more usable broker clients, send non-delivery message to original sender
1117
+ #
1118
+ # === Parameters
1119
+ # identity(String):: Identity of broker that could not deliver message
1120
+ # reason(String):: Reason for return
1121
+ # "NO_ROUTE" - queue does not exist
1122
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
1123
+ # all consumers are not immediately ready to consume
1124
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
1125
+ # message(String):: Returned message in serialized packet format
1126
+ # to(String):: Queue to which message was published
1127
+ # context(Context):: Message publishing context
1128
+ #
1129
+ # === Return
1130
+ # true:: Always return true
1131
+ def handle_return(identity, reason, message, to, context)
1132
+ @brokers_hash[identity].update_status(:stopping) if reason == "ACCESS_REFUSED"
1133
+
1134
+ if context
1135
+ @returns.update("#{alias_(identity)} (#{reason.to_s.downcase})")
1136
+ name = context.name
1137
+ options = context.options || {}
1138
+ token = context.token
1139
+ one_way = context.one_way
1140
+ persistent = options[:persistent]
1141
+ mandatory = true
1142
+ remaining = (context.brokers - context.failed) & all_connected
1143
+ Log.info("RETURN reason #{reason} token #{token} brokers #{context.brokers.inspect} failed #{context.failed.inspect} " +
1144
+ " connected #{all_connected.inspect} remaining #{remaining.inspect}")
1145
+ if remaining.empty?
1146
+ if (persistent || one_way) &&
1147
+ ["ACCESS_REFUSED", "NO_CONSUMERS"].include?(reason) &&
1148
+ !(remaining = context.brokers & all_connected).empty?
1149
+ # Retry because persistent, and this time w/o mandatory so that gets queued even though no consumers
1150
+ mandatory = false
1151
+ else
1152
+ t = token ? " <#{token}>" : ""
1153
+ Log.info("NO ROUTE #{aliases(context.brokers).join(", ")} [#{name}]#{t} to #{to}")
1154
+ @non_delivery.call(reason, context.type, token, context.from, to) if @non_delivery
1155
+ end
1156
+ end
1157
+
1158
+ unless remaining.empty?
1159
+ t = token ? " <#{token}>" : ""
1160
+ p = persistent ? ", persistent" : ""
1161
+ m = mandatory ? ", mandatory" : ""
1162
+ Log.info("RE-ROUTE #{aliases(remaining).join(", ")} [#{context.name}]#{t} to #{to}#{p}#{m}")
1163
+ exchange = {:type => :queue, :name => to, :options => {:no_declare => true}}
1164
+ publish(exchange, message, options.merge(:no_serialize => true, :brokers => remaining,
1165
+ :persistent => persistent, :mandatory => mandatory))
1166
+ end
1167
+ else
1168
+ @returns.update("#{alias_(identity)} (#{reason.to_s.downcase} - missing context)")
1169
+ Log.info("Dropping message returned from broker #{identity} for reason #{reason} " +
1170
+ "because no message context available for re-routing it to #{to}")
1171
+ end
1172
+ true
1173
+ end
1174
+
1175
+ # Helper for deferring block execution until specified number of actions have completed
1176
+ # or timeout occurs
1177
+ class CountedDeferrable
1178
+
1179
+ include EM::Deferrable
1180
+
1181
+ # Defer action until completion count reached or timeout occurs
1182
+ #
1183
+ # === Parameter
1184
+ # count(Integer):: Number of completions required for action
1185
+ # timeout(Integer|nil):: Number of seconds to wait for all completions and if
1186
+ # reached, proceed with action; nil means no timing
1187
+ def initialize(count, timeout = nil)
1188
+ @timer = EM::Timer.new(timeout) { succeed } if timeout
1189
+ @count = count
1190
+ end
1191
+
1192
+ # Completed one part of task
1193
+ #
1194
+ # === Return
1195
+ # true:: Always return true
1196
+ def completed_one
1197
+ if (@count -= 1) == 0
1198
+ @timer.cancel if @timer
1199
+ succeed
1200
+ end
1201
+ true
1202
+ end
1203
+
1204
+ end # CountedDeferrable
1205
+
1206
+ # Cache for context of recently published messages for use with message returns
1207
+ # Applies LRU for managing cache size but only deletes entries when old enough
1208
+ class Published
1209
+
1210
+ # Number of seconds since a cache entry was last used before it is deleted
1211
+ MAX_AGE = 30
1212
+
1213
+ # Initialize cache
1214
+ def initialize
1215
+ @cache = {}
1216
+ @lru = []
1217
+ end
1218
+
1219
+ # Store message context in cache
1220
+ #
1221
+ # === Parameters
1222
+ # message(String):: Serialized message that was published
1223
+ # context(Context):: Message publishing context
1224
+ #
1225
+ # === Return
1226
+ # true:: Always return true
1227
+ def store(message, context)
1228
+ key = identify(message)
1229
+ now = Time.now.to_i
1230
+ if entry = @cache[key]
1231
+ entry[0] = now
1232
+ @lru.push(@lru.delete(key))
1233
+ else
1234
+ @cache[key] = [now, context]
1235
+ @lru.push(key)
1236
+ @cache.delete(@lru.shift) while (now - @cache[@lru.first][0]) > MAX_AGE
1237
+ end
1238
+ true
1239
+ end
1240
+
1241
+ # Fetch context of previously published message
1242
+ #
1243
+ # === Parameters
1244
+ # message(String):: Serialized message that was published
1245
+ #
1246
+ # === Return
1247
+ # (Context|nil):: Context of message, or nil if not found in cache
1248
+ def fetch(message)
1249
+ key = identify(message)
1250
+ if entry = @cache[key]
1251
+ entry[0] = Time.now.to_i
1252
+ @lru.push(@lru.delete(key))
1253
+ entry[1]
1254
+ end
1255
+ end
1256
+
1257
+ # Obtain a unique identifier for this message
1258
+ #
1259
+ # === Parameters
1260
+ # message(String):: Serialized message that was published
1261
+ #
1262
+ # === Returns
1263
+ # (String):: Unique id for message
1264
+ def identify(message)
1265
+ # If possible use significant part of serialized signature without decoding the message,
1266
+ # otherwise use entire serialized message
1267
+ if s = (message =~ /signature/)
1268
+ message[s, 1000]
1269
+ else
1270
+ message
1271
+ end
1272
+ end
1273
+
1274
+ end # Published
1275
+
1276
+ end # HABrokerClient
1277
+
1278
+ end # RightScale