magent 0.4.2 → 0.5.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/.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
|