revactor 0.1.0 → 0.1.1
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/CHANGES +22 -0
- data/README +13 -49
- data/examples/echo_server.rb +19 -23
- data/examples/google.rb +13 -15
- data/examples/mongrel.rb +1 -1
- data/lib/revactor.rb +15 -7
- data/lib/revactor/actor.rb +75 -225
- data/lib/revactor/delegator.rb +70 -0
- data/lib/revactor/mailbox.rb +159 -0
- data/lib/revactor/mongrel.rb +3 -3
- data/lib/revactor/scheduler.rb +65 -0
- data/lib/revactor/tcp.rb +13 -5
- data/revactor.gemspec +4 -4
- data/spec/actor_spec.rb +13 -32
- data/spec/delegator_spec.rb +46 -0
- data/spec/tcp_spec.rb +24 -44
- data/tools/messaging_throughput.rb +10 -12
- metadata +8 -7
- data/lib/revactor/behaviors/server.rb +0 -87
- data/lib/revactor/server.rb +0 -153
@@ -0,0 +1,70 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../revactor'
|
8
|
+
|
9
|
+
# A Delegator whose delegate runs in a separate Actor. This allows
|
10
|
+
# an easy method for constructing synchronous calls to a separate Actor
|
11
|
+
# which cause the current Actor to block until they complete.
|
12
|
+
class Revactor::Delegator
|
13
|
+
# Create a new delegator for the given object
|
14
|
+
def initialize(obj)
|
15
|
+
@obj = obj
|
16
|
+
@running = true
|
17
|
+
@actor = Actor.spawn(&method(:start))
|
18
|
+
end
|
19
|
+
|
20
|
+
# Stop the delegator Actor
|
21
|
+
def stop
|
22
|
+
@actor << :stop
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
# Send a message to the Actor delegate
|
27
|
+
def send(meth, *args, &block)
|
28
|
+
@actor << T[:call, Actor.current, meth, args, block]
|
29
|
+
Actor.receive do |filter|
|
30
|
+
filter.when(Case[:call_reply, @actor, Object]) { |_, _, reply| reply }
|
31
|
+
filter.when(Case[:call_error, @actor, Object]) { |_, _, ex| raise ex }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :method_missing, :send
|
36
|
+
|
37
|
+
#########
|
38
|
+
protected
|
39
|
+
#########
|
40
|
+
|
41
|
+
# Start the server
|
42
|
+
def start
|
43
|
+
loop do
|
44
|
+
Actor.receive do |filter|
|
45
|
+
filter.when(:stop) { |_| return }
|
46
|
+
filter.when(Object) { |message| handle_message(message) }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Dispatch the incoming message to the appropriate handler
|
52
|
+
def handle_message(message)
|
53
|
+
case message.first
|
54
|
+
when :call then handle_call(message)
|
55
|
+
else @obj.__send__(:on_message, message) if @obj.respond_to? :on_message
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Wrapper for calling the provided object's handle_call method
|
60
|
+
def handle_call(message)
|
61
|
+
_, from, meth, args, block = message
|
62
|
+
|
63
|
+
begin
|
64
|
+
result = @obj.__send__(meth, *args, &block)
|
65
|
+
from << T[:call_reply, Actor.current, result]
|
66
|
+
rescue Exception => ex
|
67
|
+
from << T[:call_error, Actor.current, ex]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../revactor'
|
8
|
+
|
9
|
+
class Actor
|
10
|
+
# Actor mailbox. For purposes of efficiency the mailbox also handles
|
11
|
+
# suspending and resuming an actor when no messages match its filter set.
|
12
|
+
class Mailbox
|
13
|
+
attr_accessor :timer
|
14
|
+
attr_accessor :timed_out
|
15
|
+
attr_accessor :timeout_action
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@timer = nil
|
19
|
+
@queue = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add a message to the mailbox queue
|
23
|
+
def <<(message)
|
24
|
+
@queue << message
|
25
|
+
end
|
26
|
+
|
27
|
+
# Attempt to receive a message
|
28
|
+
def receive
|
29
|
+
raise ArgumentError, "no filter block given" unless block_given?
|
30
|
+
|
31
|
+
# Clear mailbox processing variables
|
32
|
+
action = matched_index = nil
|
33
|
+
processed_upto = 0
|
34
|
+
|
35
|
+
# Clear timeout variables
|
36
|
+
@timed_out = false
|
37
|
+
@timeout_action = nil
|
38
|
+
|
39
|
+
# Build the filter
|
40
|
+
filter = Filter.new(self)
|
41
|
+
yield filter
|
42
|
+
raise ArgumentError, "empty filter" if filter.empty?
|
43
|
+
|
44
|
+
# Process incoming messages
|
45
|
+
while action.nil?
|
46
|
+
@queue[processed_upto..@queue.size].each_with_index do |message, index|
|
47
|
+
unless (action = filter.match message)
|
48
|
+
# The filter did not match an action for the current message
|
49
|
+
# Keep track of which messages we've ran the filter across so it
|
50
|
+
# isn't re-run against messages it already failed to match
|
51
|
+
processed_upto += 1
|
52
|
+
next
|
53
|
+
end
|
54
|
+
|
55
|
+
# We've found a matching action, so break out of the loop
|
56
|
+
matched_index = processed_upto + index
|
57
|
+
break
|
58
|
+
end
|
59
|
+
|
60
|
+
# Ignore timeouts if we've matched a message
|
61
|
+
if action
|
62
|
+
@timed_out = false
|
63
|
+
break
|
64
|
+
end
|
65
|
+
|
66
|
+
# Otherwise run the timeout action
|
67
|
+
action = @timeout_action if @timed_out
|
68
|
+
|
69
|
+
# If no matching action is found, reschedule until we get another message
|
70
|
+
Actor.reschedule unless action
|
71
|
+
end
|
72
|
+
|
73
|
+
@timeout_action = nil
|
74
|
+
|
75
|
+
if @timer
|
76
|
+
@timer.detach if @timer.attached?
|
77
|
+
@timer = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# If we encountered a timeout, call the action directly
|
81
|
+
if @timed_out
|
82
|
+
@timed_out = false
|
83
|
+
return action.call
|
84
|
+
end
|
85
|
+
|
86
|
+
# Otherwise we matched a message, so process it with the action
|
87
|
+
action.(@queue.delete_at matched_index)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Is the mailbox empty?
|
91
|
+
def empty?
|
92
|
+
@queue.empty?
|
93
|
+
end
|
94
|
+
|
95
|
+
#######
|
96
|
+
private
|
97
|
+
#######
|
98
|
+
|
99
|
+
# Timeout class, used to implement receive timeouts
|
100
|
+
class Timer < Rev::TimerWatcher
|
101
|
+
def initialize(seconds, actor)
|
102
|
+
@actor = actor
|
103
|
+
super(seconds)
|
104
|
+
end
|
105
|
+
|
106
|
+
def on_timer
|
107
|
+
detach
|
108
|
+
@actor.mailbox.timed_out = true
|
109
|
+
@actor.scheduler << @actor
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Mailbox filterset. Takes patterns or procs to match messages with
|
114
|
+
# and returns the associated proc when a pattern matches.
|
115
|
+
class Filter
|
116
|
+
def initialize(mailbox)
|
117
|
+
@mailbox = mailbox
|
118
|
+
@ruleset = []
|
119
|
+
end
|
120
|
+
|
121
|
+
# Provide a pattern to match against with === and a block to call
|
122
|
+
# when the pattern is matched.
|
123
|
+
def when(pattern, &action)
|
124
|
+
raise ArgumentError, "no block given" unless action
|
125
|
+
@ruleset << [pattern, action]
|
126
|
+
end
|
127
|
+
|
128
|
+
# Provide a timeout (in seconds, can be a Float) to wait for matching
|
129
|
+
# messages. If the timeout elapses, the given block is called.
|
130
|
+
def after(seconds, &action)
|
131
|
+
raise ArgumentError, "timeout already specified" if @mailbox.timer
|
132
|
+
raise ArgumentError, "must be zero or positive" if seconds < 0
|
133
|
+
|
134
|
+
# Don't explicitly require an action to be specified for a timeout
|
135
|
+
@mailbox.timeout_action = action || proc {}
|
136
|
+
|
137
|
+
if seconds > 0
|
138
|
+
@mailbox.timer = Timer.new(seconds, Actor.current).attach(Rev::Loop.default)
|
139
|
+
else
|
140
|
+
# No need to actually set a timer if the timeout is zero,
|
141
|
+
# just short-circuit waiting for one entirely...
|
142
|
+
@mailbox.timed_out = true
|
143
|
+
Actor.scheduler << Actor.current
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Match a message using the filter
|
148
|
+
def match(message)
|
149
|
+
_, action = @ruleset.find { |pattern, _| pattern === message }
|
150
|
+
action
|
151
|
+
end
|
152
|
+
|
153
|
+
# Is the filterset empty?
|
154
|
+
def empty?
|
155
|
+
@ruleset.empty? and not @mailbox.timer
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/revactor/mongrel.rb
CHANGED
@@ -32,12 +32,12 @@ module Mongrel
|
|
32
32
|
|
33
33
|
# Runs the thing. It returns the Actor the listener is running in.
|
34
34
|
def run
|
35
|
-
@acceptor = Actor.
|
35
|
+
@acceptor = Actor.spawn do
|
36
36
|
begin
|
37
37
|
while true
|
38
38
|
begin
|
39
39
|
client = @socket.accept
|
40
|
-
actor = Actor.
|
40
|
+
actor = Actor.spawn client, &method(:process_client)
|
41
41
|
actor[:started_on] = Time.now
|
42
42
|
rescue StopServer
|
43
43
|
break
|
@@ -59,4 +59,4 @@ module Mongrel
|
|
59
59
|
return @acceptor
|
60
60
|
end
|
61
61
|
end
|
62
|
-
end
|
62
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C)2007 Tony Arcieri
|
3
|
+
# You can redistribute this under the terms of the Ruby license
|
4
|
+
# See file LICENSE for details
|
5
|
+
#++
|
6
|
+
|
7
|
+
require File.dirname(__FILE__) + '/../revactor'
|
8
|
+
|
9
|
+
class Actor
|
10
|
+
# The Actor Scheduler maintains a run queue of actors with outstanding
|
11
|
+
# messages who have not yet processed their mailbox. If all actors have
|
12
|
+
# processed their mailboxes then the scheduler waits for any outstanding
|
13
|
+
# Rev events. If there are no active Rev watchers then the scheduler exits.
|
14
|
+
class Scheduler
|
15
|
+
def initialize
|
16
|
+
@queue = []
|
17
|
+
@running = false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Schedule an Actor to be executed, and run the scheduler if it isn't
|
21
|
+
# currently running
|
22
|
+
def <<(actor)
|
23
|
+
raise ArgumentError, "must be an Actor" unless actor.is_a? Actor
|
24
|
+
|
25
|
+
@queue << actor unless @queue.last == actor
|
26
|
+
|
27
|
+
unless @running
|
28
|
+
# Persist the fiber the scheduler runs in
|
29
|
+
@fiber ||= Fiber.new do
|
30
|
+
loop { run; Fiber.yield }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Resume the scheduler
|
34
|
+
@fiber.resume
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Run the scheduler
|
39
|
+
def run
|
40
|
+
return if @running
|
41
|
+
|
42
|
+
@running = true
|
43
|
+
default_loop = Rev::Loop.default
|
44
|
+
|
45
|
+
until @queue.empty? and not default_loop.has_active_watchers?
|
46
|
+
@queue.each do |actor|
|
47
|
+
begin
|
48
|
+
actor.fiber.resume
|
49
|
+
rescue FiberError # Fiber may have died since being scheduled
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@queue.clear
|
54
|
+
default_loop.run_once if default_loop.has_active_watchers?
|
55
|
+
end
|
56
|
+
|
57
|
+
@running = false
|
58
|
+
end
|
59
|
+
|
60
|
+
# Is the scheduler running?
|
61
|
+
def running?
|
62
|
+
@running
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/revactor/tcp.rb
CHANGED
@@ -31,15 +31,15 @@ module Revactor
|
|
31
31
|
socket.attach Rev::Loop.default
|
32
32
|
|
33
33
|
Actor.receive do |filter|
|
34
|
-
filter.when(Case[Object, socket]) do |message|
|
35
|
-
case message
|
34
|
+
filter.when(Case[Object, socket]) do |message, _|
|
35
|
+
case message
|
36
36
|
when :tcp_connected
|
37
37
|
return socket
|
38
38
|
when :tcp_connect_failed
|
39
39
|
raise ConnectError, "connection refused"
|
40
40
|
when :tcp_resolve_failed
|
41
41
|
raise ResolveError, "couldn't resolve #{host}"
|
42
|
-
else raise "unexpected message for #{socket.inspect}: #{message
|
42
|
+
else raise "unexpected message for #{socket.inspect}: #{message}"
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -102,6 +102,10 @@ module Revactor
|
|
102
102
|
@read_buffer = Rev::Buffer.new
|
103
103
|
end
|
104
104
|
|
105
|
+
def inspect
|
106
|
+
"#<#{self.class}:0x#{object_id.to_s(16)} #{@remote_host}:#{@remote_port}>"
|
107
|
+
end
|
108
|
+
|
105
109
|
# Enable or disable active mode data reception. State can be any
|
106
110
|
# of the following:
|
107
111
|
#
|
@@ -255,8 +259,8 @@ module Revactor
|
|
255
259
|
# Lookup filters referenced as symbols
|
256
260
|
def symbol_to_filter(filter)
|
257
261
|
case filter
|
258
|
-
when :line then Revactor::
|
259
|
-
when :packet then Revactor::
|
262
|
+
when :line then Revactor::Filter::Line
|
263
|
+
when :packet then Revactor::Filter::Packet
|
260
264
|
else raise ArgumentError, "unrecognized filter type: #{filter}"
|
261
265
|
end
|
262
266
|
end
|
@@ -342,6 +346,10 @@ module Revactor
|
|
342
346
|
@accepting = false
|
343
347
|
end
|
344
348
|
|
349
|
+
def inspect
|
350
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
351
|
+
end
|
352
|
+
|
345
353
|
# Change the default active setting for newly accepted connections
|
346
354
|
def active=(state)
|
347
355
|
unless [true, false, :once].include? state
|
data/revactor.gemspec
CHANGED
@@ -2,10 +2,10 @@ require 'rubygems'
|
|
2
2
|
|
3
3
|
GEMSPEC = Gem::Specification.new do |s|
|
4
4
|
s.name = "revactor"
|
5
|
-
s.version = "0.1.
|
5
|
+
s.version = "0.1.1"
|
6
6
|
s.authors = "Tony Arcieri"
|
7
7
|
s.email = "tony@medioh.com"
|
8
|
-
s.date = "2008-1-
|
8
|
+
s.date = "2008-1-28"
|
9
9
|
s.summary = "Revactor is an Actor implementation for writing high performance concurrent programs"
|
10
10
|
s.platform = Gem::Platform::RUBY
|
11
11
|
s.required_ruby_version = '>= 1.9.0'
|
@@ -14,8 +14,8 @@ GEMSPEC = Gem::Specification.new do |s|
|
|
14
14
|
s.files = Dir.glob("{lib,examples,tools,spec}/**/*") + ['Rakefile', 'revactor.gemspec']
|
15
15
|
|
16
16
|
# Dependencies
|
17
|
-
s.add_dependency("rev", ">= 0.1.
|
18
|
-
s.add_dependency("case", ">= 0.
|
17
|
+
s.add_dependency("rev", ">= 0.1.4")
|
18
|
+
s.add_dependency("case", ">= 0.4")
|
19
19
|
|
20
20
|
# RubyForge info
|
21
21
|
s.homepage = "http://revactor.org"
|
data/spec/actor_spec.rb
CHANGED
@@ -8,52 +8,33 @@ require File.dirname(__FILE__) + '/../lib/revactor/actor'
|
|
8
8
|
|
9
9
|
describe Actor do
|
10
10
|
describe "creation" do
|
11
|
-
it "creates
|
12
|
-
Actor.
|
13
|
-
Fiber.current.should be_an_instance_of(Actor)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
it "allows creation of new Actors with Actor.new" do
|
18
|
-
Actor.new do
|
19
|
-
Fiber.current.should be_an_instance_of(Actor)
|
20
|
-
end
|
11
|
+
it "lazily creates Actor.current" do
|
12
|
+
Actor.current.should be_an_instance_of(Actor)
|
21
13
|
end
|
22
14
|
|
23
15
|
it "allows creation of new Actors with Actor.spawn" do
|
16
|
+
root = Actor.current
|
17
|
+
|
24
18
|
Actor.spawn do
|
25
|
-
|
19
|
+
Actor.current.should be_an_instance_of(Actor)
|
20
|
+
Actor.current.should_not eql(root)
|
26
21
|
end
|
27
22
|
end
|
28
23
|
|
29
24
|
it "allows arguments to be passed when an Actor is created" do
|
30
|
-
|
31
|
-
|
32
|
-
[foo, bar, baz].should == [1, 2, 3]
|
33
|
-
end
|
25
|
+
Actor.spawn(1, 2, 3) do |foo, bar, baz|
|
26
|
+
[foo, bar, baz].should == [1, 2, 3]
|
34
27
|
end
|
35
28
|
end
|
36
29
|
end
|
37
30
|
|
38
|
-
describe "current" do
|
39
|
-
it "allows the current Actor to be retrieved" do
|
40
|
-
Actor.new do
|
41
|
-
Actor.current.should be_an_instance_of(Actor)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
it "disallows retrieving the current Actor unless the Actor environment is started" do
|
46
|
-
proc { Actor.current }.should raise_error(ActorError)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
31
|
describe "receive" do
|
51
32
|
before :each do
|
52
33
|
@actor_run = false
|
53
34
|
end
|
54
35
|
|
55
36
|
it "returns the value of the matching filter action" do
|
56
|
-
actor = Actor.
|
37
|
+
actor = Actor.spawn do
|
57
38
|
Actor.receive do |filter|
|
58
39
|
filter.when(:foo) { |message| :bar }
|
59
40
|
end.should == :bar
|
@@ -67,7 +48,7 @@ describe Actor do
|
|
67
48
|
end
|
68
49
|
|
69
50
|
it "filters messages with ===" do
|
70
|
-
actor = Actor.
|
51
|
+
actor = Actor.spawn do
|
71
52
|
results = []
|
72
53
|
3.times do
|
73
54
|
results << Actor.receive do |filter|
|
@@ -85,7 +66,7 @@ describe Actor do
|
|
85
66
|
end
|
86
67
|
|
87
68
|
it "times out if a message isn't received after the specifed interval" do
|
88
|
-
actor = Actor.
|
69
|
+
actor = Actor.spawn do
|
89
70
|
Actor.receive do |filter|
|
90
71
|
filter.when(:foo) { :wrong }
|
91
72
|
filter.after(0.01) { :right }
|
@@ -96,7 +77,7 @@ describe Actor do
|
|
96
77
|
end
|
97
78
|
|
98
79
|
it "matches any message with Object" do
|
99
|
-
actor = Actor.
|
80
|
+
actor = Actor.spawn do
|
100
81
|
result = []
|
101
82
|
3.times do
|
102
83
|
result << Actor.receive do |filter|
|
@@ -114,7 +95,7 @@ describe Actor do
|
|
114
95
|
end
|
115
96
|
|
116
97
|
it "detects dead actors" do
|
117
|
-
actor = Actor.
|
98
|
+
actor = Actor.spawn do
|
118
99
|
Actor.receive do |filter|
|
119
100
|
filter.when(Object) {}
|
120
101
|
end
|