ezmobius-nanite 0.4.0 → 0.4.1.1

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 (60) hide show
  1. data/README.rdoc +70 -20
  2. data/Rakefile +1 -1
  3. data/bin/nanite-agent +34 -8
  4. data/bin/nanite-mapper +18 -8
  5. data/lib/nanite.rb +71 -0
  6. data/lib/nanite/actor.rb +60 -0
  7. data/lib/nanite/actor_registry.rb +24 -0
  8. data/lib/nanite/admin.rb +138 -0
  9. data/lib/nanite/agent.rb +250 -0
  10. data/lib/nanite/amqp.rb +47 -0
  11. data/lib/nanite/cluster.rb +203 -0
  12. data/lib/nanite/config.rb +102 -0
  13. data/lib/nanite/console.rb +39 -0
  14. data/lib/nanite/daemonize.rb +13 -0
  15. data/lib/nanite/dispatcher.rb +90 -0
  16. data/lib/nanite/identity.rb +16 -0
  17. data/lib/nanite/job.rb +104 -0
  18. data/lib/nanite/local_state.rb +34 -0
  19. data/lib/nanite/log.rb +64 -0
  20. data/lib/nanite/log/formatter.rb +39 -0
  21. data/lib/nanite/mapper.rb +277 -0
  22. data/lib/nanite/mapper_proxy.rb +56 -0
  23. data/lib/nanite/packets.rb +231 -0
  24. data/lib/nanite/pid_file.rb +52 -0
  25. data/lib/nanite/reaper.rb +38 -0
  26. data/lib/nanite/security/cached_certificate_store_proxy.rb +24 -0
  27. data/lib/nanite/security/certificate.rb +55 -0
  28. data/lib/nanite/security/certificate_cache.rb +66 -0
  29. data/lib/nanite/security/distinguished_name.rb +34 -0
  30. data/lib/nanite/security/encrypted_document.rb +46 -0
  31. data/lib/nanite/security/rsa_key_pair.rb +53 -0
  32. data/lib/nanite/security/secure_serializer.rb +67 -0
  33. data/lib/nanite/security/signature.rb +40 -0
  34. data/lib/nanite/security/static_certificate_store.rb +35 -0
  35. data/lib/nanite/security_provider.rb +47 -0
  36. data/lib/nanite/serializer.rb +52 -0
  37. data/lib/nanite/state.rb +164 -0
  38. data/lib/nanite/streaming.rb +125 -0
  39. data/lib/nanite/util.rb +51 -0
  40. data/spec/actor_registry_spec.rb +62 -0
  41. data/spec/actor_spec.rb +59 -0
  42. data/spec/agent_spec.rb +235 -0
  43. data/spec/cached_certificate_store_proxy_spec.rb +34 -0
  44. data/spec/certificate_cache_spec.rb +49 -0
  45. data/spec/certificate_spec.rb +27 -0
  46. data/spec/cluster_spec.rb +300 -0
  47. data/spec/dispatcher_spec.rb +136 -0
  48. data/spec/distinguished_name_spec.rb +24 -0
  49. data/spec/encrypted_document_spec.rb +21 -0
  50. data/spec/job_spec.rb +219 -0
  51. data/spec/local_state_spec.rb +112 -0
  52. data/spec/packet_spec.rb +218 -0
  53. data/spec/rsa_key_pair_spec.rb +33 -0
  54. data/spec/secure_serializer_spec.rb +41 -0
  55. data/spec/serializer_spec.rb +107 -0
  56. data/spec/signature_spec.rb +30 -0
  57. data/spec/spec_helper.rb +23 -0
  58. data/spec/static_certificate_store_spec.rb +30 -0
  59. data/spec/util_spec.rb +63 -0
  60. metadata +63 -2
