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,250 @@
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, :timestamp => Time.now.utc.to_i }
34
+ reaper.register(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
+ reaper.unregister(reg.identity)
42
+ nanites.delete(reg.identity)
43
+ callbacks[:unregister].call(reg.identity, mapper) if callbacks[:unregister]
44
+ else
45
+ Nanite::Log.warn("RECV [register] Invalid packet type: #{reg.class}")
46
+ end
47
+ end
48
+
49
+ def nanite_timed_out(token)
50
+ nanite = nanites[token]
51
+ if nanite && timed_out?(nanite)
52
+ Nanite::Log.info("Nanite #{token} timed out")
53
+ nanite = nanites.delete(token)
54
+ callbacks[:timeout].call(token, mapper) if callbacks[:timeout]
55
+ true
56
+ end
57
+ end
58
+
59
+ def route(request, targets)
60
+ EM.next_tick { targets.map { |target| publish(request, target) } }
61
+ end
62
+
63
+ def publish(request, target)
64
+ # We need to initialize the 'target' field of the request object so that the serializer has
65
+ # access to it.
66
+ begin
67
+ old_target = request.target
68
+ request.target = target unless target == 'mapper-offline'
69
+ Nanite::Log.info("SEND #{request.to_s([:from, :tags, :target])}")
70
+ amq.queue(target).publish(serializer.dump(request), :persistent => request.persistent)
71
+ ensure
72
+ request.target = old_target
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ # updates nanite information (last ping timestamps, status)
79
+ # when heartbeat message is received
80
+ def handle_ping(ping)
81
+ begin
82
+ if nanite = nanites[ping.identity]
83
+ nanites.update_status(ping.identity, ping.status)
84
+ reaper.update(ping.identity, agent_timeout + 1) { nanite_timed_out(ping.identity) }
85
+ else
86
+ packet = Advertise.new
87
+ Nanite::Log.info("SEND #{packet.to_s} to #{ping.identity}")
88
+ amq.queue(ping.identity).publish(serializer.dump(packet))
89
+ end
90
+ end
91
+ end
92
+
93
+ # forward request coming from agent
94
+ def handle_request(request)
95
+ if @security.authorize_request(request)
96
+ Nanite::Log.info("RECV #{request.to_s([:from, :target, :tags])}") unless Nanite::Log.level == :debug
97
+ Nanite::Log.debug("RECV #{request.to_s}")
98
+ case request
99
+ when Push
100
+ mapper.send_push(request)
101
+ else
102
+ intm_handler = lambda do |result, job|
103
+ result = IntermediateMessage.new(request.token, job.request.from, mapper.identity, nil, result)
104
+ forward_response(result, request.persistent)
105
+ end
106
+
107
+ result = Result.new(request.token, request.from, nil, mapper.identity)
108
+ ok = mapper.send_request(request, :intermediate_handler => intm_handler) do |res|
109
+ result.results = res
110
+ forward_response(result, request.persistent)
111
+ end
112
+
113
+ if ok == false
114
+ forward_response(result, request.persistent)
115
+ end
116
+ end
117
+ else
118
+ Nanite::Log.warn("RECV NOT AUTHORIZED #{request.to_s}")
119
+ end
120
+ end
121
+
122
+ # forward response back to agent that originally made the request
123
+ def forward_response(res, persistent)
124
+ Nanite::Log.info("SEND #{res.to_s([:to])}")
125
+ amq.queue(res.to).publish(serializer.dump(res), :persistent => persistent)
126
+ end
127
+
128
+ # returns least loaded nanite that provides given service
129
+ def least_loaded(service, tags=[])
130
+ candidates = nanites_providing(service,tags)
131
+ return [] if candidates.empty?
132
+
133
+ [candidates.min { |a,b| a[1][:status] <=> b[1][:status] }]
134
+ end
135
+
136
+ # returns all nanites that provide given service
137
+ def all(service, tags=[])
138
+ nanites_providing(service,tags)
139
+ end
140
+
141
+ # returns a random nanite
142
+ def random(service, tags=[])
143
+ candidates = nanites_providing(service,tags)
144
+ return [] if candidates.empty?
145
+
146
+ [candidates[rand(candidates.size)]]
147
+ end
148
+
149
+ # selects next nanite that provides given service
150
+ # using round robin rotation
151
+ def rr(service, tags=[])
152
+ @last ||= {}
153
+ @last[service] ||= 0
154
+ candidates = nanites_providing(service,tags)
155
+ return [] if candidates.empty?
156
+ @last[service] = 0 if @last[service] >= candidates.size
157
+ candidate = candidates[@last[service]]
158
+ @last[service] += 1
159
+ [candidate]
160
+ end
161
+
162
+ def timed_out?(nanite)
163
+ nanite[:timestamp].to_i < (Time.now.utc - agent_timeout).to_i
164
+ end
165
+
166
+ # returns all nanites that provide the given service
167
+ def nanites_providing(service, *tags)
168
+ nanites.nanites_for(service, *tags).delete_if do |nanite|
169
+ if timed_out?(nanite[1])
170
+ Nanite::Log.debug("Ignoring timed out nanite #{nanite[0]} in target selection - last seen at #{nanite[1][:timestamp]}")
171
+ end
172
+ end
173
+ end
174
+
175
+ def setup_queues
176
+ setup_heartbeat_queue
177
+ setup_registration_queue
178
+ setup_request_queue
179
+ end
180
+
181
+ def setup_heartbeat_queue
182
+ handler = lambda do |ping|
183
+ begin
184
+ ping = serializer.load(ping)
185
+ Nanite::Log.debug("RECV #{ping.to_s}") if ping.respond_to?(:to_s)
186
+ handle_ping(ping)
187
+ rescue Exception => e
188
+ Nanite::Log.error("RECV [ping] #{e.message}")
189
+ end
190
+ end
191
+ hb_fanout = amq.fanout('heartbeat', :durable => true)
192
+ if shared_state?
193
+ amq.queue("heartbeat").bind(hb_fanout).subscribe &handler
194
+ else
195
+ amq.queue("heartbeat-#{identity}", :exclusive => true).bind(hb_fanout).subscribe &handler
196
+ end
197
+ end
198
+
199
+ def setup_registration_queue
200
+ handler = lambda do |msg|
201
+ begin
202
+ register(serializer.load(msg))
203
+ rescue Exception => e
204
+ Nanite::Log.error("RECV [register] #{e.message}")
205
+ end
206
+ end
207
+ reg_fanout = amq.fanout('registration', :durable => true)
208
+ if shared_state?
209
+ amq.queue("registration").bind(reg_fanout).subscribe &handler
210
+ else
211
+ amq.queue("registration-#{identity}", :exclusive => true).bind(reg_fanout).subscribe &handler
212
+ end
213
+ end
214
+
215
+ def setup_request_queue
216
+ handler = lambda do |msg|
217
+ begin
218
+ handle_request(serializer.load(msg))
219
+ rescue Exception => e
220
+ Nanite::Log.error("RECV [request] #{e.message}")
221
+ end
222
+ end
223
+ req_fanout = amq.fanout('request', :durable => true)
224
+ if shared_state?
225
+ amq.queue("request").bind(req_fanout).subscribe &handler
226
+ else
227
+ amq.queue("request-#{identity}", :exclusive => true).bind(req_fanout).subscribe &handler
228
+ end
229
+ end
230
+
231
+ def setup_state
232
+ case @state
233
+ when String
234
+ # backwards compatibility, we assume redis if the configuration option
235
+ # was a string
236
+ Nanite::Log.info("[setup] using redis for state storage")
237
+ require 'nanite/state'
238
+ @nanites = Nanite::State.new(@state)
239
+ when Hash
240
+ else
241
+ require 'nanite/local_state'
242
+ @nanites = Nanite::LocalState.new
243
+ end
244
+ end
245
+
246
+ def shared_state?
247
+ !@state.nil?
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,112 @@
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
+ :insist => true
14
+ }
15
+
16
+ module CommonConfig
17
+ def setup_mapper_options(opts, options)
18
+ setup_common_options(opts, options, 'mapper')
19
+
20
+ 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|
21
+ options[:agent_timeout] = timeout
22
+ end
23
+
24
+ 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|
25
+ options[:offline_redelivery_frequency] = frequency
26
+ end
27
+
28
+ 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
29
+ options[:persistent] = true
30
+ end
31
+
32
+ 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
33
+ options[:offline_failsafe] = true
34
+ end
35
+
36
+ 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|
37
+ redishost, redisport = redis.split(':')
38
+ redishost = '127.0.0.1' if (redishost.nil? || redishost.empty?)
39
+ redisport = '6379' if (redishost.nil? || redishost.empty?)
40
+ redis = "#{redishost}:#{redisport}"
41
+ options[:redis] = redis
42
+ end
43
+
44
+ end
45
+
46
+ def setup_common_options(opts, options, type)
47
+ opts.version = Nanite::VERSION
48
+
49
+ opts.on("-i", "--irb-console", "Start #{type} in irb console mode.") do |console|
50
+ options[:console] = 'irb'
51
+ end
52
+
53
+ opts.on("-u", "--user USER", "Specify the rabbitmq username.") do |user|
54
+ options[:user] = user
55
+ end
56
+
57
+ opts.on("-h", "--host HOST", "Specify the rabbitmq hostname.") do |host|
58
+ options[:host] = host
59
+ end
60
+
61
+ opts.on("-P", "--port PORT", "Specify the rabbitmq PORT, default 5672.") do |port|
62
+ options[:port] = port
63
+ end
64
+
65
+ opts.on("-p", "--pass PASSWORD", "Specify the rabbitmq password") do |pass|
66
+ options[:pass] = pass
67
+ end
68
+
69
+ opts.on("-t", "--token IDENITY", "Specify the #{type} identity.") do |ident|
70
+ options[:identity] = ident
71
+ end
72
+
73
+ opts.on("-v", "--vhost VHOST", "Specify the rabbitmq vhost") do |vhost|
74
+ options[:vhost] = vhost
75
+ end
76
+
77
+ opts.on("-s", "--secure", "Use Security features of rabbitmq to restrict nanites to themselves") do
78
+ options[:secure] = true
79
+ end
80
+
81
+ opts.on("-f", "--format FORMAT", "The serialization type to use for transfering data. Can be marshal, json or yaml. Default is marshal") do |fmt|
82
+ options[:format] = fmt.to_sym
83
+ end
84
+
85
+ opts.on("-d", "--daemonize", "Run #{type} as a daemon") do |d|
86
+ options[:daemonize] = true
87
+ end
88
+
89
+ opts.on("--pid-dir PATH", "Specify the pid path, only used with daemonize") do |dir|
90
+ options[:pid_dir] = dir
91
+ end
92
+
93
+ opts.on("-l", "--log-level LEVEL", "Specify the log level (fatal, error, warn, info, debug). Default is info") do |level|
94
+ options[:log_level] = level
95
+ end
96
+
97
+ opts.on("--log-dir PATH", "Specify the log path") do |dir|
98
+ options[:log_dir] = dir
99
+ end
100
+
101
+ opts.on("--tag TAG", "Specify a tag. Can issue multiple times.") do |tag|
102
+ options[:tag] ||= []
103
+ options[:tag] << tag
104
+ end
105
+
106
+ opts.on("--version", "Show the nanite version number") do |res|
107
+ puts "Nanite Version #{opts.version}"
108
+ exit
109
+ end
110
+ end
111
+ end
112
+ 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(identity, options = {})
4
+ exit if fork
5
+ Process.setsid
6
+ exit if fork
7
+ STDIN.reopen "/dev/null"
8
+ STDOUT.reopen "#{options[:log_path]}/nanite.#{identity}.out", "a"
9
+ STDERR.reopen "#{options[:log_path]}/nanite.#{identity}.err", "a"
10
+ File.umask 0000
11
+ end
12
+ end
13
+ 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
@@ -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
+ if job = jobs[msg.token]
18
+ job.process(msg)
19
+
20
+ if job.intermediate_handler && (job.pending_keys.size > 0)
21
+
22
+ unless job.pending_keys.size == 1
23
+ raise "IntermediateMessages are currently dispatched as they arrive, shouldn't have more than one key in pending_keys: #{job.pending_keys.inspect}"
24
+ end
25
+
26
+ key = job.pending_keys.first
27
+ handler = job.intermediate_handler_for_key(key)
28
+ if handler
29
+ case handler.arity
30
+ when 2
31
+ handler.call(job.intermediate_state[msg.from][key].last, job)
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