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