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,76 @@
1
+ #
2
+ # Copyright (c) 2009-2011 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ module RightScale
24
+
25
+ # Allows generating RSA key pairs and extracting public key component
26
+ # Note: Creating a RSA key pair can take a fair amount of time (seconds)
27
+ class RsaKeyPair
28
+
29
+ DEFAULT_LENGTH = 2048
30
+
31
+ # Underlying OpenSSL keys
32
+ attr_reader :raw_key
33
+
34
+ # Create new RSA key pair using 'length' bits
35
+ def initialize(length = DEFAULT_LENGTH)
36
+ @raw_key = OpenSSL::PKey::RSA.generate(length)
37
+ end
38
+
39
+ # Does key pair include private key?
40
+ def has_private?
41
+ raw_key.private?
42
+ end
43
+
44
+ # New RsaKeyPair instance with identical public key but no private key
45
+ def to_public
46
+ RsaKeyPair.from_data(raw_key.public_key.to_pem)
47
+ end
48
+
49
+ # Key material in PEM format
50
+ def data
51
+ raw_key.to_pem
52
+ end
53
+ alias :to_s :data
54
+
55
+ # Load key pair previously serialized via 'data'
56
+ def self.from_data(data)
57
+ res = RsaKeyPair.allocate
58
+ res.instance_variable_set(:@raw_key, OpenSSL::PKey::RSA.new(data))
59
+ res
60
+ end
61
+
62
+ # Load key from file
63
+ def self.load(file)
64
+ from_data(File.read(file))
65
+ end
66
+
67
+ # Save key to file in PEM format
68
+ def save(file)
69
+ File.open(file, "w") do |f|
70
+ f.write(@raw_key.to_pem)
71
+ end
72
+ end
73
+
74
+ end # RsaKeyPair
75
+
76
+ end # RightScale
@@ -0,0 +1,86 @@
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
+ if RUBY_VERSION < '1.8.7'
24
+ RightScale::PKCS7 = OpenSSL::PKCS7::PKCS7
25
+ else
26
+ RightScale::PKCS7 = OpenSSL::PKCS7
27
+ end
28
+
29
+ module RightScale
30
+
31
+ # Signature that can be validated against certificates
32
+ class Signature
33
+
34
+ FLAGS = OpenSSL::PKCS7::NOCERTS || OpenSSL::PKCS7::BINARY || OpenSSL::PKCS7::NOATTR || OpenSSL::PKCS7::NOSMIMECAP || OpenSSL::PKCS7::DETACH
35
+
36
+ # Create signature using certificate and key pair.
37
+ #
38
+ # === Parameters
39
+ # data(String):: Data to be signed
40
+ # cert(Certificate):: Certificate used for signature
41
+ # key(RsaKeyPair):: Key pair used for signature
42
+ def initialize(data, cert, key)
43
+ @p7 = OpenSSL::PKCS7.sign(cert.raw_cert, key.raw_key, data, [], FLAGS)
44
+ @store = OpenSSL::X509::Store.new
45
+ end
46
+
47
+ # Load signature from previously serialized data
48
+ #
49
+ # === Parameters
50
+ # data(String):: Serialized data
51
+ #
52
+ # === Return
53
+ # sig(Signature):: Signature for data
54
+ def self.from_data(data)
55
+ sig = Signature.allocate
56
+ sig.instance_variable_set(:@p7, RightScale::PKCS7.new(data))
57
+ sig.instance_variable_set(:@store, OpenSSL::X509::Store.new)
58
+ sig
59
+ end
60
+
61
+ # Check whether signature was created using cert
62
+ #
63
+ # === Parameters
64
+ # cert(Certificate):: Certificate
65
+ #
66
+ # === Return
67
+ # (Boolean):: true if created using given cert, otherwise false
68
+ def match?(cert)
69
+ @p7.verify([cert.raw_cert], @store, nil, OpenSSL::PKCS7::NOVERIFY)
70
+ end
71
+
72
+ # Signature data in PEM or DER format
73
+ #
74
+ # === Parameters
75
+ # format(Symbol):: Encode format: :pem or :der, defaults to :pem
76
+ #
77
+ # === Return
78
+ # (String):: Signature
79
+ def data(format = :pem)
80
+ format == :pem ? @p7.to_pem : @p7.to_der
81
+ end
82
+ alias :to_s :data
83
+
84
+ end # Signature
85
+
86
+ end # RightScale
@@ -0,0 +1,69 @@
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
+ # Simple certificate store, serves a static set of certificates
26
+ class StaticCertificateStore
27
+
28
+ # Initialize store
29
+ #
30
+ # === Parameters
31
+ # signer_certs(Array|Certificate):: Signer certificate(s) used when loading data to
32
+ # check the digital signature. The signature associated with the serialized data
33
+ # needs to match with one of the signer certificates for loading to succeed.
34
+ # recipients_certs(Array|Certificate):: Recipient certificate(s) used when serializing
35
+ # data for encryption. Loading the data can only be done through serializers that
36
+ # have been initialized with a certificate that's in the recipient certificates
37
+ # if encryption is enabled.
38
+ def initialize(signer_certs, recipients_certs)
39
+ signer_certs = [ signer_certs ] unless signer_certs.respond_to?(:each)
40
+ @signer_certs = signer_certs
41
+ recipients_certs = [ recipients_certs ] unless recipients_certs.respond_to?(:each)
42
+ @recipients_certs = recipients_certs
43
+ end
44
+
45
+ # Retrieve signer certificates
46
+ #
47
+ # === Parameters
48
+ # id(String):: Serialized identity of signer, ignored
49
+ #
50
+ # === Return
51
+ # (Array):: Signer certificates
52
+ def get_signer(id)
53
+ @signer_certs
54
+ end
55
+
56
+ # Retrieve recipient certificates that will be able to decrypt the serialized data
57
+ #
58
+ # === Parameters
59
+ # packet(RightScale::Packet):: Packet containing recipient identity, ignored
60
+ #
61
+ # === Return
62
+ # (Array):: Recipient certificates
63
+ def get_recipients(packet)
64
+ @recipients_certs
65
+ end
66
+
67
+ end # StaticCertificateStore
68
+
69
+ end # RightScale
@@ -0,0 +1,937 @@
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
+ # This class allows sending requests to agents without having to run a local mapper
26
+ # It is used by Actor.request which is used by actors that need to send requests to remote agents
27
+ # If requested, it will queue requests when there are no broker connections
28
+ # All requests go through the mapper for security purposes
29
+ class Sender
30
+
31
+ include StatsHelper
32
+
33
+ # Minimum number of seconds between restarts of the inactivity timer
34
+ MIN_RESTART_INACTIVITY_TIMER_INTERVAL = 60
35
+
36
+ # Number of seconds to wait for ping response from a mapper when checking connectivity
37
+ PING_TIMEOUT = 30
38
+
39
+ # Factor used on each retry iteration to achieve exponential backoff
40
+ RETRY_BACKOFF_FACTOR = 4
41
+
42
+ # Maximum seconds to wait before starting flushing offline queue when disabling offline mode
43
+ MAX_QUEUE_FLUSH_DELAY = 120 # 2 minutes
44
+
45
+ # Maximum number of offline queued requests before triggering restart vote
46
+ MAX_QUEUED_REQUESTS = 1000
47
+
48
+ # Number of seconds that should be spent in offline mode before triggering a restart vote
49
+ RESTART_VOTE_DELAY = 900 # 15 minutes
50
+
51
+ # (EM::Timer) Timer while waiting for mapper ping response
52
+ attr_accessor :pending_ping
53
+
54
+ # (Hash) Pending requests; key is request token and value is a hash
55
+ # :response_handler(Proc):: Block to be activated when response is received
56
+ # :receive_time(Time):: Time when message was received
57
+ # :request_kind(String):: Kind of Sender request, optional
58
+ # :retry_parent(String):: Token for parent request in a retry situation, optional
59
+ attr_accessor :pending_requests
60
+
61
+ # (HABrokerClient) High availability AMQP broker client
62
+ attr_accessor :broker
63
+
64
+ # (String) Identity of the associated agent
65
+ attr_reader :identity
66
+
67
+ # Accessor for use by actor
68
+ #
69
+ # === Return
70
+ # (Sender):: This sender instance if defined, otherwise nil
71
+ def self.instance
72
+ @@instance if defined?(@@instance)
73
+ end
74
+
75
+ # Initialize sender
76
+ #
77
+ # === Parameters
78
+ # agent(Agent):: Agent using this sender; uses its identity, broker, and following options:
79
+ # :exception_callback(Proc):: Callback with following parameters that is activated on exception events:
80
+ # exception(Exception):: Exception
81
+ # message(Packet):: Message being processed
82
+ # agent(Agent):: Reference to agent
83
+ # :offline_queueing(Boolean):: Whether to queue request if currently not connected to any brokers,
84
+ # also requires agent invocation of initialize_offline_queue and start_offline_queue methods below
85
+ # :restart_callback(Proc):: Callback that is activated on each restart vote with votes being initiated
86
+ # by offline queue exceeding MAX_QUEUED_REQUESTS or by repeated failures to access mapper when online
87
+ # :retry_timeout(Numeric):: Maximum number of seconds to retry request before give up
88
+ # :retry_interval(Numeric):: Number of seconds before initial request retry, increases exponentially
89
+ # :time_to_live(Integer):: Number of seconds before a request expires and is to be ignored
90
+ # by the receiver, 0 means never expire
91
+ # :secure(Boolean):: true indicates to use Security features of rabbitmq to restrict agents to themselves
92
+ # :single_threaded(Boolean):: true indicates to run all operations in one thread; false indicates
93
+ # to do requested work on EM defer thread and all else, such as pings on main thread
94
+ def initialize(agent)
95
+ @agent = agent
96
+ @identity = @agent.identity
97
+ @options = @agent.options || {}
98
+ @broker = @agent.broker
99
+ @secure = @options[:secure]
100
+ @single_threaded = @options[:single_threaded]
101
+ @queueing_mode = :initializing
102
+ @queue_running = false
103
+ @queue_initializing = false
104
+ @queue = []
105
+ @restart_vote_count = 0
106
+ @retry_timeout = nil_if_zero(@options[:retry_timeout])
107
+ @retry_interval = nil_if_zero(@options[:retry_interval])
108
+ @ping_interval = @options[:ping_interval] || 0
109
+
110
+ # Only to be accessed from primary thread
111
+ @pending_requests = {}
112
+ @pending_ping = nil
113
+
114
+ reset_stats
115
+ @last_received = 0
116
+ @message_received_callbacks = []
117
+ restart_inactivity_timer if @ping_interval > 0
118
+ @@instance = self
119
+ end
120
+
121
+ # Update the time this agent last received a request or response message
122
+ # and restart the inactivity timer thus deferring the next connectivity check
123
+ # Also forward this message receipt notification to any callbacks that have registered
124
+ #
125
+ # === Block
126
+ # Optional block without parameters that is activated when a message is received
127
+ #
128
+ # === Return
129
+ # true:: Always return true
130
+ def message_received(&callback)
131
+ if block_given?
132
+ @message_received_callbacks << callback
133
+ else
134
+ @message_received_callbacks.each { |c| c.call }
135
+ if @ping_interval > 0
136
+ now = Time.now.to_i
137
+ if (now - @last_received) > MIN_RESTART_INACTIVITY_TIMER_INTERVAL
138
+ @last_received = now
139
+ restart_inactivity_timer
140
+ end
141
+ end
142
+ end
143
+ true
144
+ end
145
+
146
+ # Initialize the offline queue (should be called once)
147
+ # All requests sent prior to running this initialization are queued if offline
148
+ # queueing is enabled and then are sent once this initialization has run
149
+ # All requests following this call and prior to calling start_offline_queue
150
+ # are prepended to the request queue
151
+ #
152
+ # === Return
153
+ # true:: Always return true
154
+ def initialize_offline_queue
155
+ unless @queue_running || !@options[:offline_queueing]
156
+ @queue_running = true
157
+ @queue_initializing = true
158
+ end
159
+ true
160
+ end
161
+
162
+ # Switch offline queueing to online mode and flush all buffered messages
163
+ #
164
+ # === Return
165
+ # true:: Always return true
166
+ def start_offline_queue
167
+ if @queue_initializing
168
+ @queue_initializing = false
169
+ flush_queue unless @queueing_mode == :offline
170
+ @queueing_mode = :online if @queueing_mode == :initializing
171
+ end
172
+ true
173
+ end
174
+
175
+ # Send a request to a single target or multiple targets with no response expected other
176
+ # than routing failures
177
+ # Do not persist the request en route
178
+ # Enqueue the request if the target is not currently available
179
+ # Never automatically retry the request
180
+ # Set time-to-live to be forever
181
+ #
182
+ # === Parameters
183
+ # type(String):: Dispatch route for the request; typically identifies actor and action
184
+ # payload(Object):: Data to be sent with marshalling en route
185
+ # target(String|Hash):: Identity of specific target, hash for selecting potentially multiple
186
+ # targets, or nil if routing solely using type
187
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
188
+ # :scope(Hash):: Scoping to be used to restrict routing
189
+ # :account(Integer):: Restrict to agents with this account id
190
+ # :deployment(Integer):: Restrict to agents with this deployment id
191
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
192
+ # ones with no shard id
193
+ # :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
194
+ # defaults to :any
195
+ #
196
+ # === Block
197
+ # Optional block used to process routing response failures asynchronously with the following parameter:
198
+ # result(Result):: Response with an OperationResult of RETRY, NON_DELIVERY, or ERROR,
199
+ # use RightScale::OperationResult.from_results to decode
200
+ #
201
+ # === Return
202
+ # true:: Always return true
203
+ def send_push(type, payload = nil, target = nil, &callback)
204
+ build_push(:send_push, type, payload, target, &callback)
205
+ end
206
+
207
+ # Send a request to a single target or multiple targets with no response expected other
208
+ # than routing failures
209
+ # Persist the request en route to reduce the chance of it being lost at the expense of some
210
+ # additional network overhead
211
+ # Enqueue the request if the target is not currently available
212
+ # Never automatically retry the request
213
+ # Set time-to-live to be forever
214
+ #
215
+ # === Parameters
216
+ # type(String):: Dispatch route for the request; typically identifies actor and action
217
+ # payload(Object):: Data to be sent with marshalling en route
218
+ # target(String|Hash):: Identity of specific target, hash for selecting potentially multiple
219
+ # targets, or nil if routing solely using type
220
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
221
+ # :scope(Hash):: Scoping to be used to restrict routing
222
+ # :account(Integer):: Restrict to agents with this account id
223
+ # :deployment(Integer):: Restrict to agents with this deployment id
224
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
225
+ # ones with no shard id
226
+ # :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
227
+ # defaults to :any
228
+ #
229
+ # === Block
230
+ # Optional block used to process routing response failures asynchronously with the following parameter:
231
+ # result(Result):: Response with an OperationResult of RETRY, NON_DELIVERY, or ERROR,
232
+ # use RightScale::OperationResult.from_results to decode
233
+ #
234
+ # === Return
235
+ # true:: Always return true
236
+ def send_persistent_push(type, payload = nil, target = nil, &callback)
237
+ build_push(:send_persistent_push, type, payload, target, &callback)
238
+ end
239
+
240
+ # Send a request to a single target with a response expected
241
+ # Automatically retry the request if a response is not received in a reasonable amount of time
242
+ # or if there is a non-delivery response indicating the target is not currently available
243
+ # Timeout the request if a response is not received in time, typically configured to 2 minutes
244
+ # Because of retries there is the possibility of duplicated requests, and these are detected and
245
+ # discarded automatically unless the receiving agent is using a shared queue, in which case this
246
+ # method should not be used for actions that are non-idempotent
247
+ # Allow the request to expire per the agent's configured time-to-live, typically 1 minute
248
+ # Note that receiving a response does not guarantee that the request activity has actually
249
+ # completed since the request processing may involve other asynchronous requests
250
+ #
251
+ # === Parameters
252
+ # type(String):: Dispatch route for the request; typically identifies actor and action
253
+ # payload(Object):: Data to be sent with marshalling en route
254
+ # target(String|Hash):: Identity of specific target, hash for selecting targets of which one is picked
255
+ # randomly, or nil if routing solely using type
256
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
257
+ # :scope(Hash):: Scoping to be used to restrict routing
258
+ # :account(Integer):: Restrict to agents with this account id
259
+ # :deployment(Integer):: Restrict to agents with this deployment id
260
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
261
+ # ones with no shard id
262
+ #
263
+ # === Block
264
+ # Required block used to process response asynchronously with the following parameter:
265
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
266
+ # use RightScale::OperationResult.from_results to decode
267
+ #
268
+ # === Return
269
+ # true:: Always return true
270
+ def send_retryable_request(type, payload = nil, target = nil, &callback)
271
+ build_request(:send_retryable_request, type, payload, target, &callback)
272
+ end
273
+
274
+ # Send a request to a single target with a response expected
275
+ # Persist the request en route to reduce the chance of it being lost at the expense of some
276
+ # additional network overhead
277
+ # Enqueue the request if the target is not currently available
278
+ # Never automatically retry the request if there is the possibility of the request being duplicated
279
+ # Set time-to-live to be forever
280
+ # Note that receiving a response does not guarantee that the request activity has actually
281
+ # completed since the request processing may involve other asynchronous requests
282
+ #
283
+ # === Parameters
284
+ # type(String):: Dispatch route for the request; typically identifies actor and action
285
+ # payload(Object):: Data to be sent with marshalling en route
286
+ # target(String|Hash):: Identity of specific target, hash for selecting targets of which one is picked
287
+ # randomly, or nil if routing solely using type
288
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
289
+ # :scope(Hash):: Scoping to be used to restrict routing
290
+ # :account(Integer):: Restrict to agents with this account id
291
+ # :deployment(Integer):: Restrict to agents with this deployment id
292
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
293
+ # ones with no shard id
294
+ #
295
+ # === Block
296
+ # Required block used to process response asynchronously with the following parameter:
297
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
298
+ # use RightScale::OperationResult.from_results to decode
299
+ #
300
+ # === Return
301
+ # true:: Always return true
302
+ def send_persistent_request(type, payload = nil, target = nil, &callback)
303
+ build_request(:send_persistent_request, type, payload, target, &callback)
304
+ end
305
+
306
+ # Handle response to a request
307
+ # Only to be called from primary thread
308
+ #
309
+ # === Parameters
310
+ # response(Result):: Packet received as result of request
311
+ #
312
+ # === Return
313
+ # true:: Always return true
314
+ def handle_response(response)
315
+ token = response.token
316
+ if response.is_a?(Result)
317
+ if result = OperationResult.from_results(response)
318
+ if result.non_delivery?
319
+ @non_deliveries.update(result.content.nil? ? "nil" : result.content.inspect)
320
+ elsif result.error?
321
+ @result_errors.update(result.content.nil? ? "nil" : result.content.inspect)
322
+ end
323
+ @results.update(result.status)
324
+ else
325
+ @results.update(response.results.nil? ? "nil" : response.results)
326
+ end
327
+
328
+ if handler = @pending_requests[token]
329
+ if result && result.non_delivery? && handler[:request_kind] == :send_retryable_request &&
330
+ [OperationResult::TARGET_NOT_CONNECTED, OperationResult::TTL_EXPIRATION].include?(result.content)
331
+ # Log and ignore so that timeout retry mechanism continues
332
+ # Leave purging of associated request until final response, i.e., success response or retry timeout
333
+ Log.info("Non-delivery of <#{token}> because #{result.content}")
334
+ else
335
+ deliver(response, handler)
336
+ end
337
+ elsif result && result.non_delivery?
338
+ Log.info("Non-delivery of <#{token}> because #{result.content}")
339
+ else
340
+ Log.debug("No pending request for response #{response.to_s([])}")
341
+ end
342
+ end
343
+ true
344
+ end
345
+
346
+ # Switch to offline mode, in this mode requests are queued in memory
347
+ # rather than sent to the mapper
348
+ # Idempotent
349
+ #
350
+ # === Return
351
+ # true:: Always return true
352
+ def enable_offline_mode
353
+ if offline?
354
+ if @flushing_queue
355
+ # If we were in offline mode then switched back to online but are still in the
356
+ # process of flushing the in memory queue and are now switching to offline mode
357
+ # again then stop the flushing
358
+ @stop_flushing_queue = true
359
+ end
360
+ else
361
+ Log.info("[offline] Disconnect from broker detected, entering offline mode")
362
+ Log.info("[offline] Messages will be queued in memory until connection to broker is re-established")
363
+ @offlines.update
364
+ @queue ||= [] # ensure queue is valid without losing any messages when going offline
365
+ @queueing_mode = :offline
366
+ @restart_vote_timer ||= EM::Timer.new(RESTART_VOTE_DELAY) { vote_to_restart(timer_trigger=true) }
367
+ end
368
+ end
369
+
370
+ # Switch back to sending requests to mapper after in memory queue gets flushed
371
+ # Idempotent
372
+ #
373
+ # === Return
374
+ # true:: Always return true
375
+ def disable_offline_mode
376
+ if offline? && @queue_running
377
+ Log.info("[offline] Connection to broker re-established")
378
+ @offlines.finish
379
+ @restart_vote_timer.cancel if @restart_vote_timer
380
+ @restart_vote_timer = nil
381
+ @stop_flushing_queue = false
382
+ @flushing_queue = true
383
+ # Let's wait a bit not to flood the mapper
384
+ EM.add_timer(rand(MAX_QUEUE_FLUSH_DELAY)) { flush_queue } if @queue_running
385
+ end
386
+ true
387
+ end
388
+
389
+ # Get age of youngest pending request
390
+ #
391
+ # === Return
392
+ # age(Integer|nil):: Age in seconds of youngest request, or nil if no pending requests
393
+ def request_age
394
+ time = Time.now
395
+ age = nil
396
+ @pending_requests.each_value do |request|
397
+ seconds = time - request[:receive_time]
398
+ age = seconds.to_i if age.nil? || seconds < age
399
+ end
400
+ age
401
+ end
402
+
403
+ # Take any actions necessary to quiesce mapper interaction in preparation
404
+ # for agent termination but allow message receipt to continue
405
+ #
406
+ # === Return
407
+ # (Array):: Number of pending requests and age of youngest request
408
+ def terminate
409
+ @terminating = true
410
+ @ping_interval = 0
411
+ if @pending_ping
412
+ @pending_ping.cancel
413
+ @pending_ping = nil
414
+ end
415
+ if @timer
416
+ @timer.cancel
417
+ @timer = nil
418
+ end
419
+ if @restart_vote_timer
420
+ @restart_vote_timer.cancel
421
+ @restart_vote_timer = nil
422
+ end
423
+ [@pending_requests.size, request_age]
424
+ end
425
+
426
+ # Create displayable dump of unfinished request information
427
+ # Truncate list if there are more than 50 requests
428
+ #
429
+ # === Return
430
+ # info(Array(String)):: Receive time and token for each request in descending time order
431
+ def dump_requests
432
+ info = []
433
+ @pending_requests.each do |token, request|
434
+ info << "#{request[:receive_time].localtime} <#{token}>"
435
+ end
436
+ info.sort.reverse
437
+ info = info[0..49] + ["..."] if info.size > 50
438
+ info
439
+ end
440
+
441
+ # Get sender statistics
442
+ #
443
+ # === Parameters
444
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
445
+ #
446
+ # === Return
447
+ # stats(Hash):: Current statistics:
448
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
449
+ # "total"(Integer):: Total exceptions for this category
450
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
451
+ # "non-deliveries"(Hash|nil):: Non-delivery activity stats with keys "total", "percent", "last",
452
+ # and 'rate' with percentage breakdown per reason, or nil if none
453
+ # "offlines"(Hash|nil):: Offline activity stats with keys "total", "last", and "duration",
454
+ # or nil if none
455
+ # "pings"(Hash|nil):: Request activity stats with keys "total", "percent", "last", and "rate"
456
+ # with percentage breakdown for "success" vs. "timeout", or nil if none
457
+ # "request kinds"(Hash|nil):: Request kind activity stats with keys "total", "percent", and "last"
458
+ # with percentage breakdown per kind, or nil if none
459
+ # "requests"(Hash|nil):: Request activity stats with keys "total", "percent", "last", and "rate"
460
+ # with percentage breakdown per request type, or nil if none
461
+ # "requests pending"(Hash|nil):: Number of requests waiting for response and age of oldest, or nil if none
462
+ # "response time"(Float):: Average number of seconds to respond to a request recently
463
+ # "result errors"(Hash|nil):: Error result activity stats with keys "total", "percent", "last",
464
+ # and 'rate' with percentage breakdown per error, or nil if none
465
+ # "results"(Hash|nil):: Results activity stats with keys "total", "percent", "last", and "rate"
466
+ # with percentage breakdown per operation result type, or nil if none
467
+ # "retries"(Hash|nil):: Retry activity stats with keys "total", "percent", "last", and "rate"
468
+ # with percentage breakdown per request type, or nil if none
469
+ def stats(reset = false)
470
+ offlines = @offlines.all
471
+ offlines.merge!("duration" => @offlines.avg_duration) if offlines
472
+ requests_pending = if @pending_requests.size > 0
473
+ now = Time.now.to_i
474
+ oldest = @pending_requests.values.inject(0) { |m, r| [m, now - r[:receive_time].to_i].max }
475
+ {"total" => @pending_requests.size, "oldest age" => oldest}
476
+ end
477
+ stats = {
478
+ "exceptions" => @exceptions.stats,
479
+ "non-deliveries" => @non_deliveries.all,
480
+ "offlines" => offlines,
481
+ "pings" => @pings.all,
482
+ "request kinds" => @request_kinds.all,
483
+ "requests" => @requests.all,
484
+ "requests pending" => requests_pending,
485
+ "response time" => @requests.avg_duration,
486
+ "result errors" => @result_errors.all,
487
+ "results" => @results.all,
488
+ "retries" => @retries.all
489
+ }
490
+ reset_stats if reset
491
+ stats
492
+ end
493
+
494
+ protected
495
+
496
+ # Reset dispatch statistics
497
+ #
498
+ # === Return
499
+ # true:: Always return true
500
+ def reset_stats
501
+ @pings = ActivityStats.new
502
+ @retries = ActivityStats.new
503
+ @requests = ActivityStats.new
504
+ @results = ActivityStats.new
505
+ @result_errors = ActivityStats.new
506
+ @non_deliveries = ActivityStats.new
507
+ @offlines = ActivityStats.new(measure_rate = false)
508
+ @request_kinds = ActivityStats.new(measure_rate = false)
509
+ @exceptions = ExceptionStats.new(@agent, @options[:exception_callback])
510
+ true
511
+ end
512
+
513
+ # Build and send Push packet
514
+ #
515
+ # === Parameters
516
+ # kind(Symbol):: Kind of push: :send_push or :send_persistent_push
517
+ # type(String):: Dispatch route for the request; typically identifies actor and action
518
+ # payload(Object):: Data to be sent with marshalling en route
519
+ # target(String|Hash):: Identity of specific target, or hash for selecting potentially multiple
520
+ # targets, or nil if routing solely using type
521
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
522
+ # :scope(Hash):: Scoping to be used to restrict routing
523
+ # :account(Integer):: Restrict to agents with this account id
524
+ # :deployment(Integer):: Restrict to agents with this deployment id
525
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
526
+ # ones with no shard id
527
+ # :selector(Symbol):: Which of the matched targets to be selected, either :any or :all,
528
+ # defaults to :any
529
+ #
530
+ # === Block
531
+ # Optional block used to process routing response failures asynchronously with the following parameter:
532
+ # result(Result):: Response with an OperationResult of RETRY, NON_DELIVERY, or ERROR,
533
+ # use RightScale::OperationResult.from_results to decode
534
+ #
535
+ # === Return
536
+ # true:: Always return true
537
+ #
538
+ # === Raise
539
+ # ArgumentError:: If target is invalid
540
+ def build_push(kind, type, payload = nil, target = nil, &callback)
541
+ validate_target(target, allow_selector = true)
542
+ if should_queue?
543
+ queue_request(:kind => kind, :type => type, :payload => payload, :target => target, :callback => callback)
544
+ else
545
+ method = type.split('/').last
546
+ received_at = @requests.update(method)
547
+ push = Push.new(type, payload)
548
+ push.from = @identity
549
+ push.token = AgentIdentity.generate
550
+ if target.is_a?(Hash)
551
+ push.tags = target[:tags] || []
552
+ push.scope = target[:scope]
553
+ push.selector = target[:selector] || :any
554
+ else
555
+ push.target = target
556
+ end
557
+ push.persistent = kind == :send_persistent_push
558
+ @request_kinds.update((push.selector == :all ? kind.to_s.sub(/push/, "fanout") : kind.to_s)[5..-1])
559
+ @pending_requests[push.token] = {
560
+ :response_handler => callback,
561
+ :receive_time => received_at,
562
+ :request_kind => kind
563
+ } if callback
564
+ publish(push)
565
+ end
566
+ true
567
+ end
568
+
569
+ # Build and send Request packet
570
+ #
571
+ # === Parameters
572
+ # kind(Symbol):: Kind of request: :send_retryable_request or :send_persistent_request
573
+ # type(String):: Dispatch route for the request; typically identifies actor and action
574
+ # payload(Object):: Data to be sent with marshalling en route
575
+ # target(String|Hash):: Identity of specific target, or hash for selecting targets of which one is picked
576
+ # randomly, or nil if routing solely using type
577
+ # :tags(Array):: Tags that must all be associated with a target for it to be selected
578
+ # :scope(Hash):: Scoping to be used to restrict routing
579
+ # :account(Integer):: Restrict to agents with this account id
580
+ # :deployment(Integer):: Restrict to agents with this deployment id
581
+ # :shard(Integer):: Restrict to agents with this shard id, or if value is Packet::GLOBAL,
582
+ # ones with no shard id
583
+ #
584
+ # === Block
585
+ # Required block used to process response asynchronously with the following parameter:
586
+ # result(Result):: Response with an OperationResult of SUCCESS, RETRY, NON_DELIVERY, or ERROR,
587
+ # use RightScale::OperationResult.from_results to decode
588
+ #
589
+ # === Return
590
+ # true:: Always return true
591
+ #
592
+ # === Raise
593
+ # ArgumentError:: If target is invalid
594
+ def build_request(kind, type, payload, target, &callback)
595
+ validate_target(target, allow_selector = false)
596
+ if should_queue?
597
+ queue_request(:kind => kind, :type => type, :payload => payload, :target => target, :callback => callback)
598
+ else
599
+ method = type.split('/').last
600
+ token = AgentIdentity.generate
601
+ non_duplicate = kind == :send_persistent_request
602
+ received_at = @requests.update(method, token)
603
+ @request_kinds.update(kind.to_s[5..-1])
604
+
605
+ # Using next_tick to ensure on primary thread since using @pending_requests
606
+ EM.next_tick do
607
+ begin
608
+ request = Request.new(type, payload)
609
+ request.from = @identity
610
+ request.token = token
611
+ if target.is_a?(Hash)
612
+ request.tags = target[:tags] || []
613
+ request.scope = target[:scope]
614
+ request.selector = :any
615
+ else
616
+ request.target = target
617
+ end
618
+ request.expires_at = Time.now.to_i + @options[:time_to_live] if !non_duplicate && @options[:time_to_live] && @options[:time_to_live] != 0
619
+ request.persistent = non_duplicate
620
+ @pending_requests[token] = {
621
+ :response_handler => callback,
622
+ :receive_time => received_at,
623
+ :request_kind => kind}
624
+ if non_duplicate
625
+ publish(request)
626
+ else
627
+ publish_with_timeout_retry(request, token)
628
+ end
629
+ rescue Exception => e
630
+ Log.error("Failed to send #{type} #{kind.to_s}", e, :trace)
631
+ @exceptions.track(kind.to_s, e, request)
632
+ end
633
+ end
634
+ end
635
+ true
636
+ end
637
+
638
+ # Validate target argument of send
639
+ #
640
+ # === Parameters
641
+ # target(String|Hash):: Identity of specific target, or hash for selecting targets of which one is picked
642
+ # allow_selector(Boolean):: Whether to allow :selector
643
+ #
644
+ # === Return
645
+ # true:: Always return true
646
+ #
647
+ # === Raise
648
+ # ArgumentError:: If target is invalid
649
+ def validate_target(target, allow_selector)
650
+ if target.is_a?(Hash)
651
+ selector = allow_selector ? ":selector, " : ""
652
+ t = SerializationHelper.symbolize_keys(target)
653
+ if s = target[:scope]
654
+ if s.is_a?(Hash)
655
+ s = SerializationHelper.symbolize_keys(s)
656
+ if ([:account, :deployment, :shard] & s.keys).empty? && !s.empty?
657
+ raise ArgumentError, "Invalid target scope (#{t[:scope].inspect}), choices are :account, :deployment, and :shard allowed"
658
+ end
659
+ t[:scope] = s
660
+ else
661
+ raise ArgumentError, "Invalid target scope (#{t[:scope].inspect}), must be a hash of :account, :deployment, and/or :shard"
662
+ end
663
+ elsif (s = t[:selector]) && allow_selector
664
+ s = s.to_sym
665
+ unless [:any, :all].include?(s)
666
+ raise ArgumentError, "Invalid target selector (#{t[:selector].inspect}), choices are :any and :all"
667
+ end
668
+ t[:selector] = s
669
+ elsif !t.has_key?(:tags) && !t.empty?
670
+ raise ArgumentError, "Invalid target hash (#{target.inspect}), choices are #{selector}:tags and/or :scope"
671
+ end
672
+ target = t
673
+ elsif !target.nil? && !target.is_a?(String)
674
+ raise ArgumentError, "Invalid target (#{target.inspect}), choices are specific target name or a hash of #{selector}:tags and/or :scope"
675
+ end
676
+ true
677
+ end
678
+
679
+ # Publish request with one or more retries if do not receive a response in time
680
+ # Send timeout result if reach configured retry timeout limit
681
+ # Use exponential backoff with RETRY_BACKOFF_FACTOR for retry spacing
682
+ # Adjust retry interval by average response time to avoid adding to system load
683
+ # when system gets slow
684
+ #
685
+ # === Parameters
686
+ # request(Request):: Request to be sent
687
+ # parent(String):: Token for original request
688
+ # count(Integer):: Number of retries so far
689
+ # multiplier(Integer):: Multiplier for retry interval for exponential backoff
690
+ # elapsed(Integer):: Elapsed time in seconds since this request was first attempted
691
+ #
692
+ # === Return
693
+ # true:: Always return true
694
+ def publish_with_timeout_retry(request, parent, count = 0, multiplier = 1, elapsed = 0)
695
+ ids = publish(request)
696
+
697
+ if @retry_interval && @retry_timeout && parent && !ids.empty?
698
+ interval = [(@retry_interval * multiplier) + (@requests.avg_duration || 0), @retry_timeout - elapsed].min
699
+ EM.add_timer(interval) do
700
+ begin
701
+ if handler = @pending_requests[parent]
702
+ count += 1
703
+ elapsed += interval
704
+ if elapsed < @retry_timeout
705
+ request.tries << request.token
706
+ request.token = AgentIdentity.generate
707
+ @pending_requests[parent][:retry_parent] = parent if count == 1
708
+ @pending_requests[request.token] = @pending_requests[parent]
709
+ publish_with_timeout_retry(request, parent, count, multiplier * RETRY_BACKOFF_FACTOR, elapsed)
710
+ @retries.update(request.type.split('/').last)
711
+ else
712
+ Log.warning("RE-SEND TIMEOUT after #{elapsed.to_i} seconds for #{request.to_s([:tags, :target, :tries])}")
713
+ result = OperationResult.non_delivery(OperationResult::RETRY_TIMEOUT)
714
+ @non_deliveries.update(result.content)
715
+ handle_response(Result.new(request.token, request.reply_to, result, @identity))
716
+ end
717
+ check_connection(ids.first) if count == 1
718
+ end
719
+ rescue Exception => e
720
+ Log.error("Failed retry for #{request.token}", e, :trace)
721
+ @exceptions.track("retry", e, request)
722
+ end
723
+ end
724
+ end
725
+ true
726
+ end
727
+
728
+ # Publish request
729
+ # Use mandatory flag to request return of message if it cannot be delivered
730
+ #
731
+ # === Parameters
732
+ # request(Push|Request):: Packet to be sent
733
+ # ids(Array|nil):: Identity of specific brokers to choose from, or nil if any okay
734
+ #
735
+ # === Return
736
+ # ids(Array):: Identity of brokers published to
737
+ def publish(request, ids = nil)
738
+ begin
739
+ exchange = {:type => :fanout, :name => "request", :options => {:durable => true, :no_declare => @secure}}
740
+ ids = @broker.publish(exchange, request, :persistent => request.persistent, :mandatory => true,
741
+ :log_filter => [:tags, :target, :tries, :persistent], :brokers => ids)
742
+ rescue HABrokerClient::NoConnectedBrokers => e
743
+ Log.error("Failed to publish request #{request.to_s([:tags, :target, :tries])}", e)
744
+ ids = []
745
+ rescue Exception => e
746
+ Log.error("Failed to publish request #{request.to_s([:tags, :target, :tries])}", e, :trace)
747
+ @exceptions.track("publish", e, request)
748
+ ids = []
749
+ end
750
+ ids
751
+ end
752
+
753
+ # Deliver the response and remove associated request(s) from pending
754
+ # Use defer thread instead of primary if not single threaded, consistent with dispatcher,
755
+ # so that all shared data is accessed from the same thread
756
+ # Do callback if there is an exception, consistent with agent identity queue handling
757
+ # Only to be called from primary thread
758
+ #
759
+ # === Parameters
760
+ # response(Result):: Packet received as result of request
761
+ # handler(Hash):: Associated request handler
762
+ #
763
+ # === Return
764
+ # true:: Always return true
765
+ def deliver(response, handler)
766
+ @requests.finish(handler[:receive_time], response.token)
767
+
768
+ @pending_requests.delete(response.token)
769
+ if parent = handler[:retry_parent]
770
+ @pending_requests.reject! { |k, v| k == parent || v[:retry_parent] == parent }
771
+ end
772
+
773
+ if handler[:response_handler]
774
+ EM.__send__(@single_threaded ? :next_tick : :defer) do
775
+ begin
776
+ handler[:response_handler].call(response)
777
+ rescue Exception => e
778
+ Log.error("Failed processing response {response.to_s([])}", e, :trace)
779
+ @exceptions.track("response", e, response)
780
+ end
781
+ end
782
+ end
783
+ true
784
+ end
785
+
786
+ # Check whether broker connection is usable by pinging a mapper via that broker
787
+ # Attempt to reconnect if ping does not respond in PING_TIMEOUT seconds
788
+ # Ignore request if already checking a connection
789
+ # Only to be called from primary thread
790
+ #
791
+ # === Parameters
792
+ # id(String):: Identity of specific broker to use to send ping, defaults to any
793
+ # currently connected broker
794
+ #
795
+ # === Return
796
+ # true:: Always return true
797
+ def check_connection(id = nil)
798
+ unless @terminating || @pending_ping || (id && !@broker.connected?(id))
799
+ @pending_ping = EM::Timer.new(PING_TIMEOUT) do
800
+ begin
801
+ @pings.update("timeout")
802
+ @pending_ping = nil
803
+ Log.warning("Mapper ping via broker #{id} timed out after #{PING_TIMEOUT} seconds, attempting to reconnect")
804
+ host, port, index, priority, _ = @broker.identity_parts(id)
805
+ @agent.connect(host, port, index, priority, force = true)
806
+ rescue Exception => e
807
+ Log.error("Failed to reconnect to broker #{id}", e, :trace)
808
+ @exceptions.track("ping timeout", e)
809
+ end
810
+ end
811
+
812
+ handler = lambda do |_|
813
+ begin
814
+ if @pending_ping
815
+ @pings.update("success")
816
+ @pending_ping.cancel
817
+ @pending_ping = nil
818
+ end
819
+ rescue Exception => e
820
+ Log.error("Failed to cancel mapper ping", e, :trace)
821
+ @exceptions.track("cancel ping", e)
822
+ end
823
+ end
824
+
825
+ request = Request.new("/mapper/ping", nil, {:from => @identity, :token => AgentIdentity.generate})
826
+ @pending_requests[request.token] = {:response_handler => handler, :receive_time => Time.now}
827
+ ids = [id] if id
828
+ id = publish(request, ids).first
829
+ end
830
+ true
831
+ end
832
+
833
+ # Vote for restart and reset trigger
834
+ #
835
+ # === Parameters
836
+ # timer_trigger(Boolean):: true if vote was triggered by timer, false if it
837
+ # was triggered by number of messages in in-memory queue
838
+ #
839
+ # === Return
840
+ # true:: Always return true
841
+ def vote_to_restart(timer_trigger)
842
+ if restart_vote = @options[:restart_callback]
843
+ restart_vote.call
844
+ if timer_trigger
845
+ @restart_vote_timer = EM::Timer.new(RESTART_VOTE_DELAY) { vote_to_restart(timer_trigger = true) }
846
+ else
847
+ @restart_vote_count = 0
848
+ end
849
+ end
850
+ true
851
+ end
852
+
853
+ # Is agent currently offline?
854
+ #
855
+ # === Return
856
+ # offline(Boolean):: true if agent is disconnected or not initialized
857
+ def offline?
858
+ offline = @queueing_mode == :offline || !@queue_running
859
+ end
860
+
861
+ # Start timer that waits for inactive messaging period to end before checking connectivity
862
+ #
863
+ # === Return
864
+ # true:: Always return true
865
+ def restart_inactivity_timer
866
+ @timer.cancel if @timer
867
+ @timer = EM::Timer.new(@ping_interval) do
868
+ begin
869
+ check_connection
870
+ rescue Exception => e
871
+ Log.error("Failed connectivity check", e, :trace)
872
+ end
873
+ end
874
+ true
875
+ end
876
+
877
+ # Should agent be queueing current request?
878
+ #
879
+ # === Return
880
+ # (Boolean):: true if should queue request, otherwise false
881
+ def should_queue?
882
+ @options[:offline_queueing] && offline? && !@flushing_queue
883
+ end
884
+
885
+ # Queue given request in memory
886
+ #
887
+ # === Parameters
888
+ # request(Hash):: Request to be stored
889
+ #
890
+ # === Return
891
+ # true:: Always return true
892
+ def queue_request(request)
893
+ Log.info("[offline] Queuing request: #{request.inspect}")
894
+ @restart_vote_count += 1 if @queue_running
895
+ vote_to_restart(timer_trigger = false) if @restart_vote_count >= MAX_QUEUED_REQUESTS
896
+ if @queue_initializing
897
+ # We are in the initialization callback, requests should be put at the head of the queue
898
+ @queue.unshift(request)
899
+ else
900
+ @queue << request
901
+ end
902
+ true
903
+ end
904
+
905
+ # Flush in memory queue of requests that were stored while in offline mode
906
+ # Do this asynchronously to allow for agents to respond to requests
907
+ # Once all in-memory requests have been flushed, switch off offline mode
908
+ #
909
+ # === Return
910
+ # true:: Always return true
911
+ def flush_queue
912
+ if @stop_flushing_queue
913
+ @stop_flushing_queue = false
914
+ @flushing_queue = false
915
+ else
916
+ Log.info("[offline] Starting to flush request queue of size #{@queue.size}") unless @queueing_mode == :initializing
917
+ unless @queue.empty?
918
+ r = @queue.shift
919
+ if r[:callback]
920
+ Sender.instance.__send__(r[:kind], r[:type], r[:payload], r[:target]) { |res| r[:callback].call(res) }
921
+ else
922
+ Sender.instance.__send__(r[:kind], r[:type], r[:payload], r[:target])
923
+ end
924
+ end
925
+ if @queue.empty?
926
+ Log.info("[offline] Request queue flushed, resuming normal operations") unless @queueing_mode == :initializing
927
+ @queueing_mode = :online
928
+ @flushing_queue = false
929
+ else
930
+ EM.next_tick { flush_queue }
931
+ end
932
+ end
933
+ end
934
+
935
+ end # Sender
936
+
937
+ end # RightScale