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 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
@@ -21,7 +21,7 @@ end
21
21
  T = Tuple unless defined? T
22
22
 
23
23
  module Revactor
24
- Revactor::VERSION = '0.1.1' unless defined? Revactor::VERSION
24
+ Revactor::VERSION = '0.1.2' unless defined? Revactor::VERSION
25
25
  def self.version() VERSION end
26
26
  end
27
27
 
@@ -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
- fiber = Fiber.new do
49
- block.call(*args)
50
- Actor.current.instance_eval { @dead = true }
51
- end
52
-
53
- actor = Actor.new(fiber)
54
- fiber.instance_eval { @_actor = actor }
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
- Actor.scheduler << actor
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
- Actor.scheduler << Actor.current
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
- Actor.receive { |filter| filter.after(seconds) }
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
- def inspect
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
@@ -63,7 +63,7 @@ class Revactor::Delegator
63
63
  begin
64
64
  result = @obj.__send__(meth, *args, &block)
65
65
  from << T[:call_reply, Actor.current, result]
66
- rescue Exception => ex
66
+ rescue => ex
67
67
  from << T[:call_error, Actor.current, ex]
68
68
  end
69
69
  end
@@ -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
@@ -1,5 +1,4 @@
1
1
  require File.dirname(__FILE__) + '/../revactor'
2
- require 'rubygems'
3
2
  require 'mongrel'
4
3
 
5
4
  class Revactor::TCP::Socket
@@ -46,7 +46,12 @@ class Actor
46
46
  @queue.each do |actor|
47
47
  begin
48
48
  actor.fiber.resume
49
- rescue FiberError # Fiber may have died since being scheduled
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
@@ -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
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
 
3
3
  GEMSPEC = Gem::Specification.new do |s|
4
4
  s.name = "revactor"
5
- s.version = "0.1.1"
5
+ s.version = "0.1.2"
6
6
  s.authors = "Tony Arcieri"
7
7
  s.email = "tony@medioh.com"
8
8
  s.date = "2008-1-28"
@@ -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.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: