right_agent 0.5.1

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 (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