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