revactor 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES +10 -0
- data/README +70 -0
- data/lib/revactor.rb +1 -1
- data/lib/revactor/actor.rb +126 -13
- data/lib/revactor/delegator.rb +1 -1
- data/lib/revactor/mailbox.rb +1 -9
- data/lib/revactor/mongrel.rb +0 -1
- data/lib/revactor/scheduler.rb +32 -1
- data/lib/revactor/tcp.rb +6 -2
- data/revactor.gemspec +1 -1
- data/spec/actor_spec.rb +36 -0
- metadata +1 -1
data/CHANGES
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
+
0.1.2:
|
2
|
+
|
3
|
+
* Change Revactor::TCP::Socket#active to #active? (same for Listener)
|
4
|
+
|
5
|
+
* Fix problems with Actor#inspect reporting the wrong object ID
|
6
|
+
|
7
|
+
* Initial linking implementation
|
8
|
+
|
9
|
+
* Fix problems with zero-length timers
|
10
|
+
|
1
11
|
0.1.1:
|
2
12
|
|
3
13
|
* Eliminate Actor::Scheduler singleton and replace with thread-specific
|
data/README
CHANGED
@@ -122,6 +122,76 @@ Actor.receive returns whatever value the evaluated action returned. This means
|
|
122
122
|
you don't have to depend on side effects to extract values from receive,
|
123
123
|
instead you can just interpret its return value.
|
124
124
|
|
125
|
+
== Handling Exceptions
|
126
|
+
|
127
|
+
In a concurrent environment, dealing with exceptions can be incredibly
|
128
|
+
confusing. By default, any unhandled exceptions in an Actor are logged
|
129
|
+
and any remaining Actors continue their normal operation. However, Actors
|
130
|
+
also provide a powerful tool for implementing fault-tolerant systems that
|
131
|
+
can gracefully recover from exceptions.
|
132
|
+
|
133
|
+
Actors can be linked to each other:
|
134
|
+
|
135
|
+
another_actor = Actor.spawn { puts "I'm an Actor!" }
|
136
|
+
Actor.link(another_actor)
|
137
|
+
|
138
|
+
This can also be done as a single "atomic" operation:
|
139
|
+
|
140
|
+
actor = Actor.spawn_link { puts "I'm an Actor!" }
|
141
|
+
|
142
|
+
When Actors are linked, any exceptions which occur in one will be raised in the
|
143
|
+
other, and vice versa. This means if one Actor dies, any Actors it's linked to
|
144
|
+
will also die. Furthermore, any Actors those are linked to also die. This
|
145
|
+
occurs until the entire graph of linked Actors has been walked.
|
146
|
+
|
147
|
+
In this way, you can organize Actors into large groups which all die
|
148
|
+
simultaneously whenever an error occurs in one. This means that if an error
|
149
|
+
occurs and one Actor dies, you're not left with an interdependent network of
|
150
|
+
Actors which are in an inconsistent state. You can kill off the whole group
|
151
|
+
and start over fresh.
|
152
|
+
|
153
|
+
But if an Actor crashing kills off every Actor it's linked to, what Actor will
|
154
|
+
be left to restart the whole group? The answer is that an Actor can trap
|
155
|
+
exit events from another and receive them as messages:
|
156
|
+
|
157
|
+
Actor.current.trap_exit = true
|
158
|
+
actor = Actor.spawn_link { puts "I'm an Actor!" }
|
159
|
+
Actor.receive do |filter|
|
160
|
+
filter.when(Case[:exit, actor, Object]) { |msg| p msg }
|
161
|
+
end
|
162
|
+
|
163
|
+
will print something to the effect of:
|
164
|
+
|
165
|
+
I'm an Actor!
|
166
|
+
[:exit, #<Actor:0x54ad6c>, nil]
|
167
|
+
|
168
|
+
We were sent a message in the form:
|
169
|
+
|
170
|
+
[:exit, actor, reason]
|
171
|
+
|
172
|
+
and in this case reason was nil, which informs us the Actor exited normally.
|
173
|
+
But what if it dies due to an exception instead?
|
174
|
+
|
175
|
+
Actor.current.trap_exit = true
|
176
|
+
actor = Actor.spawn_link { raise "I fail!" }
|
177
|
+
Actor.receive do |filter|
|
178
|
+
filter.when(Case[:exit, actor, Object]) { |msg| p msg }
|
179
|
+
end
|
180
|
+
|
181
|
+
We now get the entire exception, captured and delivered as a message:
|
182
|
+
|
183
|
+
[:exit, #<Actor:0x53ec24>, #<RuntimeError: I fail!>]
|
184
|
+
|
185
|
+
If the Actor that died were linked to any others which were not trapping exits,
|
186
|
+
those would all die and the ones trapping exits would remain. This allows us
|
187
|
+
to implement supervisors which trap exits and respond to exit messages. The
|
188
|
+
supervisor's job is to start an Actor initially, and if it fails log the error
|
189
|
+
then restart it.
|
190
|
+
|
191
|
+
In this way Actors can be used to build complex concurrent systems which fail
|
192
|
+
gracefully and can respond to errors by restarting interdependent components
|
193
|
+
of the system en masse.
|
194
|
+
|
125
195
|
== Revactor::TCP
|
126
196
|
|
127
197
|
The TCP module lets you perform TCP operations on top of the Actor model. For
|
data/lib/revactor.rb
CHANGED
data/lib/revactor/actor.rb
CHANGED
@@ -22,6 +22,9 @@ class Fiber
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
# Error raised when attempting to link to dead Actors
|
26
|
+
class DeadActorError < StandardError; end
|
27
|
+
|
25
28
|
# Actors are lightweight concurrency primitives which communiucate via message
|
26
29
|
# passing. Each actor has a mailbox which it scans for matching messages.
|
27
30
|
# An actor sleeps until it receives a message, at which time it scans messages
|
@@ -45,18 +48,31 @@ class Actor
|
|
45
48
|
def spawn(*args, &block)
|
46
49
|
raise ArgumentError, "no block given" unless block
|
47
50
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
actor = _spawn(*args, &block)
|
52
|
+
scheduler << actor
|
53
|
+
actor
|
54
|
+
end
|
55
|
+
|
56
|
+
# Spawn an Actor and immediately link it to the current one
|
57
|
+
def spawn_link(*args, &block)
|
58
|
+
raise ArgumentError, "no block given" unless block
|
55
59
|
|
56
|
-
|
60
|
+
actor = _spawn(*args, &block)
|
61
|
+
current.link actor
|
62
|
+
scheduler << actor
|
57
63
|
actor
|
58
64
|
end
|
59
65
|
|
66
|
+
# Link the current Actor to another one
|
67
|
+
def link(actor)
|
68
|
+
current.link actor
|
69
|
+
end
|
70
|
+
|
71
|
+
# Unlink the current Actor from another one
|
72
|
+
def unlink(actor)
|
73
|
+
current.unlink actor
|
74
|
+
end
|
75
|
+
|
60
76
|
# Obtain a handle to the current Actor
|
61
77
|
def current
|
62
78
|
Fiber.current._actor
|
@@ -72,13 +88,15 @@ class Actor
|
|
72
88
|
if scheduler.running?
|
73
89
|
Fiber.yield
|
74
90
|
else
|
75
|
-
|
91
|
+
scheduler << current
|
76
92
|
end
|
93
|
+
|
94
|
+
current.__send__(:process_events)
|
77
95
|
end
|
78
96
|
|
79
97
|
# Sleep for the specified number of seconds
|
80
98
|
def sleep(seconds)
|
81
|
-
|
99
|
+
receive { |filter| filter.after(seconds) }
|
82
100
|
end
|
83
101
|
|
84
102
|
# Wait for messages matching a given filter. The filter object is yielded
|
@@ -111,6 +129,20 @@ class Actor
|
|
111
129
|
def delete(key, &block)
|
112
130
|
@@registered.delete(key, &block)
|
113
131
|
end
|
132
|
+
|
133
|
+
#########
|
134
|
+
protected
|
135
|
+
#########
|
136
|
+
|
137
|
+
def _spawn(*args, &block)
|
138
|
+
fiber = Fiber.new do
|
139
|
+
block.call(*args)
|
140
|
+
current.instance_eval { @dead = true }
|
141
|
+
end
|
142
|
+
|
143
|
+
actor = Actor.new(fiber)
|
144
|
+
fiber.instance_eval { @_actor = actor }
|
145
|
+
end
|
114
146
|
end
|
115
147
|
|
116
148
|
def initialize(fiber = Fiber.current)
|
@@ -120,13 +152,14 @@ class Actor
|
|
120
152
|
@scheduler = Actor.scheduler
|
121
153
|
@thread = Thread.current
|
122
154
|
@mailbox = Mailbox.new
|
155
|
+
@links = []
|
156
|
+
@events = []
|
157
|
+
@trap_exit = false
|
123
158
|
@dead = false
|
124
159
|
@dictionary = {}
|
125
160
|
end
|
126
161
|
|
127
|
-
|
128
|
-
"#<#{self.class}:0x#{object_id.to_s(16)}>"
|
129
|
-
end
|
162
|
+
alias_method :inspect, :to_s
|
130
163
|
|
131
164
|
# Look up value in the actor's dictionary
|
132
165
|
def [](key)
|
@@ -163,4 +196,84 @@ class Actor
|
|
163
196
|
end
|
164
197
|
|
165
198
|
alias_method :send, :<<
|
199
|
+
|
200
|
+
# Establish a bidirectional link to the given Actor and notify it of any
|
201
|
+
# system events which occur in this Actor (namely exits due to exceptions)
|
202
|
+
def link(actor)
|
203
|
+
actor.notify_link self
|
204
|
+
self.notify_link actor
|
205
|
+
end
|
206
|
+
|
207
|
+
# Unestablish a link with the given actor
|
208
|
+
def unlink(actor)
|
209
|
+
actor.notify_unlink self
|
210
|
+
self.notify_unlink actor
|
211
|
+
end
|
212
|
+
|
213
|
+
# Notify this actor that it's now linked to the given one
|
214
|
+
def notify_link(actor)
|
215
|
+
raise ArgumentError, "can only link to Actors" unless actor.is_a? Actor
|
216
|
+
|
217
|
+
# Don't allow linking to dead actors
|
218
|
+
raise DeadActorError, "actor is dead" if actor.dead?
|
219
|
+
|
220
|
+
# Ignore circular links
|
221
|
+
return true if actor == self
|
222
|
+
|
223
|
+
# Ignore duplicate links
|
224
|
+
return true if @links.include? actor
|
225
|
+
|
226
|
+
@links << actor
|
227
|
+
true
|
228
|
+
end
|
229
|
+
|
230
|
+
# Notify this actor that it's now unlinked from the given one
|
231
|
+
def notify_unlink(actor)
|
232
|
+
@links.delete(actor)
|
233
|
+
true
|
234
|
+
end
|
235
|
+
|
236
|
+
# Notify this actor that one of the Actors it's linked to has exited
|
237
|
+
def notify_exited(actor, reason)
|
238
|
+
@events << T[:exit, actor, reason]
|
239
|
+
end
|
240
|
+
|
241
|
+
# Actors trapping exit do not die when an error occurs in an Actor they
|
242
|
+
# are linked to. Instead the exit message is sent to their regular
|
243
|
+
# mailbox in the form [:exit, actor, reason]. This allows certain
|
244
|
+
# Actors to supervise sets of others and restart them in the event
|
245
|
+
# of an error.
|
246
|
+
def trap_exit=(value)
|
247
|
+
raise ArgumentError, "must be true or false" unless value == true or value == false
|
248
|
+
@trap_exit = value
|
249
|
+
end
|
250
|
+
|
251
|
+
# Is the Actor trapping exit?
|
252
|
+
def trap_exit?
|
253
|
+
@trap_exit
|
254
|
+
end
|
255
|
+
|
256
|
+
#########
|
257
|
+
protected
|
258
|
+
#########
|
259
|
+
|
260
|
+
# Process the Actor's system event queue
|
261
|
+
def process_events
|
262
|
+
@events.each do |event|
|
263
|
+
type, *operands = event
|
264
|
+
case type
|
265
|
+
when :exit
|
266
|
+
actor, ex = operands
|
267
|
+
notify_unlink actor
|
268
|
+
|
269
|
+
if @trap_exit
|
270
|
+
self << event
|
271
|
+
elsif ex
|
272
|
+
raise ex
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
@events.clear
|
278
|
+
end
|
166
279
|
end
|
data/lib/revactor/delegator.rb
CHANGED
data/lib/revactor/mailbox.rb
CHANGED
@@ -133,15 +133,7 @@ class Actor
|
|
133
133
|
|
134
134
|
# Don't explicitly require an action to be specified for a timeout
|
135
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
|
136
|
+
@mailbox.timer = Timer.new(seconds, Actor.current).attach(Rev::Loop.default)
|
145
137
|
end
|
146
138
|
|
147
139
|
# Match a message using the filter
|
data/lib/revactor/mongrel.rb
CHANGED
data/lib/revactor/scheduler.rb
CHANGED
@@ -46,7 +46,12 @@ class Actor
|
|
46
46
|
@queue.each do |actor|
|
47
47
|
begin
|
48
48
|
actor.fiber.resume
|
49
|
-
|
49
|
+
handle_exit(actor) if actor.dead?
|
50
|
+
rescue FiberError
|
51
|
+
# Handle Actors whose Fibers died after being scheduled
|
52
|
+
handle_exit(actor)
|
53
|
+
rescue => ex
|
54
|
+
handle_exit(actor, ex)
|
50
55
|
end
|
51
56
|
end
|
52
57
|
|
@@ -61,5 +66,31 @@ class Actor
|
|
61
66
|
def running?
|
62
67
|
@running
|
63
68
|
end
|
69
|
+
|
70
|
+
#########
|
71
|
+
protected
|
72
|
+
#########
|
73
|
+
|
74
|
+
def handle_exit(actor, ex = nil)
|
75
|
+
actor.instance_eval do
|
76
|
+
# Mark Actor as dead
|
77
|
+
@dead = true
|
78
|
+
|
79
|
+
if @links.empty?
|
80
|
+
Actor.scheduler.__send__(:log_exception, ex) if ex
|
81
|
+
else
|
82
|
+
# Notify all linked Actors of the exception
|
83
|
+
@links.each do |link|
|
84
|
+
link.notify_exited(actor, ex)
|
85
|
+
Actor.scheduler << link
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def log_exception(ex)
|
92
|
+
# FIXME this should go to a real logger
|
93
|
+
STDERR.puts "#{ex.class}: #{[ex, *ex.backtrace].join("\n\t")}"
|
94
|
+
end
|
64
95
|
end
|
65
96
|
end
|
data/lib/revactor/tcp.rb
CHANGED
@@ -63,7 +63,6 @@ module Revactor
|
|
63
63
|
# TCP socket class, returned by Revactor::TCP.connect and
|
64
64
|
# Revactor::TCP::Listener#accept
|
65
65
|
class Socket < Rev::TCPSocket
|
66
|
-
attr_reader :active
|
67
66
|
attr_reader :controller
|
68
67
|
|
69
68
|
class << self
|
@@ -134,6 +133,9 @@ module Revactor
|
|
134
133
|
@active = state
|
135
134
|
end
|
136
135
|
|
136
|
+
# Is the socket in active mode?
|
137
|
+
def active?; @active; end
|
138
|
+
|
137
139
|
# Set the controlling actor
|
138
140
|
def controller=(controller)
|
139
141
|
raise ArgumentError, "controller must be an actor" unless controller.is_a? Actor
|
@@ -323,7 +325,6 @@ module Revactor
|
|
323
325
|
|
324
326
|
# TCP Listener returned from Revactor::TCP.listen
|
325
327
|
class Listener < Rev::TCPListener
|
326
|
-
attr_reader :active
|
327
328
|
attr_reader :controller
|
328
329
|
|
329
330
|
# Listen on the specified address and port. Accepts the following options:
|
@@ -359,6 +360,9 @@ module Revactor
|
|
359
360
|
@active = state
|
360
361
|
end
|
361
362
|
|
363
|
+
# Will newly accepted connections be active?
|
364
|
+
def active?; @active; end
|
365
|
+
|
362
366
|
# Change the default controller for newly accepted connections
|
363
367
|
def controller=(controller)
|
364
368
|
raise ArgumentError, "controller must be an actor" unless controller.is_a? Actor
|
data/revactor.gemspec
CHANGED
data/spec/actor_spec.rb
CHANGED
@@ -94,6 +94,42 @@ describe Actor do
|
|
94
94
|
end
|
95
95
|
end
|
96
96
|
|
97
|
+
describe "linking" do
|
98
|
+
it "forwards exceptions to linked Actors" do
|
99
|
+
Actor.spawn do
|
100
|
+
actor = Actor.spawn_link do
|
101
|
+
Actor.receive do |m|
|
102
|
+
m.when(:die) { raise 'dying' }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
proc { actor << :die; Actor.sleep 0 }.should raise_error('dying')
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "sends normal exit messages to linked Actors which are trapping exit" do
|
111
|
+
Actor.spawn do
|
112
|
+
Actor.current.trap_exit = true
|
113
|
+
actor = Actor.spawn_link {}
|
114
|
+
Actor.receive do |m|
|
115
|
+
m.when(Case[:exit, actor, Object]) { |_, _, reason| reason }
|
116
|
+
end.should be_nil
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
it "delivers exceptions to linked Actors which are trapping exit" do
|
121
|
+
error = RuntimeError.new("I fail!")
|
122
|
+
|
123
|
+
Actor.spawn do
|
124
|
+
Actor.current.trap_exit = true
|
125
|
+
actor = Actor.spawn_link { raise error }
|
126
|
+
Actor.receive do |m|
|
127
|
+
m.when(Case[:exit, actor, Object]) { |_, _, reason| reason }
|
128
|
+
end.should == error
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
97
133
|
it "detects dead actors" do
|
98
134
|
actor = Actor.spawn do
|
99
135
|
Actor.receive do |filter|
|
metadata
CHANGED
@@ -3,7 +3,7 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: revactor
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.1.
|
6
|
+
version: 0.1.2
|
7
7
|
date: 2008-01-28 00:00:00 -07:00
|
8
8
|
summary: Revactor is an Actor implementation for writing high performance concurrent programs
|
9
9
|
require_paths:
|