nanite 0.4.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) 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 +111 -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. metadata +109 -0
@@ -0,0 +1,138 @@
1
+ require 'rack'
2
+
3
+ module Nanite
4
+ # This is a Rack app for nanite-admin. You need to have an async capable
5
+ # version of Thin installed for this to work. See bin/nanite-admin for install
6
+ # instructions.
7
+ class Admin
8
+ def initialize(mapper)
9
+ @mapper = mapper
10
+ end
11
+
12
+ AsyncResponse = [-1, {}, []].freeze
13
+
14
+ def call(env)
15
+ req = Rack::Request.new(env)
16
+ if cmd = req.params['command']
17
+ @command = cmd
18
+ @selection = req.params['type'] if req.params['type']
19
+
20
+ options = {}
21
+ case @selection
22
+ when 'least_loaded', 'random', 'all', 'rr'
23
+ options[:selector] = @selection
24
+ else
25
+ options[:target] = @selection
26
+ end
27
+
28
+ @mapper.request(cmd, req.params['payload'], options) do |response, responsejob|
29
+ env['async.callback'].call [200, {'Content-Type' => 'text/html'}, [layout(ul(response, responsejob))]]
30
+ end
31
+ AsyncResponse
32
+ else
33
+ [200, {'Content-Type' => 'text/html'}, layout]
34
+ end
35
+ end
36
+
37
+ def services
38
+ buf = "<select name='command'>"
39
+ @mapper.cluster.nanites.all_services.each do |srv|
40
+ buf << "<option value='#{srv}' #{@command == srv ? 'selected="true"' : ''}>#{srv}</option>"
41
+ end
42
+ buf << "</select>"
43
+ buf
44
+ end
45
+
46
+ def ul(hash, job)
47
+ buf = "<ul>"
48
+ hash.each do |k,v|
49
+ buf << "<li><div class=\"nanite\">#{k}:</div><div class=\"response\">#{v.inspect}</div>"
50
+ if job.intermediate_state && job.intermediate_state[k]
51
+ buf << "<div class=\"intermediatestates\"><span class=\"statenote\">intermediate state:</span> #{job.intermediate_state[k].inspect}</div>"
52
+ end
53
+ buf << "</li>"
54
+ end
55
+ buf << "</ul>"
56
+ buf
57
+ end
58
+
59
+ def layout(content=nil)
60
+ %Q{
61
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
62
+ <html xmlns='http://www.w3.org/1999/xhtml'>
63
+ <head>
64
+ <meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
65
+ <meta content='en' http-equiv='Content-Language' />
66
+ <meta content='Engineyard' name='author' />
67
+ <title>Nanite Control Tower</title>
68
+
69
+ <style>
70
+ body {margin: 0; font-family: verdana; background-color: #fcfcfc;}
71
+ ul {margin: 0; padding: 0; margin-left: 10px}
72
+ li {list-style-type: none; margin-bottom: 6px}
73
+ li .nanite {font-weight: bold; font-size: 12px}
74
+ li .response {padding: 8px}
75
+ li .intermediatestates {padding: 8px; font-size: 10px;}
76
+ li .intermediatestates span.statenote {font-style: italic;}
77
+ h1, h2, h3 {margin-top: none; padding: none; margin-left: 40px;}
78
+ h1 {font-size: 22px; margin-top: 40px; margin-bottom: 30px; border-bottom: 1px solid #ddd; padding-bottom: 6px;
79
+ margin-right: 40px}
80
+ h2 {font-size: 16px;}
81
+ h3 {margin-left: 0; font-size: 14px}
82
+ .section {border: 1px solid #ccc; background-color: #fefefe; padding: 10px; margin: 20px 40px; padding: 20px;
83
+ font-size: 14px}
84
+ #footer {text-align: center; color: #AAA; font-size: 12px}
85
+ </style>
86
+
87
+ </head>
88
+
89
+ <body>
90
+ <div id="header">
91
+ <h1>Nanite Control Tower</h1>
92
+ </div>
93
+
94
+ <h2>#{@mapper.options[:vhost]}</h2>
95
+ <div class="section">
96
+ <form method="post" action="/">
97
+ <input type="hidden" value="POST" name="_method"/>
98
+
99
+ <label>Send</label>
100
+ <select name="type">
101
+ <option #{@selection == 'least_loaded' ? 'selected="true"' : ''} value="least_loaded">the least loaded nanite</option>
102
+ <option #{@selection == 'random' ? 'selected="true"' : ''} value="random">a random nanite</option>
103
+ <option #{@selection == 'all' ? 'selected="true"' : ''} value="all">all nanites</option>
104
+ <option #{@selection == 'rr' ? 'selected="true"' : ''} value="rr">a nanite chosen by round robin</option>
105
+ #{@mapper.cluster.nanites.map {|k,v| "<option #{@selection == k ? 'selected="true"' : ''} value='#{k}'>#{k}</option>" }.join}
106
+ </select>
107
+
108
+ <label>providing service</label>
109
+ #{services}
110
+
111
+ <label>the payload</label>
112
+ <input type="text" class="text" name="payload" id="payload"/>
113
+
114
+ <input type="submit" class="submit" value="Go!" name="submit"/>
115
+ </form>
116
+
117
+ #{"<h3>Responses</h3>" if content}
118
+ #{content}
119
+ </div>
120
+
121
+ <h2>Running nanites</h2>
122
+ <div class="section">
123
+ #{"No nanites online." if @mapper.cluster.nanites.size == 0}
124
+ <ul>
125
+ #{@mapper.cluster.nanites.map {|k,v| "<li>identity : #{k}<br />load : #{v[:status]}<br />services : #{v[:services].to_a.inspect}<br />tags: #{v[:tags].to_a.inspect}</li>" }.join}
126
+ </ul>
127
+ </div>
128
+ <div id="footer">
129
+ Nanite #{Nanite::VERSION}
130
+ <br />
131
+ &copy; 2009 a bunch of random geeks
132
+ </div>
133
+ </body>
134
+ </html>
135
+ }
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,264 @@
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({
12
+ :user => 'nanite',
13
+ :ping_time => 15,
14
+ :default_services => []
15
+ }) unless defined?(DEFAULT_OPTIONS)
16
+
17
+ # Initializes a new agent and establishes AMQP connection.
18
+ # This must be used inside EM.run block or if EventMachine reactor
19
+ # is already started, for instance, by a Thin server that your Merb/Rails
20
+ # application runs on.
21
+ #
22
+ # Agent options:
23
+ #
24
+ # identity : identity of this agent, may be any string
25
+ #
26
+ # status_proc : a callable object that returns agent load as a string,
27
+ # defaults to load averages string extracted from `uptime`
28
+ # format : format to use for packets serialization. One of the three:
29
+ # :marshall, :json, or :yaml. Defaults to
30
+ # Ruby's Marshall format. For interoperability with
31
+ # AMQP clients implemented in other languages, use JSON.
32
+ #
33
+ # Note that Nanite uses JSON gem,
34
+ # and ActiveSupport's JSON encoder may cause clashes
35
+ # if ActiveSupport is loaded after JSON gem.
36
+ #
37
+ # root : application root for this agent, defaults to Dir.pwd
38
+ #
39
+ # log_dir : path to directory where agent stores it's log file
40
+ # if not given, app_root is used.
41
+ #
42
+ # file_root : path to directory to files this agent provides
43
+ # defaults to app_root/files
44
+ #
45
+ # ping_time : time interval in seconds between two subsequent heartbeat messages
46
+ # this agent broadcasts. Default value is 15.
47
+ #
48
+ # console : true tells Nanite to start interactive console
49
+ #
50
+ # daemonize : true tells Nanite to daemonize
51
+ #
52
+ # pid_dir : path to the directory where the agent stores its pid file (only if daemonized)
53
+ # defaults to the root or the current working directory.
54
+ #
55
+ # services : list of services provided by this agent, by default
56
+ # all methods exposed by actors are listed
57
+ #
58
+ # single_threaded: Run all operations in one thread
59
+ #
60
+ # threadpool_size: Number of threads to run operations in
61
+ #
62
+ # Connection options:
63
+ #
64
+ # vhost : AMQP broker vhost that should be used
65
+ #
66
+ # user : AMQP broker user
67
+ #
68
+ # pass : AMQP broker password
69
+ #
70
+ # host : host AMQP broker (or node of interest) runs on,
71
+ # defaults to 0.0.0.0
72
+ #
73
+ # port : port AMQP broker (or node of interest) runs on,
74
+ # this defaults to 5672, port used by some widely
75
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
76
+ #
77
+ # On start Nanite reads config.yml, so it is common to specify
78
+ # options in the YAML file. However, when both Ruby code options
79
+ # and YAML file specify option, Ruby code options take precedence.
80
+ def self.start(options = {})
81
+ agent = new(options)
82
+ agent.run
83
+ agent
84
+ end
85
+
86
+ def initialize(opts)
87
+ set_configuration(opts)
88
+ @tags = []
89
+ @tags << opts[:tag]
90
+ @tags.flatten!
91
+ @options.freeze
92
+ end
93
+
94
+ def run
95
+ Log.init(@identity, @options[:log_path])
96
+ Log.level = @options[:log_level] if @options[:log_level]
97
+ @serializer = Serializer.new(@options[:format])
98
+ @status_proc = lambda { parse_uptime(`uptime 2> /dev/null`) rescue 'no status' }
99
+ pid_file = PidFile.new(@identity, @options)
100
+ pid_file.check
101
+ if @options[:daemonize]
102
+ daemonize(@identity, @options)
103
+ pid_file.write
104
+ at_exit { pid_file.remove }
105
+ end
106
+ @amq = start_amqp(@options)
107
+ @registry = ActorRegistry.new
108
+ @dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @options)
109
+ setup_mapper_proxy
110
+ load_actors
111
+ setup_traps
112
+ setup_queue
113
+ advertise_services
114
+ setup_heartbeat
115
+ at_exit { un_register } unless $TESTING
116
+ start_console if @options[:console] && !@options[:daemonize]
117
+ end
118
+
119
+ def register(actor, prefix = nil)
120
+ registry.register(actor, prefix)
121
+ end
122
+
123
+ # Can be used in agent's initialization file to register a security module
124
+ # This security module 'authorize' method will be called back whenever the
125
+ # agent receives a request and will be given the corresponding deliverable.
126
+ # It should return 'true' for the request to proceed.
127
+ # Requests will return 'deny_token' or the string "Denied" by default when
128
+ # 'authorize' does not return 'true'.
129
+ def register_security(security, deny_token = "Denied")
130
+ @security = security
131
+ @deny_token = deny_token
132
+ end
133
+
134
+ protected
135
+
136
+ def set_configuration(opts)
137
+ @options = DEFAULT_OPTIONS.clone
138
+ root = opts[:root] || @options[:root]
139
+ custom_config = if root
140
+ file = File.expand_path(File.join(root, 'config.yml'))
141
+ File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
142
+ else
143
+ {}
144
+ end
145
+ opts.delete(:identity) unless opts[:identity]
146
+ @options.update(custom_config.merge(opts))
147
+ @options[:file_root] ||= File.join(@options[:root], 'files')
148
+ @options[:log_path] = false
149
+ if @options[:daemonize]
150
+ @options[:log_path] = (@options[:log_dir] || @options[:root] || Dir.pwd)
151
+ end
152
+
153
+ return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
154
+ token = Identity.generate
155
+ @identity = "nanite-#{token}"
156
+ File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
157
+ fd.write(YAML.dump(custom_config.merge(:identity => token)))
158
+ end
159
+ end
160
+
161
+ def load_actors
162
+ return unless options[:root]
163
+ actors_dir = @options[:actors_dir] || "#{@options[:root]}/actors"
164
+ Nanite::Log.warn("Actors dir #{actors_dir} does not exist or is not reachable") unless File.directory?(actors_dir)
165
+ actors = @options[:actors]
166
+ Dir["#{actors_dir}/*.rb"].each do |actor|
167
+ next if actors && !actors.include?(File.basename(actor, ".rb"))
168
+ Nanite::Log.info("[setup] loading #{actor}")
169
+ require actor
170
+ end
171
+ init_path = @options[:initrb] || File.join(options[:root], 'init.rb')
172
+ if File.exist?(init_path)
173
+ instance_eval(File.read(init_path), init_path)
174
+ else
175
+ Nanite::Log.warn("init.rb #{init_path} does not exist or is not reachable") unless File.exists?(init_path)
176
+ end
177
+ end
178
+
179
+ def receive(packet)
180
+ Nanite::Log.debug("RECV #{packet.to_s}")
181
+ case packet
182
+ when Advertise
183
+ Nanite::Log.info("RECV #{packet.to_s}") unless Nanite::Log.level == :debug
184
+ advertise_services
185
+ when Request, Push
186
+ if @security && !@security.authorize(packet)
187
+ Nanite::Log.warn("RECV NOT AUTHORIZED #{packet.to_s}")
188
+ if packet.kind_of?(Request)
189
+ r = Result.new(packet.token, packet.reply_to, @deny_token, identity)
190
+ amq.queue(packet.reply_to, :no_declare => options[:secure]).publish(serializer.dump(r))
191
+ end
192
+ else
193
+ Nanite::Log.info("RECV #{packet.to_s([:from, :tags])}") unless Nanite::Log.level == :debug
194
+ dispatcher.dispatch(packet)
195
+ end
196
+ when Result
197
+ Nanite::Log.info("RECV #{packet.to_s([])}") unless Nanite::Log.level == :debug
198
+ @mapper_proxy.handle_result(packet)
199
+ when IntermediateMessage
200
+ Nanite::Log.info("RECV #{packet.to_s([])}") unless Nanite::Log.level == :debug
201
+ @mapper_proxy.handle_intermediate_result(packet)
202
+ end
203
+ end
204
+
205
+ def tag(*tags)
206
+ tags.each {|t| @tags << t}
207
+ @tags.uniq!
208
+ end
209
+
210
+ def setup_queue
211
+ amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
212
+ begin
213
+ info.ack
214
+ receive(serializer.load(msg))
215
+ rescue Exception => e
216
+ Nanite::Log.error("RECV #{e.message}")
217
+ end
218
+ end
219
+ end
220
+
221
+ def setup_heartbeat
222
+ EM.add_periodic_timer(options[:ping_time]) do
223
+ amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
224
+ end
225
+ end
226
+
227
+ def setup_mapper_proxy
228
+ @mapper_proxy = MapperProxy.new(identity, options)
229
+ end
230
+
231
+ def setup_traps
232
+ ['INT', 'TERM'].each do |sig|
233
+ old = trap(sig) do
234
+ un_register
235
+ amq.instance_variable_get('@connection').close do
236
+ EM.stop
237
+ old.call if old.is_a? Proc
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ def un_register
244
+ unless @unregistered
245
+ @unregistered = true
246
+ Nanite::Log.info("SEND [un_register]")
247
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(UnRegister.new(identity)))
248
+ end
249
+ end
250
+
251
+ def advertise_services
252
+ reg = Register.new(identity, registry.services, status_proc.call, self.tags)
253
+ Nanite::Log.info("SEND #{reg.to_s}")
254
+ amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(reg))
255
+ end
256
+
257
+ def parse_uptime(up)
258
+ if up =~ /load averages?: (.*)/
259
+ a,b,c = $1.split(/\s+|,\s+/)
260
+ (a.to_f + b.to_f + c.to_f) / 3
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,58 @@
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({
43
+ :user => options[:user],
44
+ :pass => options[:pass],
45
+ :vhost => options[:vhost],
46
+ :host => options[:host],
47
+ :port => (options[:port] || ::AMQP::PORT).to_i,
48
+ :insist => options[:insist] || false,
49
+ :retry => options[:retry] || 5,
50
+ :connection_status => options[:connection_callback] || proc {|event|
51
+ Nanite::Log.debug("CONNECTED to MQ") if event == :connected
52
+ Nanite::Log.debug("DISCONNECTED from MQ") if event == :disconnected
53
+ }
54
+ })
55
+ MQ.new(connection)
56
+ end
57
+ end
58
+ end