right_agent 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +78 -0
  3. data/Rakefile +86 -0
  4. data/lib/right_agent.rb +66 -0
  5. data/lib/right_agent/actor.rb +163 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +189 -0
  8. data/lib/right_agent/agent.rb +735 -0
  9. data/lib/right_agent/agent_config.rb +403 -0
  10. data/lib/right_agent/agent_identity.rb +209 -0
  11. data/lib/right_agent/agent_tags_manager.rb +213 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/broker_client.rb +683 -0
  14. data/lib/right_agent/command.rb +30 -0
  15. data/lib/right_agent/command/agent_manager_commands.rb +134 -0
  16. data/lib/right_agent/command/command_client.rb +136 -0
  17. data/lib/right_agent/command/command_constants.rb +42 -0
  18. data/lib/right_agent/command/command_io.rb +128 -0
  19. data/lib/right_agent/command/command_parser.rb +87 -0
  20. data/lib/right_agent/command/command_runner.rb +105 -0
  21. data/lib/right_agent/command/command_serializer.rb +63 -0
  22. data/lib/right_agent/console.rb +65 -0
  23. data/lib/right_agent/core_payload_types.rb +42 -0
  24. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  25. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  26. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  27. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  28. data/lib/right_agent/core_payload_types/dev_repositories.rb +90 -0
  29. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  30. data/lib/right_agent/core_payload_types/executable_bundle.rb +138 -0
  31. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  32. data/lib/right_agent/core_payload_types/login_user.rb +62 -0
  33. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  34. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +60 -0
  35. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  36. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  37. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +73 -0
  38. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  39. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  40. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  41. data/lib/right_agent/daemonize.rb +35 -0
  42. data/lib/right_agent/dispatcher.rb +348 -0
  43. data/lib/right_agent/enrollment_result.rb +217 -0
  44. data/lib/right_agent/exceptions.rb +30 -0
  45. data/lib/right_agent/ha_broker_client.rb +1278 -0
  46. data/lib/right_agent/idempotent_request.rb +140 -0
  47. data/lib/right_agent/log.rb +418 -0
  48. data/lib/right_agent/monkey_patches.rb +29 -0
  49. data/lib/right_agent/monkey_patches/amqp_patch.rb +274 -0
  50. data/lib/right_agent/monkey_patches/ruby_patch.rb +49 -0
  51. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  52. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  53. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  54. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  55. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  56. data/lib/right_agent/monkey_patches/ruby_patch/singleton_patch.rb +46 -0
  57. data/lib/right_agent/monkey_patches/ruby_patch/string_patch.rb +107 -0
  58. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +90 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  64. data/lib/right_agent/multiplexer.rb +91 -0
  65. data/lib/right_agent/operation_result.rb +270 -0
  66. data/lib/right_agent/packets.rb +637 -0
  67. data/lib/right_agent/payload_formatter.rb +104 -0
  68. data/lib/right_agent/pid_file.rb +159 -0
  69. data/lib/right_agent/platform.rb +319 -0
  70. data/lib/right_agent/platform/darwin.rb +227 -0
  71. data/lib/right_agent/platform/linux.rb +268 -0
  72. data/lib/right_agent/platform/windows.rb +1204 -0
  73. data/lib/right_agent/scripts/agent_controller.rb +522 -0
  74. data/lib/right_agent/scripts/agent_deployer.rb +379 -0
  75. data/lib/right_agent/scripts/common_parser.rb +153 -0
  76. data/lib/right_agent/scripts/log_level_manager.rb +193 -0
  77. data/lib/right_agent/scripts/stats_manager.rb +256 -0
  78. data/lib/right_agent/scripts/usage.rb +58 -0
  79. data/lib/right_agent/secure_identity.rb +92 -0
  80. data/lib/right_agent/security.rb +32 -0
  81. data/lib/right_agent/security/cached_certificate_store_proxy.rb +63 -0
  82. data/lib/right_agent/security/certificate.rb +102 -0
  83. data/lib/right_agent/security/certificate_cache.rb +89 -0
  84. data/lib/right_agent/security/distinguished_name.rb +56 -0
  85. data/lib/right_agent/security/encrypted_document.rb +84 -0
  86. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  87. data/lib/right_agent/security/signature.rb +86 -0
  88. data/lib/right_agent/security/static_certificate_store.rb +69 -0
  89. data/lib/right_agent/sender.rb +937 -0
  90. data/lib/right_agent/serialize.rb +29 -0
  91. data/lib/right_agent/serialize/message_pack.rb +102 -0
  92. data/lib/right_agent/serialize/secure_serializer.rb +131 -0
  93. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  94. data/lib/right_agent/serialize/serializable.rb +135 -0
  95. data/lib/right_agent/serialize/serializer.rb +149 -0
  96. data/lib/right_agent/stats_helper.rb +731 -0
  97. data/lib/right_agent/subprocess.rb +38 -0
  98. data/lib/right_agent/tracer.rb +124 -0
  99. data/right_agent.gemspec +60 -0
  100. data/spec/actor_registry_spec.rb +81 -0
  101. data/spec/actor_spec.rb +99 -0
  102. data/spec/agent_config_spec.rb +226 -0
  103. data/spec/agent_identity_spec.rb +75 -0
  104. data/spec/agent_spec.rb +571 -0
  105. data/spec/broker_client_spec.rb +961 -0
  106. data/spec/command/agent_manager_commands_spec.rb +51 -0
  107. data/spec/command/command_io_spec.rb +93 -0
  108. data/spec/command/command_parser_spec.rb +79 -0
  109. data/spec/command/command_runner_spec.rb +72 -0
  110. data/spec/command/command_serializer_spec.rb +51 -0
  111. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  112. data/spec/core_payload_types/executable_bundle_spec.rb +59 -0
  113. data/spec/core_payload_types/login_user_spec.rb +98 -0
  114. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  115. data/spec/core_payload_types/spec_helper.rb +23 -0
  116. data/spec/dispatcher_spec.rb +372 -0
  117. data/spec/enrollment_result_spec.rb +53 -0
  118. data/spec/ha_broker_client_spec.rb +1673 -0
  119. data/spec/idempotent_request_spec.rb +136 -0
  120. data/spec/log_spec.rb +177 -0
  121. data/spec/monkey_patches/amqp_patch_spec.rb +100 -0
  122. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  123. data/spec/monkey_patches/string_patch_spec.rb +99 -0
  124. data/spec/multiplexer_spec.rb +48 -0
  125. data/spec/operation_result_spec.rb +171 -0
  126. data/spec/packets_spec.rb +418 -0
  127. data/spec/platform/platform_spec.rb +60 -0
  128. data/spec/results_mock.rb +45 -0
  129. data/spec/secure_identity_spec.rb +50 -0
  130. data/spec/security/cached_certificate_store_proxy_spec.rb +56 -0
  131. data/spec/security/certificate_cache_spec.rb +71 -0
  132. data/spec/security/certificate_spec.rb +49 -0
  133. data/spec/security/distinguished_name_spec.rb +46 -0
  134. data/spec/security/encrypted_document_spec.rb +55 -0
  135. data/spec/security/rsa_key_pair_spec.rb +55 -0
  136. data/spec/security/signature_spec.rb +66 -0
  137. data/spec/security/static_certificate_store_spec.rb +52 -0
  138. data/spec/sender_spec.rb +887 -0
  139. data/spec/serialize/message_pack_spec.rb +131 -0
  140. data/spec/serialize/secure_serializer_spec.rb +102 -0
  141. data/spec/serialize/serializable_spec.rb +90 -0
  142. data/spec/serialize/serializer_spec.rb +174 -0
  143. data/spec/spec.opts +2 -0
  144. data/spec/spec_helper.rb +77 -0
  145. data/spec/stats_helper_spec.rb +681 -0
  146. data/spec/tracer_spec.rb +114 -0
  147. metadata +320 -0
@@ -0,0 +1,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