magent 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +37 -1
- data/Rakefile +48 -20
- data/VERSION +1 -0
- data/bin/magent +31 -14
- data/examples/comm/worker.rb +1 -1
- data/examples/error/error.rb +1 -1
- data/examples/mongomapper/async.rb +41 -0
- data/examples/simple/bot.rb +2 -1
- data/examples/stats/stats.rb +1 -1
- data/lib/magent.rb +80 -28
- data/lib/magent/actor.rb +5 -1
- data/lib/magent/actor_channel.rb +34 -0
- data/lib/magent/async.rb +65 -0
- data/lib/magent/async_channel.rb +38 -0
- data/lib/magent/{channel.rb → failure.rb} +5 -9
- data/lib/magent/generic_channel.rb +7 -9
- data/lib/magent/processor.rb +25 -39
- data/lib/magent/push.rb +2 -2
- data/lib/magent/railtie.rb +10 -0
- data/lib/magent/web_socket_channel.rb +18 -0
- data/lib/magent/web_socket_server.rb +95 -0
- data/lib/tasks/magent.rake +55 -0
- data/magent.gemspec +73 -22
- metadata +49 -40
data/lib/magent/actor.rb
CHANGED
@@ -28,7 +28,7 @@ module Magent
|
|
28
28
|
|
29
29
|
def channel
|
30
30
|
@channel ||= begin
|
31
|
-
|
31
|
+
ActorChannel.new(self.channel_name)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -75,4 +75,8 @@ module Magent
|
|
75
75
|
def self.current_actor
|
76
76
|
@current_actor
|
77
77
|
end
|
78
|
+
|
79
|
+
def self.current_channel
|
80
|
+
self.current_actor.channel
|
81
|
+
end
|
78
82
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Magent
|
2
|
+
class ActorChannel < GenericChannel
|
3
|
+
def push(message, args)
|
4
|
+
enqueue([message, args])
|
5
|
+
end
|
6
|
+
|
7
|
+
def process!(message)
|
8
|
+
@actor = Magent.current_actor
|
9
|
+
|
10
|
+
@method, @payload = message
|
11
|
+
return false if @method.nil?
|
12
|
+
|
13
|
+
sucess = @actor._run_tasks
|
14
|
+
|
15
|
+
$stderr.puts "#{@actor.class}##{@method}(#{@payload.inspect})"
|
16
|
+
if @actor.class.can_handle?(@method)
|
17
|
+
@actor.send(@method, @payload)
|
18
|
+
return true
|
19
|
+
else
|
20
|
+
$stderr.puts "Unknown action: #{@method} (payload=#{@payload.inspect})"
|
21
|
+
end
|
22
|
+
|
23
|
+
@method, @payload = nil
|
24
|
+
|
25
|
+
sucess
|
26
|
+
end
|
27
|
+
|
28
|
+
def on_shutdown
|
29
|
+
if @method
|
30
|
+
self.enqueue(@method, @payload)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end # Channel
|
34
|
+
end
|
data/lib/magent/async.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Example
|
2
|
+
# class Processor
|
3
|
+
# include Magent::Async
|
4
|
+
# [..snip..]
|
5
|
+
#
|
6
|
+
# def process
|
7
|
+
# puts "Processing #{@params}"
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# async call:
|
12
|
+
# Processor.find(id).async(:my_queue).process.commit!(priority)
|
13
|
+
# chained methods:
|
14
|
+
# Processor.async(:my_queue, true).find(1).process.commit!(priority)
|
15
|
+
#
|
16
|
+
|
17
|
+
module Magent
|
18
|
+
module Async
|
19
|
+
def self.included(base)
|
20
|
+
base.class_eval do
|
21
|
+
include Methods
|
22
|
+
extend Methods
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Proxy
|
27
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
|
28
|
+
|
29
|
+
def initialize(queue, target, test = false)
|
30
|
+
@queue = queue
|
31
|
+
@method_chain = []
|
32
|
+
@target = target
|
33
|
+
@test = test
|
34
|
+
|
35
|
+
@channel = Magent::AsyncChannel.new(@queue)
|
36
|
+
end
|
37
|
+
|
38
|
+
def commit!(priority = 3)
|
39
|
+
@channel.push(@target, @method_chain, priority)
|
40
|
+
|
41
|
+
if @test
|
42
|
+
target = @target
|
43
|
+
@method_chain.each do |c|
|
44
|
+
target = target.send(c[0], *c[1])
|
45
|
+
end
|
46
|
+
|
47
|
+
target
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def method_missing(m, *args, &blk)
|
52
|
+
raise ArgumentError, "ruby blocks are not supported yet" if !blk.nil?
|
53
|
+
@method_chain << [m, args]
|
54
|
+
self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Methods
|
59
|
+
# @question.async(:judge).on_view_question.commit!(1)
|
60
|
+
def async(queue = :default, test = false)
|
61
|
+
Magent::Async::Proxy.new(queue, self, test)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # Async
|
65
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Magent
|
2
|
+
class AsyncChannel < GenericChannel
|
3
|
+
def push(target, method_chain, priority)
|
4
|
+
if target.kind_of?(Class)
|
5
|
+
enqueue([target.to_s, nil, method_chain], priority)
|
6
|
+
elsif target.class.respond_to?(:find) && target.respond_to?(:id)
|
7
|
+
enqueue([target.class.to_s, target.id, method_chain], priority)
|
8
|
+
else
|
9
|
+
raise ArgumentError, "I don't know how to handle #{target.inspect}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def process!(message)
|
14
|
+
klass, id, method_chain = message
|
15
|
+
|
16
|
+
return false if method_chain.nil?
|
17
|
+
|
18
|
+
target = resolve_target(klass, id)
|
19
|
+
|
20
|
+
method_chain.each do |c|
|
21
|
+
target = target.send(c[0], *c[1])
|
22
|
+
end
|
23
|
+
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def resolve_target(klass, id)
|
29
|
+
klass = Object.module_eval(klass) if klass.kind_of?(String)
|
30
|
+
|
31
|
+
if id
|
32
|
+
klass.find(id)
|
33
|
+
else
|
34
|
+
klass
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end # AsyncChannel
|
38
|
+
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
module Magent
|
2
|
-
|
3
|
-
def enqueue(message, args)
|
4
|
-
super([message, args])
|
5
|
-
end
|
6
|
-
|
2
|
+
module Failure
|
7
3
|
def failed(info)
|
8
|
-
error_collection.save(info.merge({:_id => generate_uid, :channel => @name, :created_at => Time.now.utc}))
|
4
|
+
error_collection.save(info.merge({:_id => generate_uid, :channel => @name, :channel_class => self.class.to_s, :created_at => Time.now.utc}))
|
9
5
|
end
|
10
6
|
|
11
7
|
def error_count
|
@@ -26,12 +22,12 @@ module Magent
|
|
26
22
|
end
|
27
23
|
|
28
24
|
def retry_error(error)
|
25
|
+
process!(error["message"])
|
29
26
|
remove_error(error["_id"])
|
30
|
-
enqueue(error["method"], error["payload"])
|
31
27
|
end
|
32
28
|
|
33
29
|
def error_collection
|
34
|
-
@error_collection ||= Magent.database.collection("#{@name}
|
30
|
+
@error_collection ||= Magent.database.collection("#{@name}.errors")
|
35
31
|
end
|
36
|
-
end
|
32
|
+
end
|
37
33
|
end
|
@@ -1,13 +1,15 @@
|
|
1
1
|
module Magent
|
2
2
|
class GenericChannel
|
3
|
+
include Magent::Failure
|
4
|
+
|
3
5
|
attr_reader :name
|
4
6
|
|
5
7
|
def initialize(name)
|
6
|
-
@name = name
|
8
|
+
@name = "magent.#{name}"
|
7
9
|
end
|
8
10
|
|
9
|
-
def enqueue(message)
|
10
|
-
collection.save({:_id => generate_uid, :message => message, :priority =>
|
11
|
+
def enqueue(message, priority = 3)
|
12
|
+
collection.save({:_id => generate_uid, :message => message, :priority => priority, :created_at => Time.now.to_i})
|
11
13
|
end
|
12
14
|
|
13
15
|
def message_count
|
@@ -25,12 +27,8 @@ module Magent
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def next_message
|
28
|
-
|
29
|
-
|
30
|
-
:remove, true
|
31
|
-
]) rescue {}
|
32
|
-
|
33
|
-
rec["value"]
|
30
|
+
collection.find_and_modify(:sort => [[:priority, Mongo::ASCENDING], [:created_at, Mongo::DESCENDING]],
|
31
|
+
:remove => true) rescue {}
|
34
32
|
end
|
35
33
|
|
36
34
|
def collection
|
data/lib/magent/processor.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
module Magent
|
2
2
|
class Processor
|
3
|
-
|
4
|
-
|
5
|
-
def initialize(actor)
|
6
|
-
@actor = actor
|
3
|
+
def initialize(channel)
|
4
|
+
@channel = channel
|
7
5
|
@shutdown = false
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
7
|
+
# @actor.class.actions.each do |action|
|
8
|
+
# if !@actor.respond_to?(action)
|
9
|
+
# raise ArgumentError, "action '#{action}' is not defined"
|
10
|
+
# end
|
11
|
+
# end
|
14
12
|
end
|
15
13
|
|
16
14
|
def run!
|
@@ -23,46 +21,34 @@ module Magent
|
|
23
21
|
loop do
|
24
22
|
break if @shutdown
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
begin
|
36
|
-
if @actor.class.can_handle?(@method)
|
37
|
-
processed_messages += 1
|
38
|
-
@actor.send(@method, @payload)
|
39
|
-
|
40
|
-
if processed_messages > 20
|
41
|
-
processed_messages = 0
|
42
|
-
GC.start
|
43
|
-
end
|
44
|
-
else
|
45
|
-
$stderr.puts "Unknown action: #{@method} (payload=#{@payload.inspect})"
|
24
|
+
message = @channel.dequeue
|
25
|
+
begin
|
26
|
+
if message && @channel.process!(message)
|
27
|
+
puts "Processed #{message.inspect}"
|
28
|
+
delay = 0
|
29
|
+
processed_messages += 1
|
30
|
+
if processed_messages > 20
|
31
|
+
processed_messages = 0
|
32
|
+
GC.start
|
46
33
|
end
|
47
|
-
|
48
|
-
|
49
|
-
$stderr.puts "Error while executing #{@method.inspect} #{@payload.inspect}"
|
50
|
-
$stderr.puts "#{e.to_s}\n#{e.backtrace.join("\t\n")}"
|
51
|
-
@actor.class.channel.failed(:message => e.message, :method => @method, :payload => @payload, :backtrace => e.backtrace, :date => Time.now.utc)
|
52
|
-
ensure
|
53
|
-
@method, @payload = nil
|
34
|
+
else
|
35
|
+
delay += 0.1 if delay <= 5
|
54
36
|
end
|
37
|
+
rescue SystemExit
|
38
|
+
rescue Exception => e
|
39
|
+
$stderr.puts "Error processing #{message.inspect} => #{e.message}"
|
40
|
+
@channel.failed(:error => e.message, :message => message, :backtrace => e.backtrace, :date => Time.now.utc)
|
41
|
+
ensure
|
55
42
|
end
|
43
|
+
|
56
44
|
sleep delay
|
57
45
|
end
|
58
46
|
end
|
59
47
|
|
60
48
|
def shutdown!
|
61
49
|
@shutdown = true
|
50
|
+
@channel.on_shutdown if @channel.respond_to?(:on_shutdown)
|
62
51
|
$stderr.puts "Shutting down..."
|
63
|
-
if @method
|
64
|
-
@actor.class.channel.enqueue(@method, @payload)
|
65
|
-
end
|
66
52
|
end
|
67
53
|
end #Processor
|
68
54
|
end
|
data/lib/magent/push.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
module Magent
|
2
2
|
def self.push(channel_name, method, *args)
|
3
|
-
self.channel(channel_name.to_s).
|
3
|
+
self.channel(channel_name.to_s).push(method, args)
|
4
4
|
end
|
5
5
|
|
6
6
|
def self.channel(name)
|
7
|
-
self.channels[name] ||=
|
7
|
+
self.channels[name] ||= ActorChannel.new(name)
|
8
8
|
end
|
9
9
|
|
10
10
|
def self.channels
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Magent
|
2
|
+
class WebSocketChannel < Magent::GenericChannel
|
3
|
+
def self.push(message)
|
4
|
+
|
5
|
+
self.instance.enqueue(message)
|
6
|
+
|
7
|
+
self.instance
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.next_message
|
11
|
+
self.instance.next_message
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.instance
|
15
|
+
@channel ||= self.new(Magent.config["websocket_channel"]||"magent.websocket")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'magent/web_socket_channel'
|
2
|
+
module Magent
|
3
|
+
class WebSocketServer
|
4
|
+
def initialize(options = {})
|
5
|
+
options = {:host => "0.0.0.0", :port => 34567}.merge(options)
|
6
|
+
|
7
|
+
$stdout.puts ">> Server running and up! #{options}}" if options[:debug]
|
8
|
+
|
9
|
+
EventMachine.run do
|
10
|
+
@channel = EM::Channel.new
|
11
|
+
@channels = {}
|
12
|
+
@channel_ids = {}
|
13
|
+
@sids = {}
|
14
|
+
|
15
|
+
EventMachine.add_periodic_timer(options.delete(:interval)||10) do
|
16
|
+
while v = Magent::WebSocketChannel.next_message
|
17
|
+
message = v["message"]
|
18
|
+
if message && (channel = @channels[message["channel_id"]])
|
19
|
+
channel.push(message.to_json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
EventMachine::WebSocket.start(options) do |ws|
|
25
|
+
ws.onopen do
|
26
|
+
ws.onmessage do |msg|
|
27
|
+
data = JSON.parse(msg) rescue {}
|
28
|
+
|
29
|
+
case data["id"]
|
30
|
+
when 'start'
|
31
|
+
if data["channel_id"].present? && (channel_id = validate_channel_id(data["channel_id"]))
|
32
|
+
key = generate_unique_key(data["key"])
|
33
|
+
@channels[channel_id] ||= EM::Channel.new
|
34
|
+
@channel_ids[key] = channel_id
|
35
|
+
|
36
|
+
sid = @channels[channel_id].subscribe { |msg| ws.send(msg) }
|
37
|
+
|
38
|
+
@sids[key] = sid
|
39
|
+
ws.onclose do
|
40
|
+
@channel_ids.delete(key)
|
41
|
+
@channels[channel_id].unsubscribe(sid)
|
42
|
+
@sids.delete(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
ws.send({:id => "ack", :key => key}.to_json)
|
46
|
+
else
|
47
|
+
ws.close_connection
|
48
|
+
end
|
49
|
+
when 'chatmessage'
|
50
|
+
key = data["key"]
|
51
|
+
return invalid_key if key.blank? || @sids[key].blank?
|
52
|
+
|
53
|
+
channel_id = @channel_ids[key]
|
54
|
+
|
55
|
+
if channel_id
|
56
|
+
@channels[channel_id].push({:id => 'chatmessage', :from => user_name(key, @sids[key]), :message => data["message"]}.to_json)
|
57
|
+
else
|
58
|
+
ws.send({:id => 'announcement', :type => "error", :message => "cannot find the channel"}.to_json)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
handle_message(ws, data)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end # EM::WebSocket
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.start(options = {})
|
70
|
+
self.new(options)
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
def invalid_key(ws)
|
75
|
+
ws.send({:id => 'announcement', :type => "error", :message => "you must provide your unique key"}.to_json)
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_message(ws, data)
|
79
|
+
ws.send({:id => 'announcement', :type => "error", :message => "unhandled message: #{data.inspect}"}.to_json)
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
def generate_unique_key(default = nil)
|
84
|
+
default || UUIDTools::UUID.random_create.hexdigest
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_channel_id(id)
|
88
|
+
return id if !id.blank?
|
89
|
+
end
|
90
|
+
|
91
|
+
def user_name(socket_key, sid)
|
92
|
+
"Guest #{sid}"
|
93
|
+
end
|
94
|
+
end # Class
|
95
|
+
end
|