ezmobius-nanite 0.4.0 → 0.4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. data/README.rdoc +70 -20
  2. data/Rakefile +1 -1
  3. data/bin/nanite-agent +34 -8
  4. data/bin/nanite-mapper +18 -8
  5. data/lib/nanite.rb +71 -0
  6. data/lib/nanite/actor.rb +60 -0
  7. data/lib/nanite/actor_registry.rb +24 -0
  8. data/lib/nanite/admin.rb +138 -0
  9. data/lib/nanite/agent.rb +250 -0
  10. data/lib/nanite/amqp.rb +47 -0
  11. data/lib/nanite/cluster.rb +203 -0
  12. data/lib/nanite/config.rb +102 -0
  13. data/lib/nanite/console.rb +39 -0
  14. data/lib/nanite/daemonize.rb +13 -0
  15. data/lib/nanite/dispatcher.rb +90 -0
  16. data/lib/nanite/identity.rb +16 -0
  17. data/lib/nanite/job.rb +104 -0
  18. data/lib/nanite/local_state.rb +34 -0
  19. data/lib/nanite/log.rb +64 -0
  20. data/lib/nanite/log/formatter.rb +39 -0
  21. data/lib/nanite/mapper.rb +277 -0
  22. data/lib/nanite/mapper_proxy.rb +56 -0
  23. data/lib/nanite/packets.rb +231 -0
  24. data/lib/nanite/pid_file.rb +52 -0
  25. data/lib/nanite/reaper.rb +38 -0
  26. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  27. data/lib/nanite/security/certificate.rb +55 -0
  28. data/lib/nanite/security/certificate_cache.rb +66 -0
  29. data/lib/nanite/security/distinguished_name.rb +34 -0
  30. data/lib/nanite/security/encrypted_document.rb +46 -0
  31. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  32. data/lib/nanite/security/secure_serializer.rb +67 -0
  33. data/lib/nanite/security/signature.rb +40 -0
  34. data/lib/nanite/security/static_certificate_store.rb +35 -0
  35. data/lib/nanite/security_provider.rb +47 -0
  36. data/lib/nanite/serializer.rb +52 -0
  37. data/lib/nanite/state.rb +164 -0
  38. data/lib/nanite/streaming.rb +125 -0
  39. data/lib/nanite/util.rb +51 -0
  40. data/spec/actor_registry_spec.rb +62 -0
  41. data/spec/actor_spec.rb +59 -0
  42. data/spec/agent_spec.rb +235 -0
  43. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  44. data/spec/certificate_cache_spec.rb +49 -0
  45. data/spec/certificate_spec.rb +27 -0
  46. data/spec/cluster_spec.rb +300 -0
  47. data/spec/dispatcher_spec.rb +136 -0
  48. data/spec/distinguished_name_spec.rb +24 -0
  49. data/spec/encrypted_document_spec.rb +21 -0
  50. data/spec/job_spec.rb +219 -0
  51. data/spec/local_state_spec.rb +112 -0
  52. data/spec/packet_spec.rb +218 -0
  53. data/spec/rsa_key_pair_spec.rb +33 -0
  54. data/spec/secure_serializer_spec.rb +41 -0
  55. data/spec/serializer_spec.rb +107 -0
  56. data/spec/signature_spec.rb +30 -0
  57. data/spec/spec_helper.rb +23 -0
  58. data/spec/static_certificate_store_spec.rb +30 -0
  59. data/spec/util_spec.rb +63 -0
  60. metadata +63 -2