@@ -0,0 +1,250 @@
1
+ module Nanite
2
+ class Agent
3
+ include AMQPHelper
4
+ include FileStreaming
5
+ include ConsoleHelper
6
+ include DaemonizeHelper
7
+
8
+ attr_reader :identity, :options, :serializer, :dispatcher, :registry, :amq, :tags
9
+ attr_accessor :status_proc
10
+
11
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({:user => 'nanite', :ping_time => 15,
12
+ :default_services => []}) unless defined?(DEFAULT_OPTIONS)
13
+
14
+ # Initializes a new agent and establishes AMQP connection.
15
+ # This must be used inside EM.run block or if EventMachine reactor
16
+ # is already started, for instance, by a Thin server that your Merb/Rails
17
+ # application runs on.
18
+ #
19
+ # Agent options:
20
+ #
21
+ # identity : identity of this agent, may be any string
22
+ #
23
+ # status_proc : a callable object that returns agent load as a string,
24
+ # defaults to load averages string extracted from `uptime`
25
+ # format : format to use for packets serialization. One of the three:
26
+ # :marshall, :json, or :yaml. Defaults to
27
+ # Ruby's Marshall format. For interoperability with
28
+ # AMQP clients implemented in other languages, use JSON.
29
+ #
30
+ # Note that Nanite uses JSON gem,
31
+ # and ActiveSupport's JSON encoder may cause clashes
32
+ # if ActiveSupport is loaded after JSON gem.
33
+ #
34
+ # root : application root for this agent, defaults to Dir.pwd
35
+ #
36
+ # log_dir : path to directory where agent stores it's log file
37
+ # if not given, app_root is used.
38
+ #
39
+ # file_root : path to directory to files this agent provides
40
+ # defaults to app_root/files
41
+ #
42
+ # ping_time : time interval in seconds between two subsequent heartbeat messages
43
+ # this agent broadcasts. Default value is 15.
44
+ #
45
+ # console : true tells Nanite to start interactive console
46
+ #
47
+ # daemonize : true tells Nanite to daemonize
48
+ #
49
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
50
+ # defaults to the root or the current working directory.
51
+ #
52
+ # services : list of services provided by this agent, by default
53
+ # all methods exposed by actors are listed
54
+ #
55
+ # single_threaded: Run all operations in one thread
56
+ #
57
+ # Connection options:
58
+ #
59
+ # vhost : AMQP broker vhost that should be used
60
+ #
61
+ # user : AMQP broker user
62
+ #
63
+ # pass : AMQP broker password
64
+ #
65
+ # host : host AMQP broker (or node of interest) runs on,
66
+ # defaults to 0.0.0.0
67
+ #
68
+ # port : port AMQP broker (or node of interest) runs on,
69
+ # this defaults to 5672, port used by some widely
70
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
71
+ #
72
+ # On start Nanite reads config.yml, so it is common to specify
73
+ # options in the YAML file. However, when both Ruby code options
74
+ # and YAML file specify option, Ruby code options take precedence.
75
+ def self.start(options = {})
76
+ agent = new(options)
77
+ agent.run
78
+ agent
79
+ end
80
+
81
+ def initialize(opts)
82
+ set_configuration(opts)
83
+ @tags = []
84
+ @tags << opts[:tag]
85
+ @tags.flatten!
86
+ @options.freeze
87
+ end
88
+
89
+ def run
90
+ log_path = false
91
+ if @options[:daemonize]
92
+ log_path = (@options[:log_dir] || @options[:root] || Dir.pwd)
93
+ end
94
+ Log.init(@identity, log_path)
95
+ Log.level = @options[:log_level] if @options[:log_level]
96
+ @serializer = Serializer.new(@options[:format])
97
+ @status_proc = lambda { parse_uptime(`uptime`) rescue 'no status' }
98
+ pid_file = PidFile.new(@identity, @options)
99
+ pid_file.check
100
+ if @options[:daemonize]
101
+ daemonize
102
+ pid_file.write
103
+ at_exit { pid_file.remove }
104
+ end
105
+ @amq = start_amqp(@options)
106
+ @registry = ActorRegistry.new
107
+ @dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @options)
108
+ setup_mapper_proxy
109
+ load_actors
110
+ setup_traps
111
+ setup_queue
112
+ advertise_services
113
+ setup_heartbeat
114
+ at_exit { un_register } unless $TESTING
115
+ start_console if @options[:console] && !@options[:daemonize]
116
+ end
117
+
118
+ def register(actor, prefix = nil)
119
+ registry.register(actor, prefix)
120
+ end
121
+
122
+ # Can be used in agent's initialization file to register a security module
123
+ # This security module 'authorize' method will be called back whenever the
124
+ # agent receives a request and will be given the corresponding deliverable.
125
+ # It should return 'true' for the request to proceed.
126
+ # Requests will return 'deny_token' or the string "Denied" by default when
127
+ # 'authorize' does not return 'true'.
128
+ def register_security(security, deny_token = "Denied")
129
+ @security = security
130
+ @deny_token = deny_token
131
+ end
132
+
133
+ protected
134
+
135
+ def set_configuration(opts)
136
+ @options = DEFAULT_OPTIONS.clone
137
+ root = opts[:root] || @options[:root]
138
+ custom_config = if root
139
+ file = File.expand_path(File.join(root, 'config.yml'))
140
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
141
+ else
142
+ {}
143
+ end
144
+ opts.delete(:identity) unless opts[:identity]
145
+ @options.update(custom_config.merge(opts))
146
+ @options[:file_root] ||= File.join(@options[:root], 'files')
147
+ return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
148
+ token = Identity.generate
149
+ @identity = "nanite-#{token}"
150
+ File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
151
+ fd.write(YAML.dump(custom_config.merge(:identity => token)))
152
+ end
153
+ end
154
+
155
+ def load_actors
156
+ return unless options[:root]
157
+ actors_dir = @options[:actors_dir] || "#{@options[:root]}/actors"
158
+ actors = @options[:actors]
159
+ Dir["#{actors_dir}/*.rb"].each do |actor|
160
+ next if actors && !actors.include?(File.basename(actor, ".rb"))
161
+ Nanite::Log.info("loading actor: #{actor}")
162
+ require actor
163
+ end
164
+ init_path = @options[:initrb] || File.join(options[:root], 'init.rb')
165
+ instance_eval(File.read(init_path), init_path) if File.exist?(init_path)
166
+ end
167
+
168
+ def receive(packet)
169
+ case packet
170
+ when Advertise
171
+ Nanite::Log.debug("handling Advertise: #{packet.inspect}")
172
+ advertise_services
173
+ when Request, Push
174
+ Nanite::Log.debug("handling Request: #{packet.inspect}")
175
+ if @security && !@security.authorize(packet)
176
+ if packet.kind_of?(Request)
177
+ r = Result.new(packet.token, packet.reply_to, @deny_token, identity)
178
+ amq.queue(packet.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
179
+ end
180
+ else
181
+ dispatcher.dispatch(packet)
182
+ end
183
+ when Result
184
+ Nanite::Log.debug("handling Result: #{packet.inspect}")
185
+ @mapper_proxy.handle_result(packet)
186
+ when IntermediateMessage
187
+ Nanite::Log.debug("handling Intermediate Result: #{packet.inspect}")
188
+ @mapper_proxy.handle_intermediate_result(packet)
189
+ end
190
+ end
191
+
192
+ def tag(*tags)
193
+ tags.each {|t| @tags << t}
194
+ @tags.uniq!
195
+ end
196
+
197
+ def setup_queue
198
+ amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
199
+ begin
200
+ info.ack
201
+ packet = serializer.load(msg)
202
+ receive(packet)
203
+ rescue Exception => e
204
+ Nanite::Log.error("Error handling packet: #{e.message}")
205
+ end
206
+ end
207
+ end
208
+
209
+ def setup_heartbeat
210
+ EM.add_periodic_timer(options[:ping_time]) do
211
+ amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
212
+ end
213
+ end
214
+
215
+ def setup_mapper_proxy
216
+ @mapper_proxy = MapperProxy.new(identity, options)
217
+ end
218
+
219
+ def setup_traps
220
+ ['INT', 'TERM'].each do |sig|
221
+ old = trap(sig) do
222
+ un_register
223
+ amq.instance_variable_get('@connection').close do
224
+ EM.stop
225
+ old.call if old.is_a? Proc
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ def un_register
232
+ unless @unregistered
233
+ @unregistered = true
234
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(UnRegister.new(identity)))
235
+ end
236
+ end
237
+
238
+ def advertise_services
239
+ Nanite::Log.debug("advertise_services: #{registry.services.inspect}")
240
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(Register.new(identity, registry.services, status_proc.call, self.tags)))
241
+ end
242
+
243
+ def parse_uptime(up)
244
+ if up =~ /load averages?: (.*)/
245
+ a,b,c = $1.split(/\s+|,\s+/)
246
+ (a.to_f + b.to_f + c.to_f) / 3
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,47 @@
1
+ class MQ
2
+ class Queue
3
+ # Asks the broker to redeliver all unacknowledged messages on a
4
+ # specifieid channel. Zero or more messages may be redelivered.
5
+ #
6
+ # * requeue (default false)
7
+ # If this parameter is false, the message will be redelivered to the original recipient.
8
+ # If this flag is true, the server will attempt to requeue the message, potentially then
9
+ # delivering it to an alternative subscriber.
10
+ #
11
+ def recover requeue = false
12
+ @mq.callback{
13
+ @mq.send Protocol::Basic::Recover.new({ :requeue => requeue })
14
+ }
15
+ self
16
+ end
17
+ end
18
+ end
19
+
20
+ # monkey patch to the amqp gem that adds :no_declare => true option for new
21
+ # Exchange objects. This allows us to send messeages to exchanges that are
22
+ # declared by the mappers and that we have no configuration priviledges on.
23
+ # temporary until we get this into amqp proper
24
+ MQ::Exchange.class_eval do
25
+ def initialize mq, type, name, opts = {}
26
+ @mq = mq
27
+ @type, @name, @opts = type, name, opts
28
+ @mq.exchanges[@name = name] ||= self
29
+ @key = opts[:key]
30
+
31
+ @mq.callback{
32
+ @mq.send AMQP::Protocol::Exchange::Declare.new({ :exchange => name,
33
+ :type => type,
34
+ :nowait => true }.merge(opts))
35
+ } unless name == "amq.#{type}" or name == '' or opts[:no_declare]
36
+ end
37
+ end
38
+
39
+ module Nanite
40
+ module AMQPHelper
41
+ def start_amqp(options)
42
+ connection = AMQP.connect(:user => options[:user], :pass => options[:pass], :vhost => options[:vhost],
43
+ :host => options[:host], :port => (options[:port] || ::AMQP::PORT).to_i, :insist => options[:insist] || false)
44
+ MQ.new(connection)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,203 @@
1
+ module Nanite
2
+ class Cluster
3
+ attr_reader :agent_timeout, :nanites, :reaper, :serializer, :identity, :amq, :redis, :mapper
4
+
5
+ def initialize(amq, agent_timeout, identity, serializer, mapper, redis=nil)
6
+ @amq = amq
7
+ @agent_timeout = agent_timeout
8
+ @identity = identity
9
+ @serializer = serializer
10
+ @mapper = mapper
11
+ @redis = redis
12
+ @security = SecurityProvider.get
13
+ if redis
14
+ Nanite::Log.info("using redis for state storage")
15
+ require 'nanite/state'
16
+ @nanites = ::Nanite::State.new(redis)
17
+ else
18
+ require 'nanite/local_state'
19
+ @nanites = Nanite::LocalState.new
20
+ end
21
+ @reaper = Reaper.new(agent_timeout)
22
+ setup_queues
23
+ end
24
+
25
+ # determine which nanites should receive the given request
26
+ def targets_for(request)
27
+ return [request.target] if request.target
28
+ __send__(request.selector, request.type, request.tags).collect {|name, state| name }
29
+ end
30
+
31
+ # adds nanite to nanites map: key is nanite's identity
32
+ # and value is a services/status pair implemented
33
+ # as a hash
34
+ def register(reg)
35
+ case reg
36
+ when Register
37
+ if @security.authorize_registration(reg)
38
+ nanites[reg.identity] = { :services => reg.services, :status => reg.status, :tags => reg.tags }
39
+ reaper.timeout(reg.identity, agent_timeout + 1) { nanites.delete(reg.identity) }
40
+ Nanite::Log.info("registered: #{reg.identity}, #{nanites[reg.identity].inspect}")
41
+ else
42
+ Nanite::Log.warning("registration of #{reg.inspect} not authorized")
43
+ end
44
+ when UnRegister
45
+ nanites.delete(reg.identity)
46
+ Nanite::Log.info("un-registering: #{reg.identity}")
47
+ end
48
+ end
49
+
50
+ def route(request, targets)
51
+ EM.next_tick { targets.map { |target| publish(request, target) } }
52
+ end
53
+
54
+ def publish(request, target)
55
+ # We need to initialize the 'target' field of the request object so that the serializer has
56
+ # access to it.
57
+ begin
58
+ old_target = request.target
59
+ request.target = target unless target == 'mapper-offline'
60
+ amq.queue(target).publish(serializer.dump(request), :persistent => request.persistent)
61
+ ensure
62
+ request.target = old_target
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ # updates nanite information (last ping timestamps, status)
69
+ # when heartbeat message is received
70
+ def handle_ping(ping)
71
+ if nanite = nanites[ping.identity]
72
+ nanite[:status] = ping.status
73
+ reaper.reset_with_autoregister_hack(ping.identity, agent_timeout + 1) { nanites.delete(ping.identity) }
74
+ else
75
+ amq.queue(ping.identity).publish(serializer.dump(Advertise.new))
76
+ end
77
+ end
78
+
79
+ # forward request coming from agent
80
+ def handle_request(request)
81
+ if @security.authorize_request(request)
82
+ result = Result.new(request.token, request.from, nil, mapper.identity)
83
+ intm_handler = lambda do |res|
84
+ result.results = res
85
+ forward_response(result, request.persistent)
86
+ end
87
+ ok = mapper.send_request(request, :intermediate_handler => intm_handler) do |res|
88
+ result.results = res
89
+ forward_response(result, request.persistent)
90
+ end
91
+ if ok == false
92
+ forward_response(result, request.persistent)
93
+ end
94
+ else
95
+ Nanite::Log.warning("request #{request.inspect} not authorized")
96
+ end
97
+ end
98
+
99
+ # forward response back to agent that originally made the request
100
+ def forward_response(res, persistent)
101
+ amq.queue(res.to).publish(serializer.dump(res), :persistent => persistent)
102
+ end
103
+
104
+ # returns least loaded nanite that provides given service
105
+ def least_loaded(service, tags=[])
106
+ candidates = nanites_providing(service,tags)
107
+ return [] if candidates.empty?
108
+
109
+ [candidates.min { |a,b| a[1][:status] <=> b[1][:status] }]
110
+ end
111
+
112
+ # returns all nanites that provide given service
113
+ def all(service, tags=[])
114
+ nanites_providing(service,tags)
115
+ end
116
+
117
+ # returns a random nanite
118
+ def random(service, tags=[])
119
+ candidates = nanites_providing(service,tags)
120
+ return [] if candidates.empty?
121
+
122
+ [candidates[rand(candidates.size)]]
123
+ end
124
+
125
+ # selects next nanite that provides given service
126
+ # using round robin rotation
127
+ def rr(service, tags=[])
128
+ @last ||= {}
129
+ @last[service] ||= 0
130
+ candidates = nanites_providing(service,tags)
131
+ return [] if candidates.empty?
132
+ @last[service] = 0 if @last[service] >= candidates.size
133
+ candidate = candidates[@last[service]]
134
+ @last[service] += 1
135
+ [candidate]
136
+ end
137
+
138
+ # returns all nanites that provide the given service
139
+ def nanites_providing(service, *tags)
140
+ nanites.nanites_for(service, *tags)
141
+ end
142
+
143
+ def setup_queues
144
+ setup_heartbeat_queue
145
+ setup_registration_queue
146
+ setup_request_queue
147
+ end
148
+
149
+ def setup_heartbeat_queue
150
+ handler = lambda do |ping|
151
+ begin
152
+ ping = serializer.load(ping)
153
+ Nanite::Log.debug("got heartbeat from #{ping.identity}") if ping.respond_to?(:identity)
154
+ handle_ping(ping)
155
+ rescue Exception => e
156
+ Nanite::Log.error("Error handling heartbeat: #{e.message}")
157
+ end
158
+ end
159
+ hb_fanout = amq.fanout('heartbeat', :durable => true)
160
+ if @redis
161
+ amq.queue("heartbeat").bind(hb_fanout).subscribe &handler
162
+ else
163
+ amq.queue("heartbeat-#{identity}", :exclusive => true).bind(hb_fanout).subscribe &handler
164
+ end
165
+ end
166
+
167
+ def setup_registration_queue
168
+ handler = lambda do |msg|
169
+ begin
170
+ msg = serializer.load(msg)
171
+ Nanite::Log.debug("got registration from #{msg.identity}")
172
+ register(msg)
173
+ rescue Exception => e
174
+ Nanite::Log.error("Error handling registration: #{e.message}")
175
+ end
176
+ end
177
+ reg_fanout = amq.fanout('registration', :durable => true)
178
+ if @redis
179
+ amq.queue("registration").bind(reg_fanout).subscribe &handler
180
+ else
181
+ amq.queue("registration-#{identity}", :exclusive => true).bind(reg_fanout).subscribe &handler
182
+ end
183
+ end
184
+
185
+ def setup_request_queue
186
+ handler = lambda do |msg|
187
+ begin
188
+ msg = serializer.load(msg)
189
+ Nanite::Log.debug("got request from #{msg.from} of type #{msg.type}")
190
+ handle_request(msg)
191
+ rescue Exception => e
192
+ Nanite::Log.error("Error handling request: #{e.message}")
193
+ end
194
+ end
195
+ req_fanout = amq.fanout('request', :durable => true)
196
+ if @redis
197
+ amq.queue("request").bind(req_fanout).subscribe &handler
198
+ else
199
+ amq.queue("request-#{identity}", :exclusive => true).bind(req_fanout).subscribe &handler
200
+ end
201
+ end
202
+ end
203
+ end