br-nanite 0.3.0
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.
- 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
|