shift-nanite 0.4.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/LICENSE +201 -0
  2. data/README.rdoc +430 -0
  3. data/Rakefile +76 -0
  4. data/TODO +24 -0
  5. data/bin/nanite-admin +65 -0
  6. data/bin/nanite-agent +79 -0
  7. data/bin/nanite-mapper +50 -0
  8. data/lib/nanite.rb +74 -0
  9. data/lib/nanite/actor.rb +71 -0
  10. data/lib/nanite/actor_registry.rb +26 -0
  11. data/lib/nanite/admin.rb +138 -0
  12. data/lib/nanite/agent.rb +264 -0
  13. data/lib/nanite/amqp.rb +58 -0
  14. data/lib/nanite/cluster.rb +250 -0
  15. data/lib/nanite/config.rb +112 -0
  16. data/lib/nanite/console.rb +39 -0
  17. data/lib/nanite/daemonize.rb +13 -0
  18. data/lib/nanite/identity.rb +16 -0
  19. data/lib/nanite/job.rb +104 -0
  20. data/lib/nanite/local_state.rb +38 -0
  21. data/lib/nanite/log.rb +66 -0
  22. data/lib/nanite/log/formatter.rb +39 -0
  23. data/lib/nanite/mapper.rb +309 -0
  24. data/lib/nanite/mapper_proxy.rb +67 -0
  25. data/lib/nanite/nanite_dispatcher.rb +92 -0
  26. data/lib/nanite/packets.rb +365 -0
  27. data/lib/nanite/pid_file.rb +52 -0
  28. data/lib/nanite/reaper.rb +39 -0
  29. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  30. data/lib/nanite/security/certificate.rb +55 -0
  31. data/lib/nanite/security/certificate_cache.rb +66 -0
  32. data/lib/nanite/security/distinguished_name.rb +34 -0
  33. data/lib/nanite/security/encrypted_document.rb +46 -0
  34. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  35. data/lib/nanite/security/secure_serializer.rb +68 -0
  36. data/lib/nanite/security/signature.rb +46 -0
  37. data/lib/nanite/security/static_certificate_store.rb +35 -0
  38. data/lib/nanite/security_provider.rb +47 -0
  39. data/lib/nanite/serializer.rb +52 -0
  40. data/lib/nanite/state.rb +168 -0
  41. data/lib/nanite/streaming.rb +125 -0
  42. data/lib/nanite/util.rb +58 -0
  43. data/spec/actor_registry_spec.rb +60 -0
  44. data/spec/actor_spec.rb +77 -0
  45. data/spec/agent_spec.rb +240 -0
  46. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  47. data/spec/certificate_cache_spec.rb +49 -0
  48. data/spec/certificate_spec.rb +27 -0
  49. data/spec/cluster_spec.rb +622 -0
  50. data/spec/distinguished_name_spec.rb +24 -0
  51. data/spec/encrypted_document_spec.rb +21 -0
  52. data/spec/job_spec.rb +251 -0
  53. data/spec/local_state_spec.rb +130 -0
  54. data/spec/nanite_dispatcher_spec.rb +136 -0
  55. data/spec/packet_spec.rb +220 -0
  56. data/spec/rsa_key_pair_spec.rb +33 -0
  57. data/spec/secure_serializer_spec.rb +41 -0
  58. data/spec/serializer_spec.rb +107 -0
  59. data/spec/signature_spec.rb +30 -0
  60. data/spec/spec_helper.rb +33 -0
  61. data/spec/static_certificate_store_spec.rb +30 -0
  62. data/spec/util_spec.rb +63 -0
  63. metadata +129 -0
