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,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