rightscale-nanite 0.4.1 → 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 (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,47 @@
1
+ class MQ
2
+ class Queue
3
+ # Asks the broker to redeliver all unacknowledged messages on a
4
+ # specifieid channel. Zero or more messages may be redelivered.
5
+ #
6
+ # * requeue (default false)
7
+ # If this parameter is false, the message will be redelivered to the original recipient.
8
+ # If this flag is true, the server will attempt to requeue the message, potentially then
9
+ # delivering it to an alternative subscriber.
10
+ #
11
+ def recover requeue = false
12
+ @mq.callback{
13
+ @mq.send Protocol::Basic::Recover.new({ :requeue => requeue })
14
+ }
15
+ self
16
+ end
17
+ end
18
+ end
19
+
20
+ # monkey patch to the amqp gem that adds :no_declare => true option for new
21
+ # Exchange objects. This allows us to send messeages to exchanges that are
22
+ # declared by the mappers and that we have no configuration priviledges on.
23
+ # temporary until we get this into amqp proper
24
+ MQ::Exchange.class_eval do
25
+ def initialize mq, type, name, opts = {}
26
+ @mq = mq
27
+ @type, @name, @opts = type, name, opts
28
+ @mq.exchanges[@name = name] ||= self
29
+ @key = opts[:key]
30
+
31
+ @mq.callback{
32
+ @mq.send AMQP::Protocol::Exchange::Declare.new({ :exchange => name,
33
+ :type => type,
34
+ :nowait => true }.merge(opts))
35
+ } unless name == "amq.#{type}" or name == '' or opts[:no_declare]
36
+ end
37
+ end
38
+
39
+ module Nanite
40
+ module AMQPHelper
41
+ def start_amqp(options)
42
+ connection = AMQP.connect(:user => options[:user], :pass => options[:pass], :vhost => options[:vhost],
43
+ :host => options[:host], :port => (options[:port] || ::AMQP::PORT).to_i, :insist => options[:insist] || false)
44
+ MQ.new(connection)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,203 @@
1
+ module Nanite
2
+ class Cluster
3
+ attr_reader :agent_timeout, :nanites, :reaper, :serializer, :identity, :amq, :redis, :mapper
4
+
5
+ def initialize(amq, agent_timeout, identity, serializer, mapper, redis=nil)
6
+ @amq = amq
7
+ @agent_timeout = agent_timeout
8
+ @identity = identity
9
+ @serializer = serializer
10
+ @mapper = mapper
11
+ @redis = redis
12
+ @security = SecurityProvider.get
13
+ if redis
14
+ Nanite::Log.info("using redis for state storage")
15
+ require 'nanite/state'
16
+ @nanites = ::Nanite::State.new(redis)
17
+ else
18
+ require 'nanite/local_state'
19
+ @nanites = Nanite::LocalState.new
20
+ end
21
+ @reaper = Reaper.new(agent_timeout)
22
+ setup_queues
23
+ end
24
+
25
+ # determine which nanites should receive the given request
26
+ def targets_for(request)
27
+ return [request.target] if request.target
28
+ __send__(request.selector, request.type, request.tags).collect {|name, state| name }
29
+ end
30
+
31
+ # adds nanite to nanites map: key is nanite's identity
32
+ # and value is a services/status pair implemented
33
+ # as a hash
34
+ def register(reg)
35
+ case reg
36
+ when Register
37
+ if @security.authorize_registration(reg)
38
+ nanites[reg.identity] = { :services => reg.services, :status => reg.status, :tags => reg.tags }
39
+ reaper.timeout(reg.identity, agent_timeout + 1) { nanites.delete(reg.identity) }
40
+ Nanite::Log.info("registered: #{reg.identity}, #{nanites[reg.identity].inspect}")
41
+ else
42
+ Nanite::Log.warning("registration of #{reg.inspect} not authorized")
43
+ end
44
+ when UnRegister
45
+ nanites.delete(reg.identity)
46
+ Nanite::Log.info("un-registering: #{reg.identity}")
47
+ end
48
+ end
49
+
50
+ def route(request, targets)
51
+ EM.next_tick { targets.map { |target| publish(request, target) } }
52
+ end
53
+
54
+ def publish(request, target)
55
+ # We need to initialize the 'target' field of the request object so that the serializer has
56
+ # access to it.
57
+ begin
58
+ old_target = request.target
59
+ request.target = target unless target == 'mapper-offline'
60
+ amq.queue(target).publish(serializer.dump(request), :persistent => request.persistent)
61
+ ensure
62
+ request.target = old_target
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ # updates nanite information (last ping timestamps, status)
69
+ # when heartbeat message is received
70
+ def handle_ping(ping)
71
+ if nanite = nanites[ping.identity]
72
+ nanite[:status] = ping.status
73
+ reaper.reset_with_autoregister_hack(ping.identity, agent_timeout + 1) { nanites.delete(ping.identity) }
74
+ else
75
+ amq.queue(ping.identity).publish(serializer.dump(Advertise.new))
76
+ end
77
+ end
78
+
79
+ # forward request coming from agent
80
+ def handle_request(request)
81
+ if @security.authorize_request(request)
82
+ result = Result.new(request.token, request.from, nil, mapper.identity)
83
+ intm_handler = lambda do |res|
84
+ result.results = res
85
+ forward_response(result, request.persistent)
86
+ end
87
+ ok = mapper.send_request(request, :intermediate_handler => intm_handler) do |res|
88
+ result.results = res
89
+ forward_response(result, request.persistent)
90
+ end
91
+ if ok == false
92
+ forward_response(result, request.persistent)
93
+ end
94
+ else
95
+ Nanite::Log.warning("request #{request.inspect} not authorized")
96
+ end
97
+ end
98
+
99
+ # forward response back to agent that originally made the request
100
+ def forward_response(res, persistent)
101
+ amq.queue(res.to).publish(serializer.dump(res), :persistent => persistent)
102
+ end
103
+
104
+ # returns least loaded nanite that provides given service
105
+ def least_loaded(service, tags=[])
106
+ candidates = nanites_providing(service,tags)
107
+ return [] if candidates.empty?
108
+
109
+ [candidates.min { |a,b| a[1][:status] <=> b[1][:status] }]
110
+ end
111
+
112
+ # returns all nanites that provide given service
113
+ def all(service, tags=[])
114
+ nanites_providing(service,tags)
115
+ end
116
+
117
+ # returns a random nanite
118
+ def random(service, tags=[])
119
+ candidates = nanites_providing(service,tags)
120
+ return [] if candidates.empty?
121
+
122
+ [candidates[rand(candidates.size)]]
123
+ end
124
+
125
+ # selects next nanite that provides given service
126
+ # using round robin rotation
127
+ def rr(service, tags=[])
128
+ @last ||= {}
129
+ @last[service] ||= 0
130
+ candidates = nanites_providing(service,tags)
131
+ return [] if candidates.empty?
132
+ @last[service] = 0 if @last[service] >= candidates.size
133
+ candidate = candidates[@last[service]]
134
+ @last[service] += 1
135
+ [candidate]
136
+ end
137
+
138
+ # returns all nanites that provide the given service
139
+ def nanites_providing(service, *tags)
140
+ nanites.nanites_for(service, *tags)
141
+ end
142
+
143
+ def setup_queues
144
+ setup_heartbeat_queue
145
+ setup_registration_queue
146
+ setup_request_queue
147
+ end
148
+
149
+ def setup_heartbeat_queue
150
+ handler = lambda do |ping|
151
+ begin
152
+ ping = serializer.load(ping)
153
+ Nanite::Log.debug("got heartbeat from #{ping.identity}") if ping.respond_to?(:identity)
154
+ handle_ping(ping)
155
+ rescue Exception => e
156
+ Nanite::Log.error("Error handling heartbeat: #{e.message}")
157
+ end
158
+ end
159
+ hb_fanout = amq.fanout('heartbeat', :durable => true)
160
+ if @redis
161
+ amq.queue("heartbeat").bind(hb_fanout).subscribe &handler
162
+ else
163
+ amq.queue("heartbeat-#{identity}", :exclusive => true).bind(hb_fanout).subscribe &handler
164
+ end
165
+ end
166
+
167
+ def setup_registration_queue
168
+ handler = lambda do |msg|
169
+ begin
170
+ msg = serializer.load(msg)
171
+ Nanite::Log.debug("got registration from #{msg.identity}")
172
+ register(msg)
173
+ rescue Exception => e
174
+ Nanite::Log.error("Error handling registration: #{e.message}")
175
+ end
176
+ end
177
+ reg_fanout = amq.fanout('registration', :durable => true)
178
+ if @redis
179
+ amq.queue("registration").bind(reg_fanout).subscribe &handler
180
+ else
181
+ amq.queue("registration-#{identity}", :exclusive => true).bind(reg_fanout).subscribe &handler
182
+ end
183
+ end
184
+
185
+ def setup_request_queue
186
+ handler = lambda do |msg|
187
+ begin
188
+ msg = serializer.load(msg)
189
+ Nanite::Log.debug("got request from #{msg.from} of type #{msg.type}")
190
+ handle_request(msg)
191
+ rescue Exception => e
192
+ Nanite::Log.error("Error handling request: #{e.message}")
193
+ end
194
+ end
195
+ req_fanout = amq.fanout('request', :durable => true)
196
+ if @redis
197
+ amq.queue("request").bind(req_fanout).subscribe &handler
198
+ else
199
+ amq.queue("request-#{identity}", :exclusive => true).bind(req_fanout).subscribe &handler
200
+ end
201
+ end
202
+ end
203
+ end
@@ -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