rightscale-nanite 0.4.1 → 0.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/lib/nanite.rb +71 -0
  2. data/lib/nanite/actor.rb +60 -0
  3. data/lib/nanite/actor_registry.rb +24 -0
  4. data/lib/nanite/admin.rb +153 -0
  5. data/lib/nanite/agent.rb +250 -0
  6. data/lib/nanite/amqp.rb +47 -0
  7. data/lib/nanite/cluster.rb +203 -0
  8. data/lib/nanite/config.rb +102 -0
  9. data/lib/nanite/console.rb +39 -0
  10. data/lib/nanite/daemonize.rb +13 -0
  11. data/lib/nanite/dispatcher.rb +90 -0
  12. data/lib/nanite/identity.rb +16 -0
  13. data/lib/nanite/job.rb +104 -0
  14. data/lib/nanite/local_state.rb +34 -0
  15. data/lib/nanite/log.rb +64 -0
  16. data/lib/nanite/log/formatter.rb +39 -0
  17. data/lib/nanite/mapper.rb +277 -0
  18. data/lib/nanite/mapper_proxy.rb +56 -0
  19. data/lib/nanite/packets.rb +231 -0
  20. data/lib/nanite/pid_file.rb +52 -0
  21. data/lib/nanite/reaper.rb +38 -0
  22. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  23. data/lib/nanite/security/certificate.rb +55 -0
  24. data/lib/nanite/security/certificate_cache.rb +66 -0
  25. data/lib/nanite/security/distinguished_name.rb +34 -0
  26. data/lib/nanite/security/encrypted_document.rb +46 -0
  27. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  28. data/lib/nanite/security/secure_serializer.rb +67 -0
  29. data/lib/nanite/security/signature.rb +40 -0
  30. data/lib/nanite/security/static_certificate_store.rb +35 -0
  31. data/lib/nanite/security_provider.rb +47 -0
  32. data/lib/nanite/serializer.rb +52 -0
  33. data/lib/nanite/state.rb +164 -0
  34. data/lib/nanite/streaming.rb +125 -0
  35. data/lib/nanite/util.rb +51 -0
  36. data/spec/actor_registry_spec.rb +62 -0
  37. data/spec/actor_spec.rb +59 -0
  38. data/spec/agent_spec.rb +235 -0
  39. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  40. data/spec/certificate_cache_spec.rb +49 -0
  41. data/spec/certificate_spec.rb +27 -0
  42. data/spec/cluster_spec.rb +300 -0
  43. data/spec/dispatcher_spec.rb +136 -0
  44. data/spec/distinguished_name_spec.rb +24 -0
  45. data/spec/encrypted_document_spec.rb +21 -0
  46. data/spec/job_spec.rb +219 -0
  47. data/spec/local_state_spec.rb +112 -0
  48. data/spec/packet_spec.rb +218 -0
  49. data/spec/rsa_key_pair_spec.rb +33 -0
  50. data/spec/secure_serializer_spec.rb +41 -0
  51. data/spec/serializer_spec.rb +107 -0
  52. data/spec/signature_spec.rb +30 -0
  53. data/spec/spec_helper.rb +23 -0
  54. data/spec/static_certificate_store_spec.rb +30 -0
  55. data/spec/util_spec.rb +63 -0
  56. metadata +62 -1
