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.
@@ -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
+