br-nanite 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,12 @@
1
+ module Nanite
2
+ module DaemonizeHelper
3
+ def daemonize
4
+ exit if fork
5
+ Process.setsid
6
+ exit if fork
7
+ $stdin.reopen("/dev/null")
8
+ $stdout.reopen(log.file, "a")
9
+ $stderr.reopen($stdout)
10
+ end
11
+ end
12
+ end