br-nanite 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +201 -0
- data/README.rdoc +356 -0
- data/Rakefile +74 -0
- data/TODO +24 -0
- data/bin/nanite-admin +59 -0
- data/bin/nanite-agent +30 -0
- data/bin/nanite-mapper +22 -0
- data/lib/nanite.rb +58 -0
- data/lib/nanite/actor.rb +64 -0
- data/lib/nanite/actor_registry.rb +25 -0
- data/lib/nanite/admin.rb +147 -0
- data/lib/nanite/agent.rb +163 -0
- data/lib/nanite/amqp.rb +47 -0
- data/lib/nanite/cluster.rb +110 -0
- data/lib/nanite/config.rb +80 -0
- data/lib/nanite/console.rb +39 -0
- data/lib/nanite/daemonize.rb +12 -0
- data/lib/nanite/dispatcher.rb +59 -0
- data/lib/nanite/identity.rb +16 -0
- data/lib/nanite/job.rb +50 -0
- data/lib/nanite/log.rb +23 -0
- data/lib/nanite/mapper.rb +214 -0
- data/lib/nanite/packets.rb +192 -0
- data/lib/nanite/reaper.rb +30 -0
- data/lib/nanite/serializer.rb +40 -0
- data/lib/nanite/streaming.rb +125 -0
- data/lib/nanite/util.rb +51 -0
- metadata +104 -0
data/lib/nanite/agent.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
module Nanite
|
2
|
+
class Agent
|
3
|
+
include AMQPHelper
|
4
|
+
include FileStreaming
|
5
|
+
include ConsoleHelper
|
6
|
+
include DaemonizeHelper
|
7
|
+
|
8
|
+
attr_reader :identity, :log, :options, :serializer, :dispatcher, :registry, :amq
|
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
|
+
# services : list of services provided by this agent, by default
|
50
|
+
# all methods exposed by actors are listed
|
51
|
+
#
|
52
|
+
# Connection options:
|
53
|
+
#
|
54
|
+
# vhost : AMQP broker vhost that should be used
|
55
|
+
#
|
56
|
+
# user : AMQP broker user
|
57
|
+
#
|
58
|
+
# pass : AMQP broker password
|
59
|
+
#
|
60
|
+
# host : host AMQP broker (or node of interest) runs on,
|
61
|
+
# defaults to 0.0.0.0
|
62
|
+
#
|
63
|
+
# port : port AMQP broker (or node of interest) runs on,
|
64
|
+
# this defaults to 5672, port used by some widely
|
65
|
+
# used AMQP brokers (RabbitMQ and ZeroMQ)
|
66
|
+
#
|
67
|
+
# On start Nanite reads config.yml, so it is common to specify
|
68
|
+
# options in the YAML file. However, when both Ruby code options
|
69
|
+
# and YAML file specify option, Ruby code options take precedence.
|
70
|
+
def self.start(options = {})
|
71
|
+
new(options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize(opts)
|
75
|
+
set_configuration(opts)
|
76
|
+
@log = Log.new(@options, @identity)
|
77
|
+
@serializer = Serializer.new(@options[:format])
|
78
|
+
@status_proc = lambda { parse_uptime(`uptime`) rescue 'no status' }
|
79
|
+
daemonize if @options[:daemonize]
|
80
|
+
@amq = start_amqp(@options)
|
81
|
+
@registry = ActorRegistry.new(@log)
|
82
|
+
@dispatcher = Dispatcher.new(@amq, @registry, @serializer, @identity, @log, @options)
|
83
|
+
load_actors
|
84
|
+
setup_queue
|
85
|
+
advertise_services
|
86
|
+
setup_heartbeat
|
87
|
+
start_console if @options[:console] && !@options[:daemonize]
|
88
|
+
end
|
89
|
+
|
90
|
+
def register(actor, prefix = nil)
|
91
|
+
registry.register(actor, prefix)
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
def set_configuration(opts)
|
97
|
+
@options = DEFAULT_OPTIONS.clone
|
98
|
+
root = opts[:root] || @options[:root]
|
99
|
+
custom_config = if root
|
100
|
+
file = File.expand_path(File.join(root, 'config.yml'))
|
101
|
+
File.exists?(file) ? (YAML.load(IO.read(file)) || {}) : {}
|
102
|
+
else
|
103
|
+
{}
|
104
|
+
end
|
105
|
+
opts.delete(:identity) unless opts[:identity]
|
106
|
+
@options.update(custom_config.merge(opts))
|
107
|
+
@options[:file_root] ||= File.join(@options[:root], 'files')
|
108
|
+
return @identity = "nanite-#{@options[:identity]}" if @options[:identity]
|
109
|
+
token = Identity.generate
|
110
|
+
@identity = "nanite-#{token}"
|
111
|
+
File.open(File.expand_path(File.join(@options[:root], 'config.yml')), 'w') do |fd|
|
112
|
+
fd.write(YAML.dump(custom_config.merge(:identity => token)))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_actors
|
117
|
+
return unless options[:root]
|
118
|
+
Dir["#{options[:root]}/actors/*.rb"].each do |actor|
|
119
|
+
log.info("loading actor: #{actor}")
|
120
|
+
require actor
|
121
|
+
end
|
122
|
+
init_path = File.join(options[:root], 'init.rb')
|
123
|
+
instance_eval(File.read(init_path), init_path) if File.exist?(init_path)
|
124
|
+
end
|
125
|
+
|
126
|
+
def receive(packet)
|
127
|
+
packet = serializer.load(packet)
|
128
|
+
case packet
|
129
|
+
when Advertise
|
130
|
+
log.debug("handling Advertise: #{packet}")
|
131
|
+
advertise_services
|
132
|
+
when Request, Push
|
133
|
+
log.debug("handling Request: #{packet}")
|
134
|
+
dispatcher.dispatch(packet)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def setup_queue
|
139
|
+
amq.queue(identity, :durable => true).subscribe(:ack => true) do |info, msg|
|
140
|
+
info.ack
|
141
|
+
receive(msg)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def setup_heartbeat
|
146
|
+
EM.add_periodic_timer(options[:ping_time]) do
|
147
|
+
amq.fanout('heartbeat', :no_declare => options[:secure]).publish(serializer.dump(Ping.new(identity, status_proc.call)))
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def advertise_services
|
152
|
+
log.debug("advertise_services: #{registry.services.inspect}")
|
153
|
+
amq.fanout('registration', :no_declare => options[:secure]).publish(serializer.dump(Register.new(identity, registry.services, status_proc.call)))
|
154
|
+
end
|
155
|
+
|
156
|
+
def parse_uptime(up)
|
157
|
+
if up =~ /load averages?: (.*)/
|
158
|
+
a,b,c = $1.split(/\s+|,\s+/)
|
159
|
+
(a.to_f + b.to_f + c.to_f) / 3
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/nanite/amqp.rb
ADDED
@@ -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,110 @@
|
|
1
|
+
module Nanite
|
2
|
+
class Cluster
|
3
|
+
attr_reader :agent_timeout, :nanites, :reaper, :log, :serializer, :identity, :amq
|
4
|
+
|
5
|
+
def initialize(amq, agent_timeout, identity, log, serializer)
|
6
|
+
@amq = amq
|
7
|
+
@agent_timeout = agent_timeout
|
8
|
+
@identity = identity
|
9
|
+
@serializer = serializer
|
10
|
+
@nanites = {}
|
11
|
+
@reaper = Reaper.new(agent_timeout)
|
12
|
+
@log = log
|
13
|
+
setup_queues
|
14
|
+
end
|
15
|
+
|
16
|
+
# determine which nanites should receive the given request
|
17
|
+
def targets_for(request)
|
18
|
+
return [request.target] if request.target
|
19
|
+
__send__(request.selector, request.type).collect {|name, state| name }
|
20
|
+
end
|
21
|
+
|
22
|
+
# adds nanite to nanites map: key is nanite's identity
|
23
|
+
# and value is a services/status pair implemented
|
24
|
+
# as a hash
|
25
|
+
def register(reg)
|
26
|
+
nanites[reg.identity] = { :services => reg.services, :status => reg.status }
|
27
|
+
reaper.timeout(reg.identity, agent_timeout + 1) { nanites.delete(reg.identity) }
|
28
|
+
log.info("registered: #{reg.identity}, #{nanites[reg.identity]}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def route(request, targets)
|
32
|
+
EM.next_tick { targets.map { |target| publish(request, target) } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def publish(request, target)
|
36
|
+
amq.queue(target).publish(serializer.dump(request), :persistent => request.persistent)
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
# updates nanite information (last ping timestamps, status)
|
42
|
+
# when heartbeat message is received
|
43
|
+
def handle_ping(ping)
|
44
|
+
if nanite = nanites[ping.identity]
|
45
|
+
nanite[:status] = ping.status
|
46
|
+
reaper.reset(ping.identity)
|
47
|
+
else
|
48
|
+
amq.queue(ping.identity).publish(serializer.dump(Advertise.new))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# returns least loaded nanite that provides given service
|
53
|
+
def least_loaded(service)
|
54
|
+
candidates = nanites_providing(service)
|
55
|
+
return [] if candidates.empty?
|
56
|
+
|
57
|
+
[candidates.min { |a,b| a[1][:status] <=> b[1][:status] }]
|
58
|
+
end
|
59
|
+
|
60
|
+
# returns all nanites that provide given service
|
61
|
+
def all(service)
|
62
|
+
nanites_providing(service)
|
63
|
+
end
|
64
|
+
|
65
|
+
# returns a random nanite
|
66
|
+
def random(service)
|
67
|
+
candidates = nanites_providing(service)
|
68
|
+
return [] if candidates.empty?
|
69
|
+
|
70
|
+
[candidates[rand(candidates.size)]]
|
71
|
+
end
|
72
|
+
|
73
|
+
# selects next nanite that provides given service
|
74
|
+
# using round robin rotation
|
75
|
+
def rr(service)
|
76
|
+
@last ||= {}
|
77
|
+
@last[service] ||= 0
|
78
|
+
candidates = nanites_providing(service)
|
79
|
+
return [] if candidates.empty?
|
80
|
+
@last[service] = 0 if @last[service] >= candidates.size
|
81
|
+
candidate = candidates[@last[service]]
|
82
|
+
@last[service] += 1
|
83
|
+
[candidate]
|
84
|
+
end
|
85
|
+
|
86
|
+
# returns all nanites that provide the given service
|
87
|
+
def nanites_providing(service)
|
88
|
+
nanites.find_all { |name, state| state[:services].include?(service) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def setup_queues
|
92
|
+
setup_heartbeat_queue
|
93
|
+
setup_registration_queue
|
94
|
+
end
|
95
|
+
|
96
|
+
def setup_heartbeat_queue
|
97
|
+
amq.queue("heartbeat-#{identity}", :exclusive => true).bind(amq.fanout('heartbeat', :durable => true)).subscribe do |ping|
|
98
|
+
log.debug('got heartbeat')
|
99
|
+
handle_ping(serializer.load(ping))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_registration_queue
|
104
|
+
amq.queue("registration-#{identity}", :exclusive => true).bind(amq.fanout('registration', :durable => true)).subscribe do |msg|
|
105
|
+
log.debug('got registration')
|
106
|
+
register(serializer.load(msg))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Nanite
|
2
|
+
|
3
|
+
COMMON_DEFAULT_OPTIONS = {:pass => 'testing', :vhost => '/nanite', :secure => false, :host => '0.0.0.0',
|
4
|
+
:log_level => :info, :format => :marshal, :daemonize => false, :console => false, :root => Dir.pwd}
|
5
|
+
|
6
|
+
module CommonConfig
|
7
|
+
def setup_mapper_options(opts, options)
|
8
|
+
setup_common_options(opts, options, 'mapper')
|
9
|
+
|
10
|
+
opts.on("-a", "--agent-timeout", "How long to wait before an agent is considered to be offline and thus removed from the list of available agents.") do |timeout|
|
11
|
+
options[:agent_timeout] = timeout
|
12
|
+
end
|
13
|
+
|
14
|
+
opts.on("-r", "--offline-redelivery-frequency", "The frequency in seconds that messages stored in the offline queue will be retrieved for attempted redelivery to the nanites. Default is 10 seconds.") do |frequency|
|
15
|
+
options[:offline_redelivery_frequency] = frequency
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("--persistent", "Instructs the AMQP broker to save messages to persistent storage so that they aren't lost when the broker is restarted. Can be overriden on a per-message basis using the request and push methods.") do
|
19
|
+
options[:persistent] = true
|
20
|
+
end
|
21
|
+
|
22
|
+
opts.on("--offline-failsafe", "Store messages in an offline queue when all the nanites are offline. Messages will be redelivered when nanites come online. Can be overriden on a per-message basis using the request methods.") do
|
23
|
+
options[:offline_failsafe] = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def setup_common_options(opts, options, type)
|
28
|
+
opts.version = Nanite::VERSION
|
29
|
+
|
30
|
+
opts.on("-i", "--irb-console", "Start #{type} in irb console mode.") do |console|
|
31
|
+
options[:console] = 'irb'
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-u", "--user USER", "Specify the rabbitmq username.") do |user|
|
35
|
+
options[:user] = user
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-h", "--host HOST", "Specify the rabbitmq hostname.") do |host|
|
39
|
+
options[:host] = host
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("-P", "--port PORT", "Specify the rabbitmq PORT, default 5672.") do |port|
|
43
|
+
options[:port] = port
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("-p", "--pass PASSWORD", "Specify the rabbitmq password") do |pass|
|
47
|
+
options[:pass] = pass
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-t", "--token IDENITY", "Specify the #{type} identity.") do |ident|
|
51
|
+
options[:identity] = ident
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-v", "--vhost VHOST", "Specify the rabbitmq vhost") do |vhost|
|
55
|
+
options[:vhost] = vhost
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on("-s", "--secure", "Use Security features of rabbitmq to restrict nanites to themselves") do
|
59
|
+
options[:secure] = true
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on("-f", "--format FORMAT", "The serialization type to use for transfering data. Can be marshal, json or yaml. Default is marshal") do |json|
|
63
|
+
options[:format] = :json
|
64
|
+
end
|
65
|
+
|
66
|
+
opts.on("-d", "--daemonize", "Run #{type} as a daemon") do |d|
|
67
|
+
options[:daemonize] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
opts.on("-l", "--log-level LEVEL", "Specify the log level (fatal, error, warn, info, debug). Default is info") do |level|
|
71
|
+
options[:log_level] = level
|
72
|
+
end
|
73
|
+
|
74
|
+
opts.on("--version", "Show the nanite version number") do |res|
|
75
|
+
puts "Nanite Version #{opts.version}"
|
76
|
+
exit
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Nanite
|
2
|
+
module ConsoleHelper
|
3
|
+
def self.included(base)
|
4
|
+
@@base = base
|
5
|
+
end
|
6
|
+
|
7
|
+
def start_console
|
8
|
+
puts "Starting #{@@base.name.split(":").last.downcase} console (#{self.identity}) (Nanite #{Nanite::VERSION})"
|
9
|
+
Thread.new do
|
10
|
+
Console.start(self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Console
|
16
|
+
class << self; attr_accessor :instance; end
|
17
|
+
|
18
|
+
def self.start(binding)
|
19
|
+
require 'irb'
|
20
|
+
old_args = ARGV.dup
|
21
|
+
ARGV.replace ["--simple-prompt"]
|
22
|
+
|
23
|
+
IRB.setup(nil)
|
24
|
+
self.instance = IRB::Irb.new(IRB::WorkSpace.new(binding))
|
25
|
+
|
26
|
+
@CONF = IRB.instance_variable_get(:@CONF)
|
27
|
+
@CONF[:IRB_RC].call self.instance.context if @CONF[:IRB_RC]
|
28
|
+
@CONF[:MAIN_CONTEXT] = self.instance.context
|
29
|
+
|
30
|
+
catch(:IRB_EXIT) { self.instance.eval_input }
|
31
|
+
ensure
|
32
|
+
ARGV.replace old_args
|
33
|
+
# Clean up tty settings in some evil, evil cases
|
34
|
+
begin; catch(:IRB_EXIT) { irb_exit }; rescue Exception; end
|
35
|
+
# Make nanite exit when irb does
|
36
|
+
EM.stop if EM.reactor_running?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|