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,59 @@
1
+ module Nanite
2
+ class Dispatcher
3
+ attr_reader :registry, :serializer, :identity, :log, :amq, :options
4
+
5
+ def initialize(amq, registry, serializer, identity, log, options)
6
+ @amq = amq
7
+ @registry = registry
8
+ @serializer = serializer
9
+ @identity = identity
10
+ @log = log
11
+ @options = options
12
+ end
13
+
14
+ def dispatch(deliverable)
15
+ result = begin
16
+ prefix, meth = deliverable.type.split('/')[1..-1]
17
+ actor = registry.actor_for(prefix)
18
+ actor.send(meth, deliverable.payload)
19
+ rescue Exception => e
20
+ handle_exception(actor, meth, deliverable, e)
21
+ end
22
+
23
+ if deliverable.kind_of?(Request)
24
+ result = Result.new(deliverable.token, deliverable.reply_to, result, identity)
25
+ amq.queue(deliverable.reply_to, :no_declare => options[:secure]).publish(serializer.dump(result))
26
+ end
27
+
28
+ result
29
+ end
30
+
31
+ private
32
+
33
+ def describe_error(e)
34
+ "#{e.class.name}: #{e.message}\n #{e.backtrace.join("\n ")}"
35
+ end
36
+
37
+ def handle_exception(actor, meth, deliverable, e)
38
+ error = describe_error(e)
39
+ log.error(error)
40
+ begin
41
+ if actor.class.instance_exception_callback
42
+ case actor.class.instance_exception_callback
43
+ when Symbol, String
44
+ actor.send(actor.class.instance_exception_callback, meth.to_sym, deliverable, e)
45
+ when Proc
46
+ actor.instance_exec(meth.to_sym, deliverable, e, &actor.class.instance_exception_callback)
47
+ end
48
+ end
49
+ if Nanite::Actor.superclass_exception_callback
50
+ Nanite::Actor.superclass_exception_callback.call(actor, meth.to_sym, deliverable, e)
51
+ end
52
+ rescue Exception => e1
53
+ error = describe_error(e1)
54
+ log.error(error)
55
+ end
56
+ error
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,16 @@
1
+ module Nanite
2
+ module Identity
3
+ def self.generate
4
+ values = [
5
+ rand(0x0010000),
6
+ rand(0x0010000),
7
+ rand(0x0010000),
8
+ rand(0x0010000),
9
+ rand(0x0010000),
10
+ rand(0x1000000),
11
+ rand(0x1000000),
12
+ ]
13
+ "%04x%04x%04x%04x%04x%06x%06x" % values
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ module Nanite
2
+ class JobWarden
3
+ attr_reader :serializer, :jobs, :log
4
+
5
+ def initialize(serializer, log)
6
+ @serializer = serializer
7
+ @log = log
8
+ @jobs = {}
9
+ end
10
+
11
+ def new_job(request, targets, blk = nil)
12
+ job = Job.new(request, targets, blk)
13
+ jobs[job.token] = job
14
+ job
15
+ end
16
+
17
+ def process(msg)
18
+ msg = serializer.load(msg)
19
+ log.debug("processing message: #{msg.inspect}")
20
+ if job = jobs[msg.token]
21
+ job.process(msg)
22
+ if job.completed?
23
+ jobs.delete(job.token)
24
+ job.completed.call(job.results) if job.completed
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ class Job
31
+ attr_reader :results, :request, :token, :targets, :completed
32
+
33
+ def initialize(request, targets, blk)
34
+ @request = request
35
+ @targets = targets
36
+ @token = @request.token
37
+ @results = {}
38
+ @completed = blk
39
+ end
40
+
41
+ def process(msg)
42
+ results[msg.from] = msg.results
43
+ targets.delete(msg.from)
44
+ end
45
+
46
+ def completed?
47
+ targets.empty?
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ module Nanite
2
+ class Log
3
+ def initialize(options, identity)
4
+ @file = File.join((options[:log_dir] || options[:root] || Dir.pwd), "nanite.#{identity}.log")
5
+ @logger = Logger.new(file)
6
+ @logger.level = log_level(options[:log_level])
7
+ end
8
+
9
+ def file
10
+ @file
11
+ end
12
+
13
+ def method_missing(method, *args)
14
+ @logger.send(method, *args)
15
+ end
16
+
17
+ private
18
+
19
+ def log_level(level)
20
+ {'fatal' => Logger::FATAL, 'error' => Logger::ERROR, 'warn' => Logger::WARN, 'info' => Logger::INFO, 'debug' => Logger::DEBUG}[level] || Logger::INFO
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,214 @@
1
+ module Nanite
2
+ # Mappers are control nodes in nanite clusters. Nanite clusters
3
+ # can follow peer-to-peer model of communication as well as client-server,
4
+ # and mappers are nodes that know who to send work requests to agents.
5
+ #
6
+ # Mappers can reside inside a front end web application written in Merb/Rails
7
+ # and distribute heavy lifting to actors that register with the mapper as soon
8
+ # as they go online.
9
+ #
10
+ # Each mapper tracks nanites registered with it. It periodically checks
11
+ # when the last time a certain nanite sent a heartbeat notification,
12
+ # and removes those that have timed out from the list of available workers.
13
+ # As soon as a worker goes back online again it re-registers itself
14
+ # and the mapper adds it to the list and makes it available to
15
+ # be called again.
16
+ #
17
+ # This makes Nanite clusters self-healing and immune to individual node
18
+ # failures.
19
+ class Mapper
20
+ include AMQPHelper
21
+ include ConsoleHelper
22
+ include DaemonizeHelper
23
+
24
+ attr_reader :cluster, :identity, :job_warden, :options, :serializer, :log, :amq
25
+
26
+ DEFAULT_OPTIONS = COMMON_DEFAULT_OPTIONS.merge({:user => 'mapper', :identity => Identity.generate, :agent_timeout => 15,
27
+ :offline_redelivery_frequency => 10, :persistent => false, :offline_failsafe => false}) unless defined?(DEFAULT_OPTIONS)
28
+
29
+ # Initializes a new mapper and establishes
30
+ # AMQP connection. This must be used inside EM.run block or if EventMachine reactor
31
+ # is already started, for instance, by a Thin server that your Merb/Rails
32
+ # application runs on.
33
+ #
34
+ # Mapper options:
35
+ #
36
+ # identity : identity of this mapper, may be any string
37
+ #
38
+ # format : format to use for packets serialization. Can be :marshal, :json or :yaml.
39
+ # Defaults to Ruby's Marshall format. For interoperability with
40
+ # AMQP clients implemented in other languages, use JSON.
41
+ #
42
+ # Note that Nanite uses JSON gem,
43
+ # and ActiveSupport's JSON encoder may cause clashes
44
+ # if ActiveSupport is loaded after JSON gem.
45
+ #
46
+ # log_level : the verbosity of logging, can be debug, info, warn, error or fatal.
47
+ #
48
+ # agent_timeout : how long to wait before an agent is considered to be offline
49
+ # and thus removed from the list of available agents.
50
+ #
51
+ # log_dir : log file path, defaults to the current working directory.
52
+ #
53
+ # console : true tells mapper to start interactive console
54
+ #
55
+ # daemonize : true tells mapper to daemonize
56
+ #
57
+ # offline_redelivery_frequency : The frequency in seconds that messages stored in the offline queue will be retrieved
58
+ # for attempted redelivery to the nanites. Default is 10 seconds.
59
+ #
60
+ # persistent : true instructs the AMQP broker to save messages to persistent storage so that they aren't lost when the
61
+ # broker is restarted. Default is false. Can be overriden on a per-message basis using the request and push methods.
62
+ #
63
+ # secure : use Security features of rabbitmq to restrict nanites to themselves
64
+ #
65
+ # Connection options:
66
+ #
67
+ # vhost : AMQP broker vhost that should be used
68
+ #
69
+ # user : AMQP broker user
70
+ #
71
+ # pass : AMQP broker password
72
+ #
73
+ # host : host AMQP broker (or node of interest) runs on,
74
+ # defaults to 0.0.0.0
75
+ #
76
+ # port : port AMQP broker (or node of interest) runs on,
77
+ # this defaults to 5672, port used by some widely
78
+ # used AMQP brokers (RabbitMQ and ZeroMQ)
79
+ #
80
+ # @api :public:
81
+ def self.start(options = {})
82
+ new(options)
83
+ end
84
+
85
+ def initialize(options)
86
+ @options = DEFAULT_OPTIONS.merge(options)
87
+ @identity = "mapper-#{@options[:identity]}"
88
+ @log = Log.new(@options, @identity)
89
+ @serializer = Serializer.new(@options[:format])
90
+ daemonize if @options[:daemonize]
91
+ @amq =start_amqp(@options)
92
+ @cluster = Cluster.new(@amq, @options[:agent_timeout], @options[:identity], @log, @serializer)
93
+ @job_warden = JobWarden.new(@serializer, @log)
94
+ @log.info('starting mapper')
95
+ setup_queues
96
+ start_console if @options[:console] && !@options[:daemonize]
97
+ end
98
+
99
+ # Make a nanite request which expects a response.
100
+ #
101
+ # ==== Parameters
102
+ # type<String>:: The dispatch route for the request
103
+ # payload<Object>:: Payload to send. This will get marshalled en route
104
+ #
105
+ # ==== Options
106
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
107
+ # :least_loaded:: Pick the nanite which has the lowest load.
108
+ # :all:: Send the request to all nanites which respond to the service.
109
+ # :random:: Randomly pick a nanite.
110
+ # :rr: Select a nanite according to round robin ordering.
111
+ # :target<String>:: Select a specific nanite via identity, rather than using
112
+ # a selector.
113
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
114
+ # the nanites are offline. Messages will be redelivered when nanites come online.
115
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
116
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
117
+ # storage so that it isnt lost when the broker is restarted.
118
+ # Default is false unless the mapper was started with the --persistent flag.
119
+ #
120
+ # ==== Block Parameters
121
+ # :results<Object>:: The returned value from the nanite actor.
122
+ #
123
+ # @api :public:
124
+ def request(type, payload = '', opts = {}, &blk)
125
+ request = build_deliverable(Request, type, payload, opts)
126
+ request.reply_to = identity
127
+ targets = cluster.targets_for(request)
128
+ if !targets.empty?
129
+ job = job_warden.new_job(request, targets, blk)
130
+ cluster.route(request, job.targets)
131
+ job
132
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
133
+ cluster.publish(request, 'mapper-offline')
134
+ :offline
135
+ else
136
+ false
137
+ end
138
+ end
139
+
140
+ # Make a nanite request which does not expect a response.
141
+ #
142
+ # ==== Parameters
143
+ # type<String>:: The dispatch route for the request
144
+ # payload<Object>:: Payload to send. This will get marshalled en route
145
+ #
146
+ # ==== Options
147
+ # :selector<Symbol>:: Method for selecting an actor. Default is :least_loaded.
148
+ # :least_loaded:: Pick the nanite which has the lowest load.
149
+ # :all:: Send the request to all nanites which respond to the service.
150
+ # :random:: Randomly pick a nanite.
151
+ # :rr: Select a nanite according to round robin ordering.
152
+ # :offline_failsafe<Boolean>:: Store messages in an offline queue when all
153
+ # the nanites are offline. Messages will be redelivered when nanites come online.
154
+ # Default is false unless the mapper was started with the --offline-failsafe flag.
155
+ # :persistent<Boolean>:: Instructs the AMQP broker to save the message to persistent
156
+ # storage so that it isnt lost when the broker is restarted.
157
+ # Default is false unless the mapper was started with the --persistent flag.
158
+ #
159
+ # @api :public:
160
+ def push(type, payload = '', opts = {})
161
+ push = build_deliverable(Push, type, payload, opts)
162
+ targets = cluster.targets_for(push)
163
+ if !targets.empty?
164
+ cluster.route(push, targets)
165
+ true
166
+ elsif opts.key?(:offline_failsafe) ? opts[:offline_failsafe] : options[:offline_failsafe]
167
+ cluster.publish(push, 'mapper-offline')
168
+ :offline
169
+ else
170
+ false
171
+ end
172
+ end
173
+
174
+ private
175
+
176
+ def build_deliverable(deliverable_type, type, payload, opts)
177
+ deliverable = deliverable_type.new(type, payload, opts)
178
+ deliverable.from = identity
179
+ deliverable.token = Identity.generate
180
+ deliverable.persistent = opts.key?(:persistent) ? opts[:persistent] : options[:persistent]
181
+ deliverable
182
+ end
183
+
184
+ def setup_queues
185
+ setup_offline_queue
186
+ setup_message_queue
187
+ end
188
+
189
+ def setup_offline_queue
190
+ offline_queue = amq.queue('mapper-offline', :durable => true)
191
+ offline_queue.subscribe(:ack => true) do |info, deliverable|
192
+ deliverable = serializer.load(deliverable)
193
+ targets = cluster.targets_for(deliverable)
194
+ unless targets.empty?
195
+ info.ack
196
+ if deliverable.kind_of?(Request)
197
+ deliverable.reply_to = identity
198
+ job_warden.new_job(deliverable, targets)
199
+ end
200
+ cluster.route(deliverable, targets)
201
+ end
202
+ end
203
+
204
+ EM.add_periodic_timer(options[:offline_redelivery_frequency]) { offline_queue.recover }
205
+ end
206
+
207
+ def setup_message_queue
208
+ amq.queue(identity, :exclusive => true).bind(amq.fanout(identity)).subscribe do |msg|
209
+ job_warden.process(msg)
210
+ end
211
+ end
212
+ end
213
+ end
214
+
@@ -0,0 +1,192 @@
1
+ module Nanite
2
+ # Base class for all Nanite packets,
3
+ # knows how to dump itself to JSON
4
+ class Packet
5
+ def initialize
6
+ raise NotImplementedError.new("#{self.class.name} is an abstract class.")
7
+ end
8
+ def to_json(*a)
9
+ {
10
+ 'json_class' => self.class.name,
11
+ 'data' => instance_variables.inject({}) {|m,ivar| m[ivar.sub(/@/,'')] = instance_variable_get(ivar); m }
12
+ }.to_json(*a)
13
+ end
14
+ end
15
+
16
+ # packet that means start of a file transfer
17
+ # operation
18
+ class FileStart < Packet
19
+ attr_accessor :filename, :token, :dest
20
+ def initialize(filename, dest, token)
21
+ @filename = filename
22
+ @dest = dest
23
+ @token = token
24
+ end
25
+
26
+ def self.json_create(o)
27
+ i = o['data']
28
+ new(i['filename'], i['dest'], i['token'])
29
+ end
30
+ end
31
+
32
+ # packet that means end of a file transfer
33
+ # operation
34
+ class FileEnd < Packet
35
+ attr_accessor :token, :meta
36
+ def initialize(token, meta)
37
+ @token = token
38
+ @meta = meta
39
+ end
40
+
41
+ def self.json_create(o)
42
+ i = o['data']
43
+ new(i['token'], i['meta'])
44
+ end
45
+ end
46
+
47
+ # packet that carries data chunks during a file transfer
48
+ class FileChunk < Packet
49
+ attr_accessor :chunk, :token
50
+ def initialize(token, chunk=nil)
51
+ @chunk = chunk
52
+ @token = token
53
+ end
54
+ def self.json_create(o)
55
+ i = o['data']
56
+ new(i['token'], i['chunk'])
57
+ end
58
+ end
59
+
60
+ # packet that means a work request from mapper
61
+ # to actor node
62
+ #
63
+ # type is a service name
64
+ # payload is arbitrary data that is transferred from mapper to actor
65
+ #
66
+ # Options:
67
+ # from is sender identity
68
+ # token is a generated request id that mapper uses to identify replies
69
+ # reply_to is identity of the node actor replies to, usually a mapper itself
70
+ # selector is the selector used to route the request
71
+ # target is the target nanite for the request
72
+ # persistent signifies if this request should be saved to persistent storage by the AMQP broker
73
+ class Request < Packet
74
+ attr_accessor :from, :payload, :type, :token, :reply_to, :selector, :target, :persistent
75
+ DEFAULT_OPTIONS = {:selector => :least_loaded}
76
+ def initialize(type, payload, opts={})
77
+ opts = DEFAULT_OPTIONS.merge(opts)
78
+ @type = type
79
+ @payload = payload
80
+ @from = opts[:from]
81
+ @token = opts[:token]
82
+ @reply_to = opts[:reply_to]
83
+ @selector = opts[:selector]
84
+ @target = opts[:target]
85
+ @persistent = opts[:persistent]
86
+ end
87
+ def self.json_create(o)
88
+ i = o['data']
89
+ new(i['type'], i['payload'], {:from => i['from'], :token => i['token'], :reply_to => i['reply_to'], :selector => i['selector'],
90
+ :target => i['target'], :persistent => i['persistent']})
91
+ end
92
+ end
93
+
94
+ # packet that means a work push from mapper
95
+ # to actor node
96
+ #
97
+ # type is a service name
98
+ # payload is arbitrary data that is transferred from mapper to actor
99
+ #
100
+ # Options:
101
+ # from is sender identity
102
+ # token is a generated request id that mapper uses to identify replies
103
+ # selector is the selector used to route the request
104
+ # target is the target nanite for the request
105
+ # persistent signifies if this request should be saved to persistent storage by the AMQP broker
106
+ class Push < Packet
107
+ attr_accessor :from, :payload, :type, :token, :selector, :target, :persistent
108
+ DEFAULT_OPTIONS = {:selector => :least_loaded}
109
+ def initialize(type, payload, opts={})
110
+ opts = DEFAULT_OPTIONS.merge(opts)
111
+ @type = type
112
+ @payload = payload
113
+ @from = opts[:from]
114
+ @token = opts[:token]
115
+ @selector = opts[:selector]
116
+ @target = opts[:target]
117
+ @persistent = opts[:persistent]
118
+ end
119
+ def self.json_create(o)
120
+ i = o['data']
121
+ new(i['type'], i['payload'], {:from => i['from'], :token => i['token'], :selector => i['selector'],
122
+ :target => i['target'], :persistent => i['persistent']})
123
+ end
124
+ end
125
+
126
+ # packet that means a work result notification sent from actor to mapper
127
+ #
128
+ # from is sender identity
129
+ # results is arbitrary data that is transferred from actor, a result of actor's work
130
+ # token is a generated request id that mapper uses to identify replies
131
+ # to is identity of the node result should be delivered to
132
+ class Result < Packet
133
+ attr_accessor :token, :results, :to, :from
134
+ def initialize(token, to, results, from)
135
+ @token = token
136
+ @to = to
137
+ @from = from
138
+ @results = results
139
+ end
140
+ def self.json_create(o)
141
+ i = o['data']
142
+ new(i['token'], i['to'], i['results'], i['from'])
143
+ end
144
+ end
145
+
146
+ # packet that means an availability notification sent from actor to mapper
147
+ #
148
+ # from is sender identity
149
+ # services is a list of services provided by the node
150
+ # status is a load of the node by default, but may be any criteria
151
+ # agent may use to report it's availability, load, etc
152
+ class Register < Packet
153
+ attr_accessor :identity, :services, :status
154
+ def initialize(identity, services, status)
155
+ @status = status
156
+ @identity = identity
157
+ @services = services
158
+ end
159
+ def self.json_create(o)
160
+ i = o['data']
161
+ new(i['identity'], i['services'], i['status'])
162
+ end
163
+ end
164
+
165
+ # heartbeat packet
166
+ #
167
+ # identity is sender's identity
168
+ # status is sender's status (see Register packet documentation)
169
+ class Ping < Packet
170
+ attr_accessor :identity, :status
171
+ def initialize(identity, status)
172
+ @status = status
173
+ @identity = identity
174
+ end
175
+ def self.json_create(o)
176
+ i = o['data']
177
+ new(i['identity'], i['status'])
178
+ end
179
+ end
180
+
181
+ # packet that is sent by workers to the mapper
182
+ # when worker initially comes online to advertise
183
+ # it's services
184
+ class Advertise < Packet
185
+ def initialize
186
+ end
187
+ def self.json_create(o)
188
+ new
189
+ end
190
+ end
191
+ end
192
+