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
data/CHANGES
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
0.1.1:
|
2
|
+
|
3
|
+
* Eliminate Actor::Scheduler singleton and replace with thread-specific
|
4
|
+
scheduler objects.
|
5
|
+
|
6
|
+
* Eliminate Actor.start: now there's a current Actor by default in every thread.
|
7
|
+
This paves the way towards thread safety.
|
8
|
+
|
9
|
+
* Rename Revactor::Server to Revactor::Delegator and make more like delegator.rb
|
10
|
+
|
11
|
+
* Factor apart actor.rb into scheduler.rb and mailbox.rb
|
12
|
+
|
13
|
+
* Provide Revactor modules classes within the Actor namespace unless they have
|
14
|
+
already been defined
|
15
|
+
|
16
|
+
* Fix Revactor::Filter initialization bug
|
17
|
+
|
18
|
+
* Include Revactor::VERSION variable
|
19
|
+
|
20
|
+
* Mailbox filters can now include only a timeout (i.e. sleep). Added an
|
21
|
+
Actor.sleep shortcut to this behavior.
|
22
|
+
|
1
23
|
0.1.0:
|
2
24
|
|
3
25
|
* Initial release
|
data/README
CHANGED
@@ -33,34 +33,16 @@ network servers painlessly while still guaranteeing correct operation:
|
|
33
33
|
can preprocess or postprocess data before it's even delivered to an Actor.
|
34
34
|
This is useful for handling protocol framing or other streaming transforms.
|
35
35
|
|
36
|
-
* Behaviors - These are patterns for implementing Actors which accomplish
|
37
|
-
specific tasks. Right now only the Server behavior is supported.
|
38
|
-
|
39
36
|
== Actors
|
40
37
|
|
41
|
-
Actors
|
42
|
-
meaning that many of the worries
|
43
|
-
Any sequence of operations you do
|
44
|
-
you specify. You don't (generally) have
|
45
|
-
something in the background as you frob a
|
46
|
-
|
47
|
-
Unfortunately, in Ruby 1.9, Actors are not first-class citizens. This means
|
48
|
-
you will need to jump from the non-Actor world to the Actor world before you
|
49
|
-
can do anything with Actors. This is accomplished by running Actor.start:
|
50
|
-
|
51
|
-
# Not in Actor world
|
52
|
-
Actor.start do
|
53
|
-
# In Actor world, yay
|
54
|
-
...
|
55
|
-
end
|
56
|
-
# Won't get called until all Actors have processes their entire mailbox
|
57
|
-
# and aren't waiting for any events.
|
38
|
+
Actors are lightweight concurrency primitives which communicate using message
|
39
|
+
passing. They multitask cooperatively, meaning that many of the worries
|
40
|
+
surrounding threaded programming disappear. Any sequence of operations you do
|
41
|
+
in an Actor are executed in the order you specify. You don't (generally) have
|
42
|
+
to worry about another Actor doing something in the background as you frob a
|
43
|
+
particular data structure.
|
58
44
|
|
59
|
-
|
60
|
-
spawns all the Actors your application needs to get started and nothing else.
|
61
|
-
|
62
|
-
Once you're in Actor world, you can begin making Actors and sending them
|
63
|
-
events. You create Actors with Actor.spawn:
|
45
|
+
Actors are created by calling Actor.spawn:
|
64
46
|
|
65
47
|
myactor = Actor.spawn { puts "I'm an Actor!" }
|
66
48
|
|
@@ -84,6 +66,9 @@ prints:
|
|
84
66
|
|
85
67
|
"Yay, I got a dog!"
|
86
68
|
|
69
|
+
You can retrieve the current Actor by calling Actor.current. There will always
|
70
|
+
be a default Actor available for every Thread.
|
71
|
+
|
87
72
|
== Mailboxes
|
88
73
|
|
89
74
|
So, Actors can receive messages. But where do those messages go? The answer
|
@@ -101,12 +86,9 @@ against a message and a block to call if the message matches. The pattern is
|
|
101
86
|
compared to the message using ===, the same thing Ruby uses for case statements.
|
102
87
|
You can think of the filter as a big case statement.
|
103
88
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
Object will match all messages.
|
108
|
-
|
109
|
-
You can pass #when a regexp to match against the message.
|
89
|
+
Like the case statement, a class matches any objects of that class. Since all
|
90
|
+
classes descend from Object passing Object will match all messages. You can
|
91
|
+
also pass a regexp to match against a string.
|
110
92
|
|
111
93
|
Revactor installs the Case gem by default. This is useful for matching against
|
112
94
|
messages stored in Arrays, or in fixed-size arrays called Tuples. Case can
|
@@ -277,24 +259,6 @@ remaining message. This is a simple and straightforward way to frame
|
|
277
259
|
discrete messages on top of a streaming protocol like TCP, and is used for,
|
278
260
|
among other things, DRb.
|
279
261
|
|
280
|
-
== Behaviors
|
281
|
-
|
282
|
-
Behaviors are kind of like design patterns for Actors. Often you just want
|
283
|
-
an Actor to hold some state and service calls which query or mutate that state.
|
284
|
-
This behavior is wrapped up as a Server.
|
285
|
-
|
286
|
-
To begin implementing a server, look at Revactor::Behavior::Server, which is
|
287
|
-
a module demonstrating the API. Servers are implemented using a set of
|
288
|
-
callbacks which are called in response to certain events.
|
289
|
-
|
290
|
-
Servers should implement a number of principles, including transactional
|
291
|
-
semantics, however due to the side effect potential of mutable state in
|
292
|
-
Ruby this isn't possible to achieve.
|
293
|
-
|
294
|
-
Future versions of Revactor may attempt to address this, and also change the
|
295
|
-
present implementation (which is effectively a carbon copy of Erlang's
|
296
|
-
gen_server) to be more Ruby-like.
|
297
|
-
|
298
262
|
== Mongrel
|
299
263
|
|
300
264
|
Revactor includes complete support for running Mongrel on top of Actors and
|
data/examples/echo_server.rb
CHANGED
@@ -7,32 +7,28 @@ require File.dirname(__FILE__) + '/../lib/revactor'
|
|
7
7
|
HOST = 'localhost'
|
8
8
|
PORT = 4321
|
9
9
|
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
# Create a new listener socket on the given host and port
|
14
|
-
listener = Revactor::TCP.listen(HOST, PORT)
|
15
|
-
puts "Listening on #{HOST}:#{PORT}"
|
10
|
+
# Create a new listener socket on the given host and port
|
11
|
+
listener = Revactor::TCP.listen(HOST, PORT)
|
12
|
+
puts "Listening on #{HOST}:#{PORT}"
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
14
|
+
# Begin receiving connections
|
15
|
+
loop do
|
16
|
+
# Accept an incoming connection and start a new Actor
|
17
|
+
# to handle it
|
18
|
+
Actor.spawn(listener.accept) do |sock|
|
19
|
+
puts "#{sock.remote_addr}:#{sock.remote_port} connected"
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
21
|
+
# Begin echoing received data
|
22
|
+
loop do
|
23
|
+
begin
|
24
|
+
# Write everything we read
|
25
|
+
sock.write sock.read
|
26
|
+
rescue EOFError
|
27
|
+
puts "#{sock.remote_addr}:#{sock.remote_port} disconnected"
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
29
|
+
# Break (and exit the current actor) if the connection
|
30
|
+
# is closed, just like with a normal Ruby socket
|
31
|
+
break
|
36
32
|
end
|
37
33
|
end
|
38
34
|
end
|
data/examples/google.rb
CHANGED
@@ -3,22 +3,20 @@
|
|
3
3
|
require 'cgi'
|
4
4
|
require File.dirname(__FILE__) + '/../lib/revactor'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
sock = Revactor::TCP.connect("www.google.com", 80)
|
6
|
+
term = ARGV[0] || 'foobar'
|
7
|
+
sock = Revactor::TCP.connect("www.google.com", 80)
|
9
8
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
sock.write [
|
10
|
+
"GET /search?q=#{CGI.escape(term)} HTTP/1.0",
|
11
|
+
"Host: www.google.com",
|
12
|
+
"\r\n"
|
13
|
+
].join("\r\n")
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
15
|
+
loop do
|
16
|
+
begin
|
17
|
+
STDOUT.write sock.read
|
18
|
+
STDOUT.flush
|
19
|
+
rescue EOFError
|
20
|
+
break
|
23
21
|
end
|
24
22
|
end
|
data/examples/mongrel.rb
CHANGED
data/lib/revactor.rb
CHANGED
@@ -4,7 +4,6 @@
|
|
4
4
|
# See file LICENSE for details
|
5
5
|
#++
|
6
6
|
|
7
|
-
require 'rubygems'
|
8
7
|
require 'rev'
|
9
8
|
require 'case'
|
10
9
|
|
@@ -21,9 +20,18 @@ end
|
|
21
20
|
# Shortcut Tuple as T
|
22
21
|
T = Tuple unless defined? T
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
module Revactor
|
24
|
+
Revactor::VERSION = '0.1.1' unless defined? Revactor::VERSION
|
25
|
+
def self.version() VERSION end
|
26
|
+
end
|
27
|
+
|
28
|
+
%w{actor scheduler mailbox delegator tcp filters/line filters/packet}.each do |file|
|
29
|
+
require File.dirname(__FILE__) + '/revactor/' + file
|
30
|
+
end
|
31
|
+
|
32
|
+
# Place Revactor modules and classes under the Actor namespace
|
33
|
+
class Actor
|
34
|
+
Actor::TCP = Revactor::TCP unless defined? Actor::TCP
|
35
|
+
Actor::Filter = Revactor::Filter unless defined? Actor::Filter
|
36
|
+
Actor::Delegator = Revactor::Delegator unless defined? Actor::Delegator
|
37
|
+
end
|
data/lib/revactor/actor.rb
CHANGED
@@ -5,10 +5,22 @@
|
|
5
5
|
#++
|
6
6
|
|
7
7
|
require File.dirname(__FILE__) + '/../revactor'
|
8
|
+
require 'thread'
|
8
9
|
require 'fiber'
|
9
10
|
|
10
|
-
#
|
11
|
-
class
|
11
|
+
# Monkeypatch Thread to include a method for obtaining the current Scheduler
|
12
|
+
class Thread
|
13
|
+
def _revactor_scheduler
|
14
|
+
@_revactor_scheduler ||= Actor::Scheduler.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Monkeypatch Fiber to include a method for obtaining the current Actor
|
19
|
+
class Fiber
|
20
|
+
def _actor
|
21
|
+
@_actor ||= Actor.new
|
22
|
+
end
|
23
|
+
end
|
12
24
|
|
13
25
|
# Actors are lightweight concurrency primitives which communiucate via message
|
14
26
|
# passing. Each actor has a mailbox which it scans for matching messages.
|
@@ -21,43 +33,52 @@ class ActorError < StandardError; end
|
|
21
33
|
# should be possible to run programs written using Revactor to on top of other
|
22
34
|
# Actor implementations.
|
23
35
|
#
|
24
|
-
class Actor
|
25
|
-
|
36
|
+
class Actor
|
37
|
+
attr_reader :fiber
|
38
|
+
attr_reader :scheduler
|
39
|
+
attr_reader :mailbox
|
40
|
+
|
26
41
|
@@registered = {}
|
27
42
|
|
28
43
|
class << self
|
29
|
-
include Enumerable
|
30
|
-
|
31
44
|
# Create a new Actor with the given block and arguments
|
32
|
-
def
|
45
|
+
def spawn(*args, &block)
|
33
46
|
raise ArgumentError, "no block given" unless block
|
34
|
-
|
47
|
+
|
48
|
+
fiber = Fiber.new do
|
35
49
|
block.call(*args)
|
36
|
-
Actor.current.instance_eval { @
|
50
|
+
Actor.current.instance_eval { @dead = true }
|
37
51
|
end
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@_dictionary = {}
|
44
|
-
end
|
45
|
-
|
46
|
-
Scheduler << actor
|
52
|
+
|
53
|
+
actor = Actor.new(fiber)
|
54
|
+
fiber.instance_eval { @_actor = actor }
|
55
|
+
|
56
|
+
Actor.scheduler << actor
|
47
57
|
actor
|
48
58
|
end
|
49
59
|
|
50
|
-
alias_method :spawn, :new
|
51
|
-
|
52
|
-
# This will be defined differently in the future, but now the two are the same
|
53
|
-
alias_method :start, :new
|
54
|
-
|
55
60
|
# Obtain a handle to the current Actor
|
56
61
|
def current
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
62
|
+
Fiber.current._actor
|
63
|
+
end
|
64
|
+
|
65
|
+
# Obtain a handle to the current Scheduler
|
66
|
+
def scheduler
|
67
|
+
Thread.current._revactor_scheduler
|
68
|
+
end
|
69
|
+
|
70
|
+
# Reschedule the current actor for execution later
|
71
|
+
def reschedule
|
72
|
+
if scheduler.running?
|
73
|
+
Fiber.yield
|
74
|
+
else
|
75
|
+
Actor.scheduler << Actor.current
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Sleep for the specified number of seconds
|
80
|
+
def sleep(seconds)
|
81
|
+
Actor.receive { |filter| filter.after(seconds) }
|
61
82
|
end
|
62
83
|
|
63
84
|
# Wait for messages matching a given filter. The filter object is yielded
|
@@ -69,11 +90,7 @@ class Actor < Fiber
|
|
69
90
|
# The first filter to match a message in the mailbox is executed. If no
|
70
91
|
# filters match then the actor sleeps.
|
71
92
|
def receive(&filter)
|
72
|
-
|
73
|
-
raise ActorError, "receive must be called in the context of an Actor"
|
74
|
-
end
|
75
|
-
|
76
|
-
current.__send__(:_mailbox).receive(&filter)
|
93
|
+
current.mailbox.receive(&filter)
|
77
94
|
end
|
78
95
|
|
79
96
|
# Look up an actor in the global dictionary
|
@@ -94,223 +111,56 @@ class Actor < Fiber
|
|
94
111
|
def delete(key, &block)
|
95
112
|
@@registered.delete(key, &block)
|
96
113
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize(fiber = Fiber.current)
|
117
|
+
raise ArgumentError, "use Actor.spawn to create actors" if block_given?
|
118
|
+
|
119
|
+
@fiber = fiber
|
120
|
+
@scheduler = Actor.scheduler
|
121
|
+
@thread = Thread.current
|
122
|
+
@mailbox = Mailbox.new
|
123
|
+
@dead = false
|
124
|
+
@dictionary = {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def inspect
|
128
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
102
129
|
end
|
103
130
|
|
104
131
|
# Look up value in the actor's dictionary
|
105
132
|
def [](key)
|
106
|
-
@
|
133
|
+
@dictionary[key]
|
107
134
|
end
|
108
135
|
|
109
136
|
# Store a value in the actor's dictionary
|
110
137
|
def []=(key, value)
|
111
|
-
@
|
138
|
+
@dictionary[key] = value
|
112
139
|
end
|
113
140
|
|
114
141
|
# Delete a value from the actor's dictionary
|
115
142
|
def delete(key, &block)
|
116
|
-
@
|
117
|
-
end
|
118
|
-
|
119
|
-
# Iterate over values in the actor's dictionary
|
120
|
-
def each(&block)
|
121
|
-
@_dictionary.each(&block)
|
143
|
+
@dictionary.delete(key, &block)
|
122
144
|
end
|
123
145
|
|
124
146
|
# Is the current actor dead?
|
125
|
-
def dead?; @
|
147
|
+
def dead?; @dead; end
|
126
148
|
|
127
149
|
# Send a message to an actor
|
128
150
|
def <<(message)
|
151
|
+
return "can't send messages to actors across threads" unless @thread == Thread.current
|
152
|
+
|
129
153
|
# Erlang discards messages sent to dead actors, and if Erlang does it,
|
130
154
|
# it must be the right thing to do, right? Hooray for the Erlang
|
131
155
|
# cargo cult! I think they do this because dealing with errors raised
|
132
|
-
# from dead actors
|
156
|
+
# from dead actors greatly overcomplicates overall error handling
|
133
157
|
return message if dead?
|
134
158
|
|
135
|
-
@
|
136
|
-
|
159
|
+
@mailbox << message
|
160
|
+
@scheduler << self
|
161
|
+
|
137
162
|
message
|
138
163
|
end
|
139
164
|
|
140
165
|
alias_method :send, :<<
|
141
|
-
|
142
|
-
#########
|
143
|
-
protected
|
144
|
-
#########
|
145
|
-
|
146
|
-
attr_reader :_mailbox
|
147
|
-
|
148
|
-
# The Actor Scheduler maintains a run queue of actors with outstanding
|
149
|
-
# messages who have not yet processed their mailbox. If all actors have
|
150
|
-
# processed their mailboxes then the scheduler waits for any outstanding
|
151
|
-
# Rev events. If there are no active Rev watchers then the scheduler exits.
|
152
|
-
class Scheduler
|
153
|
-
@@queue = []
|
154
|
-
@@running = false
|
155
|
-
|
156
|
-
class << self
|
157
|
-
# Schedule an Actor to be executed, and run the scheduler if it isn't
|
158
|
-
# currently running
|
159
|
-
def <<(actor)
|
160
|
-
@@queue << actor
|
161
|
-
run unless @@running
|
162
|
-
end
|
163
|
-
|
164
|
-
# Run the scheduler
|
165
|
-
def run
|
166
|
-
return if @@running
|
167
|
-
@@running = true
|
168
|
-
default_loop = Rev::Loop.default
|
169
|
-
|
170
|
-
until @@queue.empty? and default_loop.watchers.empty?
|
171
|
-
@@queue.each do |actor|
|
172
|
-
begin
|
173
|
-
actor.resume
|
174
|
-
rescue FiberError # Fiber may have died since being scheduled
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
@@queue.clear
|
179
|
-
|
180
|
-
default_loop.run_once unless default_loop.watchers.empty?
|
181
|
-
end
|
182
|
-
|
183
|
-
@@running = false
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
# Actor mailbox. For purposes of efficiency the mailbox also handles
|
189
|
-
# suspending and resuming an actor when no messages match its filter set.
|
190
|
-
class Mailbox
|
191
|
-
attr_accessor :timer
|
192
|
-
attr_accessor :timed_out
|
193
|
-
attr_accessor :timeout_action
|
194
|
-
|
195
|
-
def initialize
|
196
|
-
@timer = nil
|
197
|
-
@queue = []
|
198
|
-
end
|
199
|
-
|
200
|
-
# Add a message to the mailbox queue
|
201
|
-
def <<(message)
|
202
|
-
@queue << message
|
203
|
-
end
|
204
|
-
|
205
|
-
# Attempt to receive a message
|
206
|
-
def receive
|
207
|
-
raise ArgumentError, "no filter block given" unless block_given?
|
208
|
-
|
209
|
-
# Clear mailbox processing variables
|
210
|
-
action = matched_index = nil
|
211
|
-
processed_upto = 0
|
212
|
-
|
213
|
-
# Clear timeout variables
|
214
|
-
@timed_out = false
|
215
|
-
@timeout_action = nil
|
216
|
-
|
217
|
-
# Build the filter
|
218
|
-
filter = Filter.new(self)
|
219
|
-
yield filter
|
220
|
-
raise ArgumentError, "empty filter" if filter.empty?
|
221
|
-
|
222
|
-
# Process incoming messages
|
223
|
-
while action.nil?
|
224
|
-
@queue[processed_upto..@queue.size].each_with_index do |message, index|
|
225
|
-
unless (action = filter.match message)
|
226
|
-
# The filter did not match an action for the current message
|
227
|
-
# Keep track of which messages we've ran the filter across so it doesn't
|
228
|
-
# get run against messages it already failed to match
|
229
|
-
processed_upto += 1
|
230
|
-
next
|
231
|
-
end
|
232
|
-
|
233
|
-
# We've found a matching action, so break out of the loop
|
234
|
-
matched_index = processed_upto + index
|
235
|
-
break
|
236
|
-
end
|
237
|
-
|
238
|
-
# If we've timed out, run the timeout action unless another has been found
|
239
|
-
action ||= @timeout_action if @timed_out
|
240
|
-
|
241
|
-
# If we didn't find a matching action, yield until we get another message
|
242
|
-
Actor.yield unless action
|
243
|
-
end
|
244
|
-
|
245
|
-
if @timer
|
246
|
-
@timer.detach if @timer.attached?
|
247
|
-
@timer = nil
|
248
|
-
end
|
249
|
-
|
250
|
-
# If we encountered a timeout, call the action directly
|
251
|
-
return action.call if @timed_out
|
252
|
-
|
253
|
-
# Otherwise we matched a message, so process it with the action
|
254
|
-
return action.(@queue.delete_at matched_index)
|
255
|
-
end
|
256
|
-
|
257
|
-
# Timeout class, used to implement receive timeouts
|
258
|
-
class Timer < Rev::TimerWatcher
|
259
|
-
def initialize(timeout, actor)
|
260
|
-
@actor = actor
|
261
|
-
super(timeout)
|
262
|
-
end
|
263
|
-
|
264
|
-
def on_timer
|
265
|
-
@actor.instance_eval { @_mailbox.timed_out = true }
|
266
|
-
Scheduler << @actor
|
267
|
-
end
|
268
|
-
end
|
269
|
-
|
270
|
-
# Mailbox filterset. Takes patterns or procs to match messages with
|
271
|
-
# and returns the associated proc when a pattern matches.
|
272
|
-
class Filter
|
273
|
-
def initialize(mailbox)
|
274
|
-
@mailbox = mailbox
|
275
|
-
@ruleset = []
|
276
|
-
end
|
277
|
-
|
278
|
-
# Provide a pattern to match against with === and a block to call
|
279
|
-
# when the pattern is matched.
|
280
|
-
def when(pattern, &action)
|
281
|
-
raise ArgumentError, "no block given" unless action
|
282
|
-
@ruleset << [pattern, action]
|
283
|
-
end
|
284
|
-
|
285
|
-
# Provide a timeout (in seconds, can be a Float) to wait for matching
|
286
|
-
# messages. If the timeout elapses, the given block is called.
|
287
|
-
def after(timeout, &action)
|
288
|
-
raise ArgumentError, "timeout already specified" if @mailbox.timer
|
289
|
-
raise ArgumentError, "must be zero or positive" if timeout < 0
|
290
|
-
|
291
|
-
# Don't explicitly require an action to be specified for a timeout
|
292
|
-
@mailbox.timeout_action = action || proc {}
|
293
|
-
|
294
|
-
if timeout > 0
|
295
|
-
@mailbox.timer = Timer.new(timeout, Actor.current).attach(Rev::Loop.default)
|
296
|
-
else
|
297
|
-
# No need to actually set a timer if the timeout is zero,
|
298
|
-
# just short-circuit waiting for one entirely...
|
299
|
-
@timed_out = true
|
300
|
-
Scheduler << self
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
# Match a message using the filter
|
305
|
-
def match(message)
|
306
|
-
_, action = @ruleset.find { |pattern, _| pattern === message }
|
307
|
-
action
|
308
|
-
end
|
309
|
-
|
310
|
-
# Is the filterset empty?
|
311
|
-
def empty?
|
312
|
-
@ruleset.empty?
|
313
|
-
end
|
314
|
-
end
|
315
|
-
end
|
316
|
-
end
|
166
|
+
end
|