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,67 @@
1
+ module Nanite
2
+
3
+ # This class allows sending requests to nanite agents without having
4
+ # to run a local mapper.
5
+ # It is used by Actor.request which can be used by actors than need
6
+ # to send requests to remote agents.
7
+ # All requests go through the mapper for security purposes.
8
+ class MapperProxy
9
+
10
+ $:.push File.dirname(__FILE__)
11
+ require 'amqp'
12
+
13
+ include AMQPHelper
14
+
15
+ attr_accessor :pending_requests, :identity, :options, :amqp, :serializer
16
+
17
+ # Accessor for actor
18
+ def self.instance
19
+ @@instance if defined?(@@instance)
20
+ end
21
+
22
+ def initialize(id, opts)
23
+ @options = opts || {}
24
+ @identity = id
25
+ @pending_requests = {}
26
+ @amqp = start_amqp(options)
27
+ @serializer = Serializer.new(options[:format])
28
+ @@instance = self
29
+ end
30
+
31
+ # Send request to given agent through the mapper
32
+ def request(type, payload = '', opts = {}, &blk)
33
+ raise "Mapper proxy not initialized" unless identity && options
34
+ request = Request.new(type, payload, nil, opts)
35
+ request.from = identity
36
+ request.token = Identity.generate
37
+ request.persistent = opts.key?(:persistent) ? opts[:persistent] : options[:persistent]
38
+ pending_requests[request.token] =
39
+ { :intermediate_handler => opts[:intermediate_handler], :result_handler => blk }
40
+ Nanite::Log.info("SEND #{request.to_s([:tags, :target])}")
41
+ amqp.fanout('request', :no_declare => options[:secure]).publish(serializer.dump(request))
42
+ end
43
+
44
+ def push(type, payload = '', opts = {})
45
+ raise "Mapper proxy not initialized" unless identity && options
46
+ push = Push.new(type, payload, nil, opts)
47
+ push.from = identity
48
+ push.token = Identity.generate
49
+ push.persistent = opts.key?(:persistent) ? opts[:persistent] : options[:persistent]
50
+ Nanite::Log.info("SEND #{push.to_s([:tags, :target])}")
51
+ amqp.fanout('request', :no_declare => options[:secure]).publish(serializer.dump(push))
52
+ end
53
+
54
+ # Handle intermediary result
55
+ def handle_intermediate_result(res)
56
+ handlers = pending_requests[res.token]
57
+ handlers[:intermediate_handler].call(res) if handlers && handlers[:intermediate_handler]
58
+ end
59
+
60
+ # Handle final result
61
+ def handle_result(res)
62
+ handlers = pending_requests.delete(res.token)
63
+ handlers[:result_handler].call(res) if handlers && handlers[:result_handler]
64
+ end
65
+
66
+ end
67
+ 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
@@ -0,0 +1,365 @@
1
+ module Nanite
2
+ # Base class for all Nanite packets,
3
+ # knows how to dump itself to JSON
4
+ class Packet
5
+
6
+ attr_accessor :size
7
+
8
+ def initialize
9
+ raise NotImplementedError.new("#{self.class.name} is an abstract class.")
10
+ end
11
+
12
+ def to_json(*a)
13
+ js = {
14
+ 'json_class' => self.class.name,
15
+ 'data' => instance_variables.inject({}) {|m,ivar| m[ivar.to_s.sub(/@/,'')] = instance_variable_get(ivar); m }
16
+ }.to_json(*a)
17
+ js = js.chop + ",\"size\":#{js.size}}"
18
+ js
19
+ end
20
+
21
+ # Log representation
22
+ def to_s(filter=nil)
23
+ res = "[#{ self.class.to_s.split('::').last.
24
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
25
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
26
+ downcase }]"
27
+ res += " (#{size.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")} bytes)" if size && !size.to_s.empty?
28
+ res
29
+ end
30
+
31
+ # Log friendly name for given agent id
32
+ def id_to_s(id)
33
+ case id
34
+ when /^mapper-/ then 'mapper'
35
+ when /^nanite-(.*)/ then Regexp.last_match(1)
36
+ else id
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ # packet that means start of a file transfer
43
+ # operation
44
+ class FileStart < Packet
45
+
46
+ attr_accessor :filename, :token, :dest
47
+
48
+ def initialize(filename, dest, token, size=nil)
49
+ @filename = filename
50
+ @dest = dest
51
+ @token = token
52
+ @size = size
53
+ end
54
+
55
+ def self.json_create(o)
56
+ i = o['data']
57
+ new(i['filename'], i['dest'], i['token'], o['size'])
58
+ end
59
+
60
+ def to_s
61
+ "#{super} <#{token}> #{filename} to #{dest}"
62
+ end
63
+ end
64
+
65
+ # packet that means end of a file transfer
66
+ # operation
67
+ class FileEnd < Packet
68
+
69
+ attr_accessor :token, :meta
70
+
71
+ def initialize(token, meta, size=nil)
72
+ @token = token
73
+ @meta = meta
74
+ @size = size
75
+ end
76
+
77
+ def self.json_create(o)
78
+ i = o['data']
79
+ new(i['token'], i['meta'], o['size'])
80
+ end
81
+
82
+ def to_s
83
+ "#{super} <#{token}> meta #{meta}"
84
+ end
85
+ end
86
+
87
+ # packet that carries data chunks during a file transfer
88
+ class FileChunk < Packet
89
+
90
+ attr_accessor :chunk, :token
91
+
92
+ def initialize(token, size=nil, chunk=nil)
93
+ @chunk = chunk
94
+ @token = token
95
+ @size = size
96
+ end
97
+
98
+ def self.json_create(o)
99
+ i = o['data']
100
+ new(i['token'], o['size'], i['chunk'])
101
+ end
102
+
103
+ def to_s
104
+ "#{super} <#{token}>"
105
+ end
106
+ end
107
+
108
+ # packet that means a work request from mapper
109
+ # to actor node
110
+ #
111
+ # type is a service name
112
+ # payload is arbitrary data that is transferred from mapper to actor
113
+ #
114
+ # Options:
115
+ # from is sender identity
116
+ # token is a generated request id that mapper uses to identify replies
117
+ # reply_to is identity of the node actor replies to, usually a mapper itself
118
+ # selector is the selector used to route the request
119
+ # target is the target nanite for the request
120
+ # persistent signifies if this request should be saved to persistent storage by the AMQP broker
121
+ class Request < Packet
122
+
123
+ attr_accessor :from, :payload, :type, :token, :reply_to, :selector, :target, :persistent, :tags
124
+
125
+ DEFAULT_OPTIONS = {:selector => :least_loaded}
126
+
127
+ def initialize(type, payload, size=nil, opts={})
128
+ opts = DEFAULT_OPTIONS.merge(opts)
129
+ @type = type
130
+ @payload = payload
131
+ @size = size
132
+ @from = opts[:from]
133
+ @token = opts[:token]
134
+ @reply_to = opts[:reply_to]
135
+ @selector = opts[:selector]
136
+ @target = opts[:target]
137
+ @persistent = opts[:persistent]
138
+ @tags = opts[:tags] || []
139
+ end
140
+
141
+ def self.json_create(o)
142
+ i = o['data']
143
+ new(i['type'], i['payload'], o['size'], { :from => i['from'], :token => i['token'],
144
+ :reply_to => i['reply_to'], :selector => i['selector'],
145
+ :target => i['target'], :persistent => i['persistent'],
146
+ :tags => i['tags'] })
147
+ end
148
+
149
+ def to_s(filter=nil)
150
+ log_msg = "#{super} <#{token}> #{type}"
151
+ log_msg += " from #{id_to_s(from)}" if filter.nil? || filter.include?(:from)
152
+ log_msg += " to #{id_to_s(target)}" if target && (filter.nil? || filter.include?(:target))
153
+ log_msg += ", reply_to #{id_to_s(reply_to)}" if reply_to && (filter.nil? || filter.include?(:reply_to))
154
+ log_msg += ", tags #{tags.inspect}" if tags && !tags.empty? && (filter.nil? || filter.include?(:tags))
155
+ log_msg += ", payload #{payload.inspect}" if filter.nil? || filter.include?(:payload)
156
+ log_msg
157
+ end
158
+
159
+ end
160
+
161
+ # packet that means a work push from mapper
162
+ # to actor node
163
+ #
164
+ # type is a service name
165
+ # payload is arbitrary data that is transferred from mapper to actor
166
+ #
167
+ # Options:
168
+ # from is sender identity
169
+ # token is a generated request id that mapper uses to identify replies
170
+ # selector is the selector used to route the request
171
+ # target is the target nanite for the request
172
+ # persistent signifies if this request should be saved to persistent storage by the AMQP broker
173
+ class Push < Packet
174
+
175
+ attr_accessor :from, :payload, :type, :token, :selector, :target, :persistent, :tags
176
+
177
+ DEFAULT_OPTIONS = {:selector => :least_loaded}
178
+
179
+ def initialize(type, payload, size=nil, opts={})
180
+ opts = DEFAULT_OPTIONS.merge(opts)
181
+ @type = type
182
+ @payload = payload
183
+ @size = size
184
+ @from = opts[:from]
185
+ @token = opts[:token]
186
+ @selector = opts[:selector]
187
+ @target = opts[:target]
188
+ @persistent = opts[:persistent]
189
+ @tags = opts[:tags] || []
190
+ end
191
+
192
+ def self.json_create(o)
193
+ i = o['data']
194
+ new(i['type'], i['payload'], o['size'], { :from => i['from'], :token => i['token'],
195
+ :selector => i['selector'], :target => i['target'],
196
+ :persistent => i['persistent'], :tags => i['tags'] })
197
+ end
198
+
199
+ def to_s(filter=nil)
200
+ log_msg = "#{super} <#{token}> #{type}"
201
+ log_msg += " from #{id_to_s(from)}" if filter.nil? || filter.include?(:from)
202
+ log_msg += ", target #{id_to_s(target)}" if target && (filter.nil? || filter.include?(:target))
203
+ log_msg += ", tags #{tags.inspect}" if tags && !tags.empty? && (filter.nil? || filter.include?(:tags))
204
+ log_msg += ", payload #{payload.inspect}" if filter.nil? || filter.include?(:payload)
205
+ log_msg
206
+ end
207
+ end
208
+
209
+ # packet that means a work result notification sent from actor to mapper
210
+ #
211
+ # from is sender identity
212
+ # results is arbitrary data that is transferred from actor, a result of actor's work
213
+ # token is a generated request id that mapper uses to identify replies
214
+ # to is identity of the node result should be delivered to
215
+ class Result < Packet
216
+
217
+ attr_accessor :token, :results, :to, :from
218
+
219
+ def initialize(token, to, results, from, size=nil)
220
+ @token = token
221
+ @to = to
222
+ @from = from
223
+ @results = results
224
+ @size = size
225
+ end
226
+
227
+ def self.json_create(o)
228
+ i = o['data']
229
+ new(i['token'], i['to'], i['results'], i['from'], o['size'])
230
+ end
231
+
232
+ def to_s(filter=nil)
233
+ log_msg = "#{super} <#{token}>"
234
+ log_msg += " from #{id_to_s(from)}" if filter.nil? || filter.include?(:from)
235
+ log_msg += " to #{id_to_s(to)}" if filter.nil? || filter.include?(:to)
236
+ log_msg += " results: #{results.inspect}" if filter.nil? || filter.include?(:results)
237
+ log_msg
238
+ end
239
+ end
240
+
241
+ # packet that means an intermediate status notification sent from actor to mapper. is appended to a list of messages matching messagekey.
242
+ #
243
+ # from is sender identity
244
+ # messagekey is a string that can become part of a redis key, which identifies the name under which the message is stored
245
+ # message is arbitrary data that is transferred from actor, an intermediate result of actor's work
246
+ # token is a generated request id that mapper uses to identify replies
247
+ # to is identity of the node result should be delivered to
248
+ class IntermediateMessage < Packet
249
+
250
+ attr_accessor :token, :messagekey, :message, :to, :from
251
+
252
+ def initialize(token, to, from, messagekey, message, size=nil)
253
+ @token = token
254
+ @to = to
255
+ @from = from
256
+ @messagekey = messagekey
257
+ @message = message
258
+ @size = size
259
+ end
260
+
261
+ def self.json_create(o)
262
+ i = o['data']
263
+ new(i['token'], i['to'], i['from'], i['messagekey'], i['message'], o['size'])
264
+ end
265
+
266
+ def to_s
267
+ "#{super} <#{token}> from #{id_to_s(from)}, key #{messagekey}"
268
+ end
269
+ end
270
+
271
+ # packet that means an availability notification sent from actor to mapper
272
+ #
273
+ # from is sender identity
274
+ # services is a list of services provided by the node
275
+ # status is a load of the node by default, but may be any criteria
276
+ # agent may use to report it's availability, load, etc
277
+ class Register < Packet
278
+
279
+ attr_accessor :identity, :services, :status, :tags
280
+
281
+ def initialize(identity, services, status, tags, size=nil)
282
+ @status = status
283
+ @tags = tags
284
+ @identity = identity
285
+ @services = services
286
+ @size = size
287
+ end
288
+
289
+ def self.json_create(o)
290
+ i = o['data']
291
+ new(i['identity'], i['services'], i['status'], i['tags'], o['size'])
292
+ end
293
+
294
+ def to_s
295
+ log_msg = "#{super} #{id_to_s(identity)}"
296
+ log_msg += ", services: #{services.join(', ')}" if services && !services.empty?
297
+ log_msg += ", tags: #{tags.join(', ')}" if tags && !tags.empty?
298
+ log_msg
299
+ end
300
+ end
301
+
302
+ # packet that means deregister an agent from the mappers
303
+ #
304
+ # from is sender identity
305
+ class UnRegister < Packet
306
+
307
+ attr_accessor :identity
308
+
309
+ def initialize(identity, size=nil)
310
+ @identity = identity
311
+ @size = size
312
+ end
313
+
314
+ def self.json_create(o)
315
+ i = o['data']
316
+ new(i['identity'], o['size'])
317
+ end
318
+
319
+ def to_s
320
+ "#{super} #{id_to_s(identity)}"
321
+ end
322
+ end
323
+
324
+ # heartbeat packet
325
+ #
326
+ # identity is sender's identity
327
+ # status is sender's status (see Register packet documentation)
328
+ class Ping < Packet
329
+
330
+ attr_accessor :identity, :status
331
+
332
+ def initialize(identity, status, size=nil)
333
+ @status = status
334
+ @identity = identity
335
+ @size = size
336
+ end
337
+
338
+ def self.json_create(o)
339
+ i = o['data']
340
+ new(i['identity'], i['status'], o['size'])
341
+ end
342
+
343
+ def to_s
344
+ "#{super} #{id_to_s(identity)} status #{status}"
345
+ end
346
+
347
+ end
348
+
349
+ # packet that is sent by workers to the mapper
350
+ # when worker initially comes online to advertise
351
+ # it's services
352
+ class Advertise < Packet
353
+
354
+ def initialize(size=nil)
355
+ @size = size
356
+ end
357
+
358
+ def self.json_create(o)
359
+ new(o['size'])
360
+ end
361
+
362
+ end
363
+
364
+ end
365
+