@@ -0,0 +1,16 @@
1
+ module Nanite
2
+ module Identity
3
+ def self.generate
4
+ values = [
5
+ rand(0x0010000),
6
+ rand(0x0010000),
7
+ rand(0x0010000),
8
+ rand(0x0010000),
9
+ rand(0x0010000),
10
+ rand(0x1000000),
11
+ rand(0x1000000),
12
+ ]
13
+ "%04x%04x%04x%04x%04x%06x%06x" % values
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,104 @@
1
+ module Nanite
2
+ class JobWarden
3
+ attr_reader :serializer, :jobs
4
+
5
+ def initialize(serializer)
6
+ @serializer = serializer
7
+ @jobs = {}
8
+ end
9
+
10
+ def new_job(request, targets, inthandler = nil, blk = nil)
11
+ job = Job.new(request, targets, inthandler, blk)
12
+ jobs[job.token] = job
13
+ job
14
+ end
15
+
16
+ def process(msg)
17
+ Nanite::Log.debug("processing message: #{msg.inspect}")
18
+
19
+ if job = jobs[msg.token]
20
+ job.process(msg)
21
+
22
+ if job.intermediate_handler && (job.pending_keys.size > 0)
23
+
24
+ unless job.pending_keys.size == 1
25
+ raise "IntermediateMessages are currently dispatched as they arrive, shouldn't have more than one key in pending_keys: #{job.pending_keys.inspect}"
26
+ end
27
+
28
+ key = job.pending_keys.first
29
+ handler = job.intermediate_handler_for_key(key)
30
+ if handler
31
+ case handler.arity
32
+ when 3
33
+ handler.call(key, msg.from, job.intermediate_state[msg.from][key].last)
34
+ when 4
35
+ handler.call(key, msg.from, job.intermediate_state[msg.from][key].last, job)
36
+ end
37
+ end
38
+
39
+ job.reset_pending_intermediate_state_keys
40
+ end
41
+
42
+ if job.completed?
43
+ jobs.delete(job.token)
44
+ if job.completed
45
+ case job.completed.arity
46
+ when 1
47
+ job.completed.call(job.results)
48
+ when 2
49
+ job.completed.call(job.results, job)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end # JobWarden
56
+
57
+ class Job
58
+ attr_reader :results, :request, :token, :completed, :intermediate_state, :pending_keys, :intermediate_handler
59
+ attr_accessor :targets # This can be updated when a request gets picked up from the offline queue
60
+
61
+ def initialize(request, targets, inthandler = nil, blk = nil)
62
+ @request = request
63
+ @targets = targets
64
+ @token = @request.token
65
+ @results = {}
66
+ @intermediate_handler = inthandler
67
+ @pending_keys = []
68
+ @completed = blk
69
+ @intermediate_state = {}
70
+ end
71
+
72
+ def process(msg)
73
+ case msg
74
+ when Result
75
+ results[msg.from] = msg.results
76
+ targets.delete(msg.from)
77
+ when IntermediateMessage
78
+ intermediate_state[msg.from] ||= {}
79
+ intermediate_state[msg.from][msg.messagekey] ||= []
80
+ intermediate_state[msg.from][msg.messagekey] << msg.message
81
+ @pending_keys << msg.messagekey
82
+ end
83
+ end
84
+
85
+ def intermediate_handler_for_key(key)
86
+ return nil unless @intermediate_handler
87
+ case @intermediate_handler
88
+ when Proc
89
+ @intermediate_handler
90
+ when Hash
91
+ @intermediate_handler[key] || @intermediate_handler['*']
92
+ end
93
+ end
94
+
95
+ def reset_pending_intermediate_state_keys
96
+ @pending_keys = []
97
+ end
98
+
99
+ def completed?
100
+ targets.empty?
101
+ end
102
+ end # Job
103
+
104
+ end # Nanite
@@ -0,0 +1,34 @@
1
+ module Nanite
2
+ class LocalState < ::Hash
3
+ def initialize(hsh={})
4
+ hsh.each do |k,v|
5
+ self[k] = v
6
+ end
7
+ end
8
+
9
+ def all_services
10
+ all(:services)
11
+ end
12
+
13
+ def all_tags
14
+ all(:tags)
15
+ end
16
+
17
+ def nanites_for(service, *tags)
18
+ tags = tags.dup.flatten
19
+ nanites = select { |name, state| state[:services].include?(service) }
20
+ unless tags.empty?
21
+ nanites.select { |a| !(a[1][:tags] & tags).empty? }
22
+ else
23
+ nanites
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def all(key)
30
+ map { |n,s| s[key] }.flatten.uniq.compact
31
+ end
32
+
33
+ end # LocalState
34
+ end # Nanite
@@ -0,0 +1,64 @@
1
+ require 'nanite/config'
2
+ require 'nanite/log/formatter'
3
+ require 'logger'
4
+
5
+ module Nanite
6
+ class Log
7
+
8
+ @logger = nil
9
+
10
+ class << self
11
+ attr_accessor :logger, :level, :file #:nodoc
12
+
13
+ # Use Nanite::Logger.init when you want to set up the logger manually.
14
+ # If this method is called with no arguments, it will log to STDOUT at the :info level.
15
+ # It also configures the Logger instance it creates to use the custom Nanite::Log::Formatter class.
16
+ def init(identity, path = false)
17
+ if path
18
+ @file = File.join(path, "nanite.#{identity}.log")
19
+ else
20
+ @file = STDOUT
21
+ end
22
+ @logger = Logger.new(file)
23
+ @logger.formatter = Nanite::Log::Formatter.new
24
+ level = @log_level = :info
25
+ end
26
+
27
+ # Sets the level for the Logger by symbol or by command line argument.
28
+ # Throws an ArgumentError if you feed it a bogus log level (that is not
29
+ # one of :debug, :info, :warn, :error, :fatal or the corresponding strings)
30
+ def level=(loglevel)
31
+ init() unless @logger
32
+ loglevel = loglevel.intern if loglevel.is_a?(String)
33
+ @logger.info("Setting log level to #{loglevel.to_s.upcase}")
34
+ case loglevel
35
+ when :debug
36
+ @logger.level = Logger::DEBUG
37
+ when :info
38
+ @logger.level = Logger::INFO
39
+ when :warn
40
+ @logger.level = Logger::WARN
41
+ when :error
42
+ @logger.level = Logger::ERROR
43
+ when :fatal
44
+ @logger.level = Logger::FATAL
45
+ else
46
+ raise ArgumentError, "Log level must be one of :debug, :info, :warn, :error, or :fatal"
47
+ end
48
+ end
49
+
50
+ # Passes any other method calls on directly to the underlying Logger object created with init. If
51
+ # this method gets hit before a call to Nanite::Logger.init has been made, it will call
52
+ # Nanite::Logger.init() with no arguments.
53
+ def method_missing(method_symbol, *args)
54
+ init(identity) unless @logger
55
+ if args.length > 0
56
+ @logger.send(method_symbol, *args)
57
+ else
58
+ @logger.send(method_symbol)
59
+ end
60
+ end
61
+
62
+ end # class << self
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ require 'logger'
2
+ require 'time'
3
+
4
+ module Nanite
5
+ class Log
6
+ class Formatter < Logger::Formatter
7
+ @@show_time = true
8
+
9
+ def self.show_time=(show=false)
10
+ @@show_time = show
11
+ end
12
+
13
+ # Prints a log message as '[time] severity: message' if Nanite::Log::Formatter.show_time == true.
14
+ # Otherwise, doesn't print the time.
15
+ def call(severity, time, progname, msg)
16
+ if @@show_time
17
+ sprintf("[%s] %s: %s\n", time.rfc2822(), severity, msg2str(msg))
18
+ else
19
+ sprintf("%s: %s\n", severity, msg2str(msg))
20
+ end
21
+ end
22
+
23
+ # Converts some argument to a Logger.severity() call to a string. Regular strings pass through like
24
+ # normal, Exceptions get formatted as "message (class)\nbacktrace", and other random stuff gets
25
+ # put through "object.inspect"
26
+ def msg2str(msg)
27
+ case msg
28
+ when ::String
29
+ msg
30
+ when ::Exception
31
+ "#{ msg.message } (#{ msg.class })\n" <<
32
+ (msg.backtrace || []).join("\n")
33
+ else
34
+ msg.inspect
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,277 @@
1
+ module Nanite
2
+ # Mappers are control nodes in nanite clusters. Nanite clusters
3
+ # can follow peer-to-peer model of communication as well as client-server,
4
+ # and mappers are nodes that know who to send work requests to agents.
5
+ #
6
+ # Mappers can reside inside a front end web application written in Merb/Rails
7
+ # and distribute heavy lifting to actors that register with the mapper as soon
8
+ # as they go online.
9
+ #
10
+ # Each mapper tracks nanites registered with it. It periodically checks
11
+ # when the last time a certain nanite sent a heartbeat notification,
12
+ # and removes those that have timed out from the list of available workers.
13
+ # As soon as a worker goes back online again it re-registers itself
14
+ # and the mapper adds it to the list and makes it available to
15
+ # be called again.
16
+ #
17
+ # This makes Nanite clusters self-healing and immune to individual node
18
+ # failures.
19
+ class Mapper
20
+ include AMQPHelper
21
+ include ConsoleHelper
22
+ include DaemonizeHelper
23
+
24
+ attr_reader :cluster, :identity, :job_warden, :options, :serializer, :amq
25
+
26
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({:user => 'mapper', :identity => Identity.generate, :agent_timeout => 15,
27
+ :offline_redelivery_frequency => 10, :persistent => false, :offline_failsafe => false}) unless defined?(DEFAULT_OPTIONS)
28
+
29
+ # Initializes a new mapper and establishes
30
+ # AMQP connection. This must be used inside EM.run block or if EventMachine reactor
31
+ # is already started, for instance, by a Thin server that your Merb/Rails
32
+ # application runs on.
33
+ #
34
+ # Mapper options:
35
+ #
36
+ # identity : identity of this mapper, may be any string
37
+ #
38
+ # format : format to use for packets serialization. Can be :marshal, :json or :yaml or :secure.
39
+ # Defaults to Ruby's Marshall format. For interoperability with
40
+ # AMQP clients implemented in other languages, use JSON.
41
+ #
42
+ # Note that Nanite uses JSON gem,
43
+ # and ActiveSupport's JSON encoder may cause clashes
44
+ # if ActiveSupport is loaded after JSON gem.
45
+ #
46
+ # Also using the secure format requires prior initialization of the serializer, see
47
+ # SecureSerializer.init
48
+ #
49
+ # log_level : the verbosity of logging, can be debug, info, warn, error or fatal.
50
+ #
51
+ # agent_timeout : how long to wait before an agent is considered to be offline
52
+ # and thus removed from the list of available agents.
53
+ #
54
+ # log_dir : log file path, defaults to the current working directory.
55
+ #
56
+ # console : true tells mapper to start interactive console
57
+ #
58
+ # daemonize : true tells mapper to daemonize
59
+ #
60
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
61
+ # defaults to the root or the current working directory.
62
+ #
63
+ # offline_redelivery_frequency : The frequency in seconds that messages stored in the offline queue will be retrieved
64
+ # for attempted redelivery to the nanites. Default is 10 seconds.
65
+ #
66
+ # persistent : true instructs the AMQP broker to save messages to persistent storage so that they aren't lost when the
67
+ # broker is restarted. Default is false. Can be overriden on a per-message basis using the request and push methods.
68
+ #
69
+ # secure : use Security features of rabbitmq to restrict nanites to themselves
70
+ #
71
+ # Connection options:
72
+ #
73
+ # vhost : AMQP broker vhost that should be used
74
+ #
75
+ # user : AMQP broker user
76
+ #
77
+ # pass : AMQP broker password
78
+ #
79
+ # host : host AMQP broker (or node of interest) runs on,
80
+ # defaults to 0.0.0.0
81
+ #
82
+ # port : port AMQP broker (or node of interest) runs on,
83
+ # this defaults to 5672, port used by some widely
84
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
85
+ #
86
+ # @api :public:
87
+ def self.start(options = {})
88
+ mapper = new(options)
89
+ mapper.run
90
+ mapper
91
+ end
92
+
93
+ def initialize(options)
94
+ @options = DEFAULT_OPTIONS.clone.merge(options)
95
+ root = options[:root] || @options[:root]
96
+ custom_config = if root
97
+ file = File.expand_path(File.join(root, 'config.yml'))
98
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
99
+ else
100
+ {}
101
+ end
102
+ options.delete(:identity) unless options[:identity]
103
+ @options.update(custom_config.merge(options))
104
+ @identity = "mapper-#{@options[:identity]}"
105
+ @options[:file_root] ||= File.join(@options[:root], 'files')
106
+ @options.freeze
107
+ end
108
+
109
+ def run
110
+ log_path = false
111
+ if @options[:daemonize]
112
+ log_path = (@options[:log_dir] || @options[:root] || Dir.pwd)
113
+ end
114
+ Nanite::Log.init(@identity, log_path)
115
+ Nanite::Log.level = @options[:log_level] if @options[:log_level]
116
+ @serializer = Serializer.new(@options[:format])
117
+ pid_file = PidFile.new(@identity, @options)
118
+ pid_file.check
119
+ if @options[:daemonize]
120
+ daemonize
121
+ pid_file.write
122
+ at_exit { pid_file.remove }
123
+ end
124
+ @amq = start_amqp(@options)
125
+ @job_warden = JobWarden.new(@serializer)
126
+ @cluster = Cluster.new(@amq, @options[:agent_timeout], @options[:identity], @serializer, self, @options[:redis])
127
+ Nanite::Log.info('starting mapper')
128
+ setup_queues
129
+ start_console if @options[:console] && !@options[:daemonize]
130
+ end
131
+
132
+ # Make a nanite request which expects a response.
133
+ #
134
+ # ==== Parameters
135
+ # type<String>:: The dispatch route for the request
136
+ # payload<Object>:: Payload to send. This will get marshalled en route
137
+ #
138
+ # ==== Options
139
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
140
+ # :least_loaded:: Pick the nanite which has the lowest load.
141
+ # :all:: Send the request to all nanites which respond to the service.
142
+ # :random:: Randomly pick a nanite.
143
+ # :rr: Select a nanite according to round robin ordering.
144
+ # :target<String>:: Select a specific nanite via identity, rather than using
145
+ # a selector.
146
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
147
+ # the nanites are offline. Messages will be redelivered when nanites come online.
148
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
149
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
150
+ # storage so that it isnt lost when the broker is restarted.
151
+ # Default is false unless the mapper was started with the --persistent flag.
152
+ # :intermediate_handler:: Takes a lambda to call when an IntermediateMessage
153
+ # event arrives from a nanite. If passed a Hash, hash keys should correspond to
154
+ # the IntermediateMessage keys provided by the nanite, and each should have a value
155
+ # that is a lambda/proc taking the parameters specified here. Can supply a key '*'
156
+ # as a catch-all for unmatched keys.
157
+ #
158
+ # ==== Block Parameters for intermediate_handler
159
+ # key<String>:: array of unique keys for which intermediate state has been received
160
+ # since the last call to this block.
161
+ # nanite<String>:: nanite which sent the message.
162
+ # state:: most recently delivered intermediate state for the key provided.
163
+ # job:: (optional) -- if provided, this parameter gets the whole job object, if there's
164
+ # a reason to do more complex work with the job.
165
+ #
166
+ # ==== Block Parameters
167
+ # :results<Object>:: The returned value from the nanite actor.
168
+ #
169
+ # @api :public:
170
+ def request(type, payload = '', opts = {}, &blk)
171
+ request = build_deliverable(Request, type, payload, opts)
172
+ send_request(request, opts, &blk)
173
+ end
174
+
175
+ # Send request with pre-built request instance
176
+ def send_request(request, opts = {}, &blk)
177
+ request.reply_to = identity
178
+ intm_handler = opts.delete(:intermediate_handler)
179
+ targets = cluster.targets_for(request)
180
+ if !targets.empty?
181
+ job = job_warden.new_job(request, targets, intm_handler, blk)
182
+ cluster.route(request, job.targets)
183
+ job
184
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
185
+ job_warden.new_job(request, [], intm_handler, blk)
186
+ cluster.publish(request, 'mapper-offline')
187
+ :offline
188
+ else
189
+ false
190
+ end
191
+ end
192
+
193
+ # Make a nanite request which does not expect a response.
194
+ #
195
+ # ==== Parameters
196
+ # type<String>:: The dispatch route for the request
197
+ # payload<Object>:: Payload to send. This will get marshalled en route
198
+ #
199
+ # ==== Options
200
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
201
+ # :least_loaded:: Pick the nanite which has the lowest load.
202
+ # :all:: Send the request to all nanites which respond to the service.
203
+ # :random:: Randomly pick a nanite.
204
+ # :rr: Select a nanite according to round robin ordering.
205
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
206
+ # the nanites are offline. Messages will be redelivered when nanites come online.
207
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
208
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
209
+ # storage so that it isnt lost when the broker is restarted.
210
+ # Default is false unless the mapper was started with the --persistent flag.
211
+ #
212
+ # @api :public:
213
+ def push(type, payload = '', opts = {})
214
+ push = build_deliverable(Push, type, payload, opts)
215
+ targets = cluster.targets_for(push)
216
+ if !targets.empty?
217
+ cluster.route(push, targets)
218
+ true
219
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
220
+ cluster.publish(push, 'mapper-offline')
221
+ :offline
222
+ else
223
+ false
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ def build_deliverable(deliverable_type, type, payload, opts)
230
+ deliverable = deliverable_type.new(type, payload, opts)
231
+ deliverable.from = identity
232
+ deliverable.token = Identity.generate
233
+ deliverable.persistent = opts.key?(:persistent) ? opts[:persistent] : options[:persistent]
234
+ deliverable
235
+ end
236
+
237
+ def setup_queues
238
+ setup_offline_queue
239
+ setup_message_queue
240
+ end
241
+
242
+ def setup_offline_queue
243
+ offline_queue = amq.queue('mapper-offline', :durable => true)
244
+ offline_queue.subscribe(:ack => true) do |info, deliverable|
245
+ deliverable = serializer.load(deliverable)
246
+ targets = cluster.targets_for(deliverable)
247
+ unless targets.empty?
248
+ info.ack
249
+ if deliverable.kind_of?(Request)
250
+ if job = job_warden.jobs[deliverable.token]
251
+ job.targets = targets
252
+ else
253
+ deliverable.reply_to = identity
254
+ job_warden.new_job(deliverable, targets)
255
+ end
256
+ end
257
+ cluster.route(deliverable, targets)
258
+ end
259
+ end
260
+
261
+ EM.add_periodic_timer(options[:offline_redelivery_frequency]) { offline_queue.recover }
262
+ end
263
+
264
+ def setup_message_queue
265
+ amq.queue(identity, :exclusive => true).bind(amq.fanout(identity)).subscribe do |msg|
266
+ begin
267
+ msg = serializer.load(msg)
268
+ Nanite::Log.debug("got result from #{msg.from}: #{msg.results.inspect}")
269
+ job_warden.process(msg)
270
+ rescue Exception => e
271
+ Nanite::Log.error("Error handling result: #{e.message}")
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
277
+