shift-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 +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