nofxx-nanite 0.4.1.2

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 (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 +258 -0
  13. data/lib/nanite/amqp.rb +54 -0
  14. data/lib/nanite/cluster.rb +236 -0
  15. data/lib/nanite/config.rb +111 -0
  16. data/lib/nanite/console.rb +39 -0
  17. data/lib/nanite/daemonize.rb +13 -0
  18. data/lib/nanite/dispatcher.rb +92 -0
  19. data/lib/nanite/identity.rb +16 -0
  20. data/lib/nanite/job.rb +104 -0
  21. data/lib/nanite/local_state.rb +34 -0
  22. data/lib/nanite/log.rb +66 -0
  23. data/lib/nanite/log/formatter.rb +39 -0
  24. data/lib/nanite/mapper.rb +310 -0
  25. data/lib/nanite/mapper_proxy.rb +67 -0
  26. data/lib/nanite/packets.rb +365 -0
  27. data/lib/nanite/pid_file.rb +52 -0
  28. data/lib/nanite/reaper.rb +38 -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 +164 -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 +485 -0
  50. data/spec/dispatcher_spec.rb +136 -0
  51. data/spec/distinguished_name_spec.rb +24 -0
  52. data/spec/encrypted_document_spec.rb +21 -0
  53. data/spec/job_spec.rb +251 -0
  54. data/spec/local_state_spec.rb +112 -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 +131 -0
@@ -0,0 +1,236 @@
1
+ module Nanite
2
+ class Cluster
3
+ attr_reader :agent_timeout, :nanites, :reaper, :serializer, :identity, :amq, :redis, :mapper, :callbacks
4
+
5
+ def initialize(amq, agent_timeout, identity, serializer, mapper, state_configuration=nil, callbacks = {})
6
+ @amq = amq
7
+ @agent_timeout = agent_timeout
8
+ @identity = identity
9
+ @serializer = serializer
10
+ @mapper = mapper
11
+ @state = state_configuration
12
+ @security = SecurityProvider.get
13
+ @callbacks = callbacks
14
+ setup_state
15
+ @reaper = Reaper.new(agent_timeout)
16
+ setup_queues
17
+ end
18
+
19
+ # determine which nanites should receive the given request
20
+ def targets_for(request)
21
+ return [request.target] if request.target
22
+ __send__(request.selector, request.type, request.tags).collect {|name, state| name }
23
+ end
24
+
25
+ # adds nanite to nanites map: key is nanite's identity
26
+ # and value is a services/status pair implemented
27
+ # as a hash
28
+ def register(reg)
29
+ case reg
30
+ when Register
31
+ if @security.authorize_registration(reg)
32
+ Nanite::Log.info("RECV #{reg.to_s}")
33
+ nanites[reg.identity] = { :services => reg.services, :status => reg.status, :tags => reg.tags }
34
+ reaper.timeout(reg.identity, agent_timeout + 1) { nanite_timed_out(reg.identity) }
35
+ callbacks[:register].call(reg.identity, mapper) if callbacks[:register]
36
+ else
37
+ Nanite::Log.warn("RECV NOT AUTHORIZED #{reg.to_s}")
38
+ end
39
+ when UnRegister
40
+ Nanite::Log.info("RECV #{reg.to_s}")
41
+ nanites.delete(reg.identity)
42
+ callbacks[:unregister].call(reg.identity, mapper) if callbacks[:unregister]
43
+ else
44
+ Nanite::Log.warn("RECV [register] Invalid packet type: #{reg.class}")
45
+ end
46
+ end
47
+
48
+ def nanite_timed_out(token)
49
+ nanite = nanites.delete(token)
50
+ callbacks[:timeout].call(token, mapper) if callbacks[:timeout]
51
+ end
52
+
53
+ def route(request, targets)
54
+ EM.next_tick { targets.map { |target| publish(request, target) } }
55
+ end
56
+
57
+ def publish(request, target)
58
+ # We need to initialize the 'target' field of the request object so that the serializer has
59
+ # access to it.
60
+ begin
61
+ old_target = request.target
62
+ request.target = target unless target == 'mapper-offline'
63
+ Nanite::Log.info("SEND #{request.to_s([:from, :tags, :target])}")
64
+ amq.queue(target).publish(serializer.dump(request), :persistent => request.persistent)
65
+ ensure
66
+ request.target = old_target
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ # updates nanite information (last ping timestamps, status)
73
+ # when heartbeat message is received
74
+ def handle_ping(ping)
75
+ begin
76
+ if nanite = nanites[ping.identity]
77
+ nanite[:status] = ping.status
78
+ reaper.reset_with_autoregister_hack(ping.identity, agent_timeout + 1) { nanite_timed_out(ping.identity) }
79
+ else
80
+ packet = Advertise.new
81
+ Nanite::Log.info("SEND #{packet.to_s} to #{ping.identity}")
82
+ amq.queue(ping.identity).publish(serializer.dump(packet))
83
+ end
84
+ end
85
+ end
86
+
87
+ # forward request coming from agent
88
+ def handle_request(request)
89
+ if @security.authorize_request(request)
90
+ Nanite::Log.info("RECV #{request.to_s([:from, :target, :tags])}") unless Nanite::Log.level == :debug
91
+ Nanite::Log.debug("RECV #{request.to_s}")
92
+ case request
93
+ when Push
94
+ mapper.send_push(request)
95
+ else
96
+ intm_handler = lambda do |result, job|
97
+ result = IntermediateMessage.new(request.token, job.request.from, mapper.identity, nil, result)
98
+ forward_response(result, request.persistent)
99
+ end
100
+
101
+ result = Result.new(request.token, request.from, nil, mapper.identity)
102
+ ok = mapper.send_request(request, :intermediate_handler => intm_handler) do |res|
103
+ result.results = res
104
+ forward_response(result, request.persistent)
105
+ end
106
+
107
+ if ok == false
108
+ forward_response(result, request.persistent)
109
+ end
110
+ end
111
+ else
112
+ Nanite::Log.warn("RECV NOT AUTHORIZED #{request.to_s}")
113
+ end
114
+ end
115
+
116
+ # forward response back to agent that originally made the request
117
+ def forward_response(res, persistent)
118
+ Nanite::Log.info("SEND #{res.to_s([:to])}")
119
+ amq.queue(res.to).publish(serializer.dump(res), :persistent => persistent)
120
+ end
121
+
122
+ # returns least loaded nanite that provides given service
123
+ def least_loaded(service, tags=[])
124
+ candidates = nanites_providing(service,tags)
125
+ return [] if candidates.empty?
126
+
127
+ [candidates.min { |a,b| a[1][:status] <=> b[1][:status] }]
128
+ end
129
+
130
+ # returns all nanites that provide given service
131
+ def all(service, tags=[])
132
+ nanites_providing(service,tags)
133
+ end
134
+
135
+ # returns a random nanite
136
+ def random(service, tags=[])
137
+ candidates = nanites_providing(service,tags)
138
+ return [] if candidates.empty?
139
+
140
+ [candidates[rand(candidates.size)]]
141
+ end
142
+
143
+ # selects next nanite that provides given service
144
+ # using round robin rotation
145
+ def rr(service, tags=[])
146
+ @last ||= {}
147
+ @last[service] ||= 0
148
+ candidates = nanites_providing(service,tags)
149
+ return [] if candidates.empty?
150
+ @last[service] = 0 if @last[service] >= candidates.size
151
+ candidate = candidates[@last[service]]
152
+ @last[service] += 1
153
+ [candidate]
154
+ end
155
+
156
+ # returns all nanites that provide the given service
157
+ def nanites_providing(service, *tags)
158
+ nanites.nanites_for(service, *tags)
159
+ end
160
+
161
+ def setup_queues
162
+ setup_heartbeat_queue
163
+ setup_registration_queue
164
+ setup_request_queue
165
+ end
166
+
167
+ def setup_heartbeat_queue
168
+ handler = lambda do |ping|
169
+ begin
170
+ ping = serializer.load(ping)
171
+ Nanite::Log.debug("RECV #{ping.to_s}") if ping.respond_to?(:to_s)
172
+ handle_ping(ping)
173
+ rescue Exception => e
174
+ Nanite::Log.error("RECV [ping] #{e.message}")
175
+ end
176
+ end
177
+ hb_fanout = amq.fanout('heartbeat', :durable => true)
178
+ if shared_state?
179
+ amq.queue("heartbeat").bind(hb_fanout).subscribe &handler
180
+ else
181
+ amq.queue("heartbeat-#{identity}", :exclusive => true).bind(hb_fanout).subscribe &handler
182
+ end
183
+ end
184
+
185
+ def setup_registration_queue
186
+ handler = lambda do |msg|
187
+ begin
188
+ register(serializer.load(msg))
189
+ rescue Exception => e
190
+ Nanite::Log.error("RECV [register] #{e.message}")
191
+ end
192
+ end
193
+ reg_fanout = amq.fanout('registration', :durable => true)
194
+ if shared_state?
195
+ amq.queue("registration").bind(reg_fanout).subscribe &handler
196
+ else
197
+ amq.queue("registration-#{identity}", :exclusive => true).bind(reg_fanout).subscribe &handler
198
+ end
199
+ end
200
+
201
+ def setup_request_queue
202
+ handler = lambda do |msg|
203
+ begin
204
+ handle_request(serializer.load(msg))
205
+ rescue Exception => e
206
+ Nanite::Log.error("RECV [request] #{e.message}")
207
+ end
208
+ end
209
+ req_fanout = amq.fanout('request', :durable => true)
210
+ if shared_state? || mapper.options[:requests_failsafe]
211
+ amq.queue("request").bind(req_fanout).subscribe &handler
212
+ else
213
+ amq.queue("request-#{identity}", :exclusive => true).bind(req_fanout).subscribe &handler
214
+ end
215
+ end
216
+
217
+ def setup_state
218
+ case @state
219
+ when String
220
+ # backwards compatibility, we assume redis if the configuration option
221
+ # was a string
222
+ Nanite::Log.info("[setup] using redis for state storage")
223
+ require 'nanite/state'
224
+ @nanites = Nanite::State.new(@state)
225
+ when Hash
226
+ else
227
+ require 'nanite/local_state'
228
+ @nanites = Nanite::LocalState.new
229
+ end
230
+ end
231
+
232
+ def shared_state?
233
+ !@state.nil?
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,111 @@
1
+ module Nanite
2
+
3
+ COMMON_DEFAULT_OPTIONS = {
4
+ :pass => 'testing',
5
+ :vhost => '/nanite',
6
+ :secure => false,
7
+ :host => '0.0.0.0',
8
+ :log_level => :info,
9
+ :format => :marshal,
10
+ :daemonize => false,
11
+ :console => false,
12
+ :root => Dir.pwd
13
+ }
14
+
15
+ module CommonConfig
16
+ def setup_mapper_options(opts, options)
17
+ setup_common_options(opts, options, 'mapper')
18
+
19
+ 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|
20
+ options[:agent_timeout] = timeout
21
+ end
22
+
23
+ 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|
24
+ options[:offline_redelivery_frequency] = frequency
25
+ end
26
+
27
+ 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
28
+ options[:persistent] = true
29
+ end
30
+
31
+ 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
32
+ options[:offline_failsafe] = true
33
+ end
34
+
35
+ 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|
36
+ redishost, redisport = redis.split(':')
37
+ redishost = '127.0.0.1' if (redishost.nil? || redishost.empty?)
38
+ redisport = '6379' if (redishost.nil? || redishost.empty?)
39
+ redis = "#{redishost}:#{redisport}"
40
+ options[:redis] = redis
41
+ end
42
+
43
+ end
44
+
45
+ def setup_common_options(opts, options, type)
46
+ opts.version = Nanite::VERSION
47
+
48
+ opts.on("-i", "--irb-console", "Start #{type} in irb console mode.") do |console|
49
+ options[:console] = 'irb'
50
+ end
51
+
52
+ opts.on("-u", "--user USER", "Specify the rabbitmq username.") do |user|
53
+ options[:user] = user
54
+ end
55
+
56
+ opts.on("-h", "--host HOST", "Specify the rabbitmq hostname.") do |host|
57
+ options[:host] = host
58
+ end
59
+
60
+ opts.on("-P", "--port PORT", "Specify the rabbitmq PORT, default 5672.") do |port|
61
+ options[:port] = port
62
+ end
63
+
64
+ opts.on("-p", "--pass PASSWORD", "Specify the rabbitmq password") do |pass|
65
+ options[:pass] = pass
66
+ end
67
+
68
+ opts.on("-t", "--token IDENITY", "Specify the #{type} identity.") do |ident|
69
+ options[:identity] = ident
70
+ end
71
+
72
+ opts.on("-v", "--vhost VHOST", "Specify the rabbitmq vhost") do |vhost|
73
+ options[:vhost] = vhost
74
+ end
75
+
76
+ opts.on("-s", "--secure", "Use Security features of rabbitmq to restrict nanites to themselves") do
77
+ options[:secure] = true
78
+ end
79
+
80
+ opts.on("-f", "--format FORMAT", "The serialization type to use for transfering data. Can be marshal, json or yaml. Default is marshal") do |fmt|
81
+ options[:format] = fmt.to_sym
82
+ end
83
+
84
+ opts.on("-d", "--daemonize", "Run #{type} as a daemon") do |d|
85
+ options[:daemonize] = true
86
+ end
87
+
88
+ opts.on("--pid-dir PATH", "Specify the pid path, only used with daemonize") do |dir|
89
+ options[:pid_dir] = dir
90
+ end
91
+
92
+ opts.on("-l", "--log-level LEVEL", "Specify the log level (fatal, error, warn, info, debug). Default is info") do |level|
93
+ options[:log_level] = level
94
+ end
95
+
96
+ opts.on("--log-dir PATH", "Specify the log path") do |dir|
97
+ options[:log_dir] = dir
98
+ end
99
+
100
+ opts.on("--tag TAG", "Specify a tag. Can issue multiple times.") do |tag|
101
+ options[:tag] ||= []
102
+ options[:tag] << tag
103
+ end
104
+
105
+ opts.on("--version", "Show the nanite version number") do |res|
106
+ puts "Nanite Version #{opts.version}"
107
+ exit
108
+ end
109
+ end
110
+ end
111
+ 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,92 @@
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
+ @evmclass.threadpool_size = (@options[:threadpool_size] || 20).to_i
14
+ end
15
+
16
+ def dispatch(deliverable)
17
+ prefix, meth = deliverable.type.split('/')[1..-1]
18
+ meth ||= :index
19
+ actor = registry.actor_for(prefix)
20
+
21
+ operation = lambda do
22
+ begin
23
+ intermediate_results_proc = lambda { |*args| self.handle_intermediate_results(actor, meth, deliverable, *args) }
24
+ args = [ deliverable.payload ]
25
+ args.push(deliverable) if actor.method(meth).arity == 2
26
+ actor.send(meth, *args, &intermediate_results_proc)
27
+ rescue Exception => e
28
+ handle_exception(actor, meth, deliverable, e)
29
+ end
30
+ end
31
+
32
+ callback = lambda do |r|
33
+ if deliverable.kind_of?(Request)
34
+ r = Result.new(deliverable.token, deliverable.reply_to, r, identity)
35
+ Nanite::Log.info("SEND #{r.to_s([])}")
36
+ amq.queue(deliverable.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
37
+ end
38
+ r # For unit tests
39
+ end
40
+
41
+ if @options[:single_threaded] || @options[:thread_poolsize] == 1
42
+ @evmclass.next_tick { callback.call(operation.call) }
43
+ else
44
+ @evmclass.defer(operation, callback)
45
+ end
46
+ end
47
+
48
+ protected
49
+
50
+ def handle_intermediate_results(actor, meth, deliverable, *args)
51
+ messagekey = case args.size
52
+ when 1
53
+ 'defaultkey'
54
+ when 2
55
+ args.first.to_s
56
+ else
57
+ raise ArgumentError, "handle_intermediate_results passed unexpected number of arguments (#{args.size})"
58
+ end
59
+ message = args.last
60
+ @evmclass.defer(lambda {
61
+ [deliverable.reply_to, IntermediateMessage.new(deliverable.token, deliverable.reply_to, identity, messagekey, message)]
62
+ }, lambda { |r|
63
+ amq.queue(r.first, :no_declare => options[:secure]).publish(serializer.dump(r.last))
64
+ })
65
+ end
66
+
67
+ private
68
+
69
+ def describe_error(e)
70
+ "#{e.class.name}: #{e.message}\n #{e.backtrace.join("\n ")}"
71
+ end
72
+
73
+ def handle_exception(actor, meth, deliverable, e)
74
+ error = describe_error(e)
75
+ Nanite::Log.error(error)
76
+ begin
77
+ if actor.class.exception_callback
78
+ case actor.class.exception_callback
79
+ when Symbol, String
80
+ actor.send(actor.class.exception_callback, meth.to_sym, deliverable, e)
81
+ when Proc
82
+ actor.instance_exec(meth.to_sym, deliverable, e, &actor.class.exception_callback)
83
+ end
84
+ end
85
+ rescue Exception => e1
86
+ error = describe_error(e1)
87
+ Nanite::Log.error(error)
88
+ end
89
+ error
90
+ end
91
+ end
92
+ end