@@ -0,0 +1,102 @@
1
+ module Nanite
2
+
3
+ COMMON_DEFAULT_OPTIONS = {:pass => 'testing', :vhost => '/nanite', :secure => false, :host => '0.0.0.0',
4
+ :log_level => :info, :format => :marshal, :daemonize => false, :console => false, :root => Dir.pwd}
5
+
6
+ module CommonConfig
7
+ def setup_mapper_options(opts, options)
8
+ setup_common_options(opts, options, 'mapper')
9
+
10
+ opts.on("-a", "--agent-timeout TIMEOUT", "How long to wait before an agent is considered to be offline and thus removed from the list of available agents.") do |timeout|
11
+ options[:agent_timeout] = timeout
12
+ end
13
+
14
+ opts.on("-r", "--offline-redelivery-frequency", "The frequency in seconds that messages stored in the offline queue will be retrieved for attempted redelivery to the nanites. Default is 10 seconds.") do |frequency|
15
+ options[:offline_redelivery_frequency] = frequency
16
+ end
17
+
18
+ opts.on("--persistent", "Instructs the AMQP broker to save messages to persistent storage so that they aren't lost when the broker is restarted. Can be overriden on a per-message basis using the request and push methods.") do
19
+ options[:persistent] = true
20
+ end
21
+
22
+ opts.on("--offline-failsafe", "Store messages in an offline queue when all the nanites are offline. Messages will be redelivered when nanites come online. Can be overriden on a per-message basis using the request methods.") do
23
+ options[:offline_failsafe] = true
24
+ end
25
+
26
+ opts.on("--redis HOST_PORT", "Use redis as the agent state storage in the mapper: --redis 127.0.0.1:6379; missing host and/or port will be filled with defaults if colon is present") do |redis|
27
+ redishost, redisport = redis.split(':')
28
+ redishost = '127.0.0.1' if (redishost.nil? || redishost.empty?)
29
+ redisport = '6379' if (redishost.nil? || redishost.empty?)
30
+ redis = "#{redishost}:#{redisport}"
31
+ options[:redis] = redis
32
+ end
33
+
34
+ end
35
+
36
+ def setup_common_options(opts, options, type)
37
+ opts.version = Nanite::VERSION
38
+
39
+ opts.on("-i", "--irb-console", "Start #{type} in irb console mode.") do |console|
40
+ options[:console] = 'irb'
41
+ end
42
+
43
+ opts.on("-u", "--user USER", "Specify the rabbitmq username.") do |user|
44
+ options[:user] = user
45
+ end
46
+
47
+ opts.on("-h", "--host HOST", "Specify the rabbitmq hostname.") do |host|
48
+ options[:host] = host
49
+ end
50
+
51
+ opts.on("-P", "--port PORT", "Specify the rabbitmq PORT, default 5672.") do |port|
52
+ options[:port] = port
53
+ end
54
+
55
+ opts.on("-p", "--pass PASSWORD", "Specify the rabbitmq password") do |pass|
56
+ options[:pass] = pass
57
+ end
58
+
59
+ opts.on("-t", "--token IDENITY", "Specify the #{type} identity.") do |ident|
60
+ options[:identity] = ident
61
+ end
62
+
63
+ opts.on("-v", "--vhost VHOST", "Specify the rabbitmq vhost") do |vhost|
64
+ options[:vhost] = vhost
65
+ end
66
+
67
+ opts.on("-s", "--secure", "Use Security features of rabbitmq to restrict nanites to themselves") do
68
+ options[:secure] = true
69
+ end
70
+
71
+ opts.on("-f", "--format FORMAT", "The serialization type to use for transfering data. Can be marshal, json or yaml. Default is marshal") do |fmt|
72
+ options[:format] = fmt.to_sym
73
+ end
74
+
75
+ opts.on("-d", "--daemonize", "Run #{type} as a daemon") do |d|
76
+ options[:daemonize] = true
77
+ end
78
+
79
+ opts.on("--pid-dir PATH", "Specify the pid path, only used with daemonize") do |dir|
80
+ options[:pid_dir] = dir
81
+ end
82
+
83
+ opts.on("-l", "--log-level LEVEL", "Specify the log level (fatal, error, warn, info, debug). Default is info") do |level|
84
+ options[:log_level] = level
85
+ end
86
+
87
+ opts.on("--log-dir PATH", "Specify the log path") do |dir|
88
+ options[:log_dir] = dir
89
+ end
90
+
91
+ opts.on("--tag TAG", "Specify a tag. Can issue multiple times.") do |tag|
92
+ options[:tag] ||= []
93
+ options[:tag] << tag
94
+ end
95
+
96
+ opts.on("--version", "Show the nanite version number") do |res|
97
+ puts "Nanite Version #{opts.version}"
98
+ exit
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,39 @@
1
+ module Nanite
2
+ module ConsoleHelper
3
+ def self.included(base)
4
+ @@base = base
5
+ end
6
+
7
+ def start_console
8
+ puts "Starting #{@@base.name.split(":").last.downcase} console (#{self.identity}) (Nanite #{Nanite::VERSION})"
9
+ Thread.new do
10
+ Console.start(self)
11
+ end
12
+ end
13
+ end
14
+
15
+ module Console
16
+ class << self; attr_accessor :instance; end
17
+
18
+ def self.start(binding)
19
+ require 'irb'
20
+ old_args = ARGV.dup
21
+ ARGV.replace ["--simple-prompt"]
22
+
23
+ IRB.setup(nil)
24
+ self.instance = IRB::Irb.new(IRB::WorkSpace.new(binding))
25
+
26
+ @CONF = IRB.instance_variable_get(:@CONF)
27
+ @CONF[:IRB_RC].call self.instance.context if @CONF[:IRB_RC]
28
+ @CONF[:MAIN_CONTEXT] = self.instance.context
29
+
30
+ catch(:IRB_EXIT) { self.instance.eval_input }
31
+ ensure
32
+ ARGV.replace old_args
33
+ # Clean up tty settings in some evil, evil cases
34
+ begin; catch(:IRB_EXIT) { irb_exit }; rescue Exception; end
35
+ # Make nanite exit when irb does
36
+ EM.stop if EM.reactor_running?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module Nanite
2
+ module DaemonizeHelper
3
+ def daemonize
4
+ exit if fork
5
+ Process.setsid
6
+ exit if fork
7
+ File.umask 0000
8
+ STDIN.reopen "/dev/null"
9
+ STDOUT.reopen "/dev/null", "a"
10
+ STDERR.reopen STDOUT
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,90 @@
1
+ module Nanite
2
+ class Dispatcher
3
+ attr_reader :registry, :serializer, :identity, :amq, :options
4
+ attr_accessor :evmclass
5
+
6
+ def initialize(amq, registry, serializer, identity, options)
7
+ @amq = amq
8
+ @registry = registry
9
+ @serializer = serializer
10
+ @identity = identity
11
+ @options = options
12
+ @evmclass = EM
13
+ end
14
+
15
+ def dispatch(deliverable)
16
+ prefix, meth = deliverable.type.split('/')[1..-1]
17
+ meth ||= :index
18
+ actor = registry.actor_for(prefix)
19
+
20
+ operation = lambda do
21
+ begin
22
+ intermediate_results_proc = lambda { |*args| self.handle_intermediate_results(actor, meth, deliverable, *args) }
23
+ args = [ deliverable.payload ]
24
+ args.push(deliverable) if actor.method(meth).arity == 2
25
+ actor.send(meth, *args, &intermediate_results_proc)
26
+ rescue Exception => e
27
+ handle_exception(actor, meth, deliverable, e)
28
+ end
29
+ end
30
+
31
+ callback = lambda do |r|
32
+ if deliverable.kind_of?(Request)
33
+ r = Result.new(deliverable.token, deliverable.reply_to, r, identity)
34
+ amq.queue(deliverable.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
35
+ end
36
+ r # For unit tests
37
+ end
38
+
39
+ if @options[:single_threaded]
40
+ @evmclass.next_tick { callback.call(operation.call) }
41
+ else
42
+ @evmclass.defer(operation, callback)
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ def handle_intermediate_results(actor, meth, deliverable, *args)
49
+ messagekey = case args.size
50
+ when 1
51
+ 'defaultkey'
52
+ when 2
53
+ args.first.to_s
54
+ else
55
+ raise ArgumentError, "handle_intermediate_results passed unexpected number of arguments (#{args.size})"
56
+ end
57
+ message = args.last
58
+ @evmclass.defer(lambda {
59
+ [deliverable.reply_to, IntermediateMessage.new(deliverable.token, deliverable.reply_to, identity, messagekey, message)]
60
+ }, lambda { |r|
61
+ amq.queue(r.first, :no_declare => options[:secure]).publish(serializer.dump(r.last))
62
+ })
63
+ end
64
+
65
+ private
66
+
67
+ def describe_error(e)
68
+ "#{e.class.name}: #{e.message}\n #{e.backtrace.join("\n ")}"
69
+ end
70
+
71
+ def handle_exception(actor, meth, deliverable, e)
72
+ error = describe_error(e)
73
+ Nanite::Log.error(error)
74
+ begin
75
+ if actor.class.exception_callback
76
+ case actor.class.exception_callback
77
+ when Symbol, String
78
+ actor.send(actor.class.exception_callback, meth.to_sym, deliverable, e)
79
+ when Proc
80
+ actor.instance_exec(meth.to_sym, deliverable, e, &actor.class.exception_callback)
81
+ end
82
+ end
83
+ rescue Exception => e1
84
+ error = describe_error(e1)
85
+ Nanite::Log.error(error)
86
+ end
87
+ error
88
+ end
89
+ end
90
+ end
@@ -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
data/lib/nanite/job.rb ADDED
@@ -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
data/lib/nanite/log.rb ADDED
@@ -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