revactor 0.1.1 → 0.1.2
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 +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:
|