revactor 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|