@@ -0,0 +1,38 @@
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, b| !(b[:tags] & tags).empty? }
22
+ else
23
+ nanites
24
+ end.to_a
25
+ end
26
+
27
+ def update_status(name, status)
28
+ self[name].update(:status => status, :timestamp => Time.now.utc.to_i)
29
+ end
30
+
31
+ private
32
+
33
+ def all(key)
34
+ map { |n,s| s[key] }.flatten.uniq.compact
35
+ end
36
+
37
+ end # LocalState
38
+ end # Nanite
@@ -0,0 +1,66 @@
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
+ # Map log levels symbols to values
11
+ LEVELS = { :debug => Logger::DEBUG,
12
+ :info => Logger::INFO,
13
+ :warn => Logger::WARN,
14
+ :error => Logger::ERROR,
15
+ :fatal => Logger::FATAL }
16
+
17
+ class << self
18
+ attr_accessor :logger, :level, :file #:nodoc
19
+
20
+ # Use Nanite::Logger.init when you want to set up the logger manually.
21
+ # If this method is called with no arguments, it will log to STDOUT at the :info level.
22
+ # It also configures the Logger instance it creates to use the custom Nanite::Log::Formatter class.
23
+ def init(identity = nil, path = false)
24
+ if path
25
+ @file = File.join(path, "nanite.#{identity}.log")
26
+ else
27
+ @file = STDOUT
28
+ end
29
+ @logger = Logger.new(file)
30
+ @logger.formatter = Nanite::Log::Formatter.new
31
+ Log.level = :info
32
+ end
33
+
34
+ # Sets the level for the Logger by symbol or by command line argument.
35
+ # Throws an ArgumentError if you feed it a bogus log level (that is not
36
+ # one of :debug, :info, :warn, :error, :fatal or the corresponding strings or a valid Logger level)
37
+ def level=(loglevel)
38
+ init unless @logger
39
+ lvl = case loglevel
40
+ when String then loglevel.intern
41
+ when Integer then LEVELS.invert[loglevel]
42
+ else loglevel
43
+ end
44
+ unless LEVELS.include?(lvl)
45
+ raise(ArgumentError, 'Log level must be one of :debug, :info, :warn, :error, or :fatal')
46
+ end
47
+ @logger.info("[setup] setting log level to #{lvl.to_s.upcase}")
48
+ @level = lvl
49
+ @logger.level = LEVELS[lvl]
50
+ end
51
+
52
+ # Passes any other method calls on directly to the underlying Logger object created with init. If
53
+ # this method gets hit before a call to Nanite::Logger.init has been made, it will call
54
+ # Nanite::Logger.init() with no arguments.
55
+ def method_missing(method_symbol, *args)
56
+ init unless @logger
57
+ if args.length > 0
58
+ @logger.send(method_symbol, *args)
59
+ else
60
+ @logger.send(method_symbol)
61
+ end
62
+ end
63
+
64
+ end # class << self
65
+ end
66
+ 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,309 @@
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({
27
+ :user => 'mapper',
28
+ :identity => Identity.generate,
29
+ :agent_timeout => 15,
30
+ :offline_redelivery_frequency => 10,
31
+ :persistent => false,
32
+ :offline_failsafe => false,
33
+ :callbacks => {}
34
+ }) unless defined?(DEFAULT_OPTIONS)
35
+
36
+ # Initializes a new mapper and establishes
37
+ # AMQP connection. This must be used inside EM.run block or if EventMachine reactor
38
+ # is already started, for instance, by a Thin server that your Merb/Rails
39
+ # application runs on.
40
+ #
41
+ # Mapper options:
42
+ #
43
+ # identity : identity of this mapper, may be any string
44
+ #
45
+ # format : format to use for packets serialization. Can be :marshal, :json or :yaml or :secure.
46
+ # Defaults to Ruby's Marshall format. For interoperability with
47
+ # AMQP clients implemented in other languages, use JSON.
48
+ #
49
+ # Note that Nanite uses JSON gem,
50
+ # and ActiveSupport's JSON encoder may cause clashes
51
+ # if ActiveSupport is loaded after JSON gem.
52
+ #
53
+ # Also using the secure format requires prior initialization of the serializer, see
54
+ # SecureSerializer.init
55
+ #
56
+ # log_level : the verbosity of logging, can be debug, info, warn, error or fatal.
57
+ #
58
+ # agent_timeout : how long to wait before an agent is considered to be offline
59
+ # and thus removed from the list of available agents.
60
+ #
61
+ # log_dir : log file path, defaults to the current working directory.
62
+ #
63
+ # console : true tells mapper to start interactive console
64
+ #
65
+ # daemonize : true tells mapper to daemonize
66
+ #
67
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
68
+ # defaults to the root or the current working directory.
69
+ #
70
+ # offline_redelivery_frequency : The frequency in seconds that messages stored in the offline queue will be retrieved
71
+ # for attempted redelivery to the nanites. Default is 10 seconds.
72
+ #
73
+ # persistent : true instructs the AMQP broker to save messages to persistent storage so that they aren't lost when the
74
+ # broker is restarted. Default is false. Can be overriden on a per-message basis using the request and push methods.
75
+ #
76
+ # secure : use Security features of rabbitmq to restrict nanites to themselves
77
+ #
78
+ # prefetch : Sets prefetch (only supported in RabbitMQ >= 1.6)
79
+ # callbacks : A set of callbacks to have code executed on specific events, supported events are :register,
80
+ # :unregister and :timeout. Parameter must be a hash with the corresponding events as keys and
81
+ # a block as value. The block will get the corresponding nanite's identity and a copy of the
82
+ # mapper
83
+ #
84
+ # Connection options:
85
+ #
86
+ # vhost : AMQP broker vhost that should be used
87
+ #
88
+ # user : AMQP broker user
89
+ #
90
+ # pass : AMQP broker password
91
+ #
92
+ # host : host AMQP broker (or node of interest) runs on,
93
+ # defaults to 0.0.0.0
94
+ #
95
+ # port : port AMQP broker (or node of interest) runs on,
96
+ # this defaults to 5672, port used by some widely
97
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
98
+ #
99
+ # @api :public:
100
+ def self.start(options = {})
101
+ mapper = new(options)
102
+ mapper.run
103
+ mapper
104
+ end
105
+
106
+ def initialize(options)
107
+ @options = DEFAULT_OPTIONS.clone.merge(options)
108
+ root = options[:root] || @options[:root]
109
+ custom_config = if root
110
+ file = File.expand_path(File.join(root, 'config.yml'))
111
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
112
+ else
113
+ {}
114
+ end
115
+ options.delete(:identity) unless options[:identity]
116
+ @options.update(custom_config.merge(options))
117
+ @identity = "mapper-#{@options[:identity]}"
118
+ @options[:file_root] ||= File.join(@options[:root], 'files')
119
+ @options[:log_path] = false
120
+ if @options[:daemonize]
121
+ @options[:log_path] = (@options[:log_dir] || @options[:root] || Dir.pwd)
122
+ end
123
+ @options.freeze
124
+ end
125
+
126
+ def run
127
+ setup_logging
128
+ @serializer = Serializer.new(@options[:format])
129
+ pid_file = PidFile.new(@identity, @options)
130
+ pid_file.check
131
+ if @options[:daemonize]
132
+ daemonize(@identity, @options)
133
+ pid_file.write
134
+ at_exit { pid_file.remove }
135
+ else
136
+ trap("INT") {exit}
137
+ end
138
+ @amq = start_amqp(@options)
139
+ @job_warden = JobWarden.new(@serializer)
140
+ setup_cluster
141
+ Nanite::Log.info('[setup] starting mapper')
142
+ setup_queues
143
+ start_console if @options[:console] && !@options[:daemonize]
144
+ end
145
+
146
+ # Make a nanite request which expects a response.
147
+ #
148
+ # ==== Parameters
149
+ # type<String>:: The dispatch route for the request
150
+ # payload<Object>:: Payload to send. This will get marshalled en route
151
+ #
152
+ # ==== Options
153
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
154
+ # :least_loaded:: Pick the nanite which has the lowest load.
155
+ # :all:: Send the request to all nanites which respond to the service.
156
+ # :random:: Randomly pick a nanite.
157
+ # :rr: Select a nanite according to round robin ordering.
158
+ # :target<String>:: Select a specific nanite via identity, rather than using
159
+ # a selector.
160
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
161
+ # the nanites are offline. Messages will be redelivered when nanites come online.
162
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
163
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
164
+ # storage so that it isnt lost when the broker is restarted.
165
+ # Default is false unless the mapper was started with the --persistent flag.
166
+ # :intermediate_handler:: Takes a lambda to call when an IntermediateMessage
167
+ # event arrives from a nanite. If passed a Hash, hash keys should correspond to
168
+ # the IntermediateMessage keys provided by the nanite, and each should have a value
169
+ # that is a lambda/proc taking the parameters specified here. Can supply a key '*'
170
+ # as a catch-all for unmatched keys.
171
+ #
172
+ # ==== Block Parameters for intermediate_handler
173
+ # key<String>:: array of unique keys for which intermediate state has been received
174
+ # since the last call to this block.
175
+ # nanite<String>:: nanite which sent the message.
176
+ # state:: most recently delivered intermediate state for the key provided.
177
+ # job:: (optional) -- if provided, this parameter gets the whole job object, if there's
178
+ # a reason to do more complex work with the job.
179
+ #
180
+ # ==== Block Parameters
181
+ # :results<Object>:: The returned value from the nanite actor.
182
+ #
183
+ # @api :public:
184
+ def request(type, payload = '', opts = {}, &blk)
185
+ request = build_deliverable(Request, type, payload, opts)
186
+ send_request(request, opts, &blk)
187
+ end
188
+
189
+ # Send request with pre-built request instance
190
+ def send_request(request, opts = {}, &blk)
191
+ request.reply_to = identity
192
+ intm_handler = opts.delete(:intermediate_handler)
193
+ targets = cluster.targets_for(request)
194
+ if !targets.empty?
195
+ job = job_warden.new_job(request, targets, intm_handler, blk)
196
+ cluster.route(request, job.targets)
197
+ job
198
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
199
+ job_warden.new_job(request, [], intm_handler, blk)
200
+ cluster.publish(request, 'mapper-offline')
201
+ :offline
202
+ else
203
+ false
204
+ end
205
+ end
206
+
207
+ # Make a nanite request which does not expect a response.
208
+ #
209
+ # ==== Parameters
210
+ # type<String>:: The dispatch route for the request
211
+ # payload<Object>:: Payload to send. This will get marshalled en route
212
+ #
213
+ # ==== Options
214
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
215
+ # :least_loaded:: Pick the nanite which has the lowest load.
216
+ # :all:: Send the request to all nanites which respond to the service.
217
+ # :random:: Randomly pick a nanite.
218
+ # :rr: Select a nanite according to round robin ordering.
219
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
220
+ # the nanites are offline. Messages will be redelivered when nanites come online.
221
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
222
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
223
+ # storage so that it isnt lost when the broker is restarted.
224
+ # Default is false unless the mapper was started with the --persistent flag.
225
+ #
226
+ # @api :public:
227
+ def push(type, payload = '', opts = {})
228
+ push = build_deliverable(Push, type, payload, opts)
229
+ send_push(push, opts)
230
+ end
231
+
232
+ def send_push(push, opts = {})
233
+ targets = cluster.targets_for(push)
234
+ if !targets.empty?
235
+ cluster.route(push, targets)
236
+ true
237
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
238
+ cluster.publish(push, 'mapper-offline')
239
+ :offline
240
+ else
241
+ false
242
+ end
243
+ end
244
+
245
+ private
246
+
247
+ def build_deliverable(deliverable_type, type, payload, opts)
248
+ deliverable = deliverable_type.new(type, payload, nil, opts)
249
+ deliverable.from = identity
250
+ deliverable.token = Identity.generate
251
+ deliverable.persistent = opts.key?(:persistent) ? opts[:persistent] : options[:persistent]
252
+ deliverable
253
+ end
254
+
255
+ def setup_queues
256
+ if amq.respond_to?(:prefetch) && @options.has_key?(:prefetch)
257
+ amq.prefetch(@options[:prefetch])
258
+ end
259
+
260
+ setup_offline_queue
261
+ setup_message_queue
262
+ end
263
+
264
+ def setup_offline_queue
265
+ offline_queue = amq.queue('mapper-offline', :durable => true)
266
+ offline_queue.subscribe(:ack => true) do |info, deliverable|
267
+ deliverable = serializer.load(deliverable)
268
+ targets = cluster.targets_for(deliverable)
269
+ unless targets.empty?
270
+ info.ack
271
+ if deliverable.kind_of?(Request)
272
+ if job = job_warden.jobs[deliverable.token]
273
+ job.targets = targets
274
+ else
275
+ deliverable.reply_to = identity
276
+ job_warden.new_job(deliverable, targets)
277
+ end
278
+ end
279
+ cluster.route(deliverable, targets)
280
+ end
281
+ end
282
+
283
+ EM.add_periodic_timer(options[:offline_redelivery_frequency]) { offline_queue.recover }
284
+ end
285
+
286
+ def setup_message_queue
287
+ amq.queue(identity, :exclusive => true).bind(amq.fanout(identity)).subscribe do |msg|
288
+ begin
289
+ msg = serializer.load(msg)
290
+ Nanite::Log.debug("RECV #{msg.to_s}")
291
+ Nanite::Log.info("RECV #{msg.to_s([:from])}") unless Nanite::Log.level == :debug
292
+ job_warden.process(msg)
293
+ rescue Exception => e
294
+ Nanite::Log.error("RECV [result] #{e.message}")
295
+ end
296
+ end
297
+ end
298
+
299
+ def setup_logging
300
+ Nanite::Log.init(@identity, @options[:log_path])
301
+ Nanite::Log.level = @options[:log_level] if @options[:log_level]
302
+ end
303
+
304
+ def setup_cluster
305
+ @cluster = Cluster.new(@amq, @options[:agent_timeout], @options[:identity], @serializer, self, @options[:redis], @options[:callbacks])
306
+ end
307
+ end
308
+ end
309
+