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 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 add mailboxes to Ruby's Fiber mechanism. They multitask cooperatively,
42
- meaning that many of the worries surrounding threaded programming disappear.
43
- Any sequence of operations you do in an Actor are executed in the order
44
- you specify. You don't (generally) have to worry about another Actor doing
45
- something in the background as you frob a particular data structure.
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
- Ideally, what you place in Actor.start is a small startup routine which
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
- Understanding the === statement is a bit strange, so here's a short guide:
105
-
106
- You can pass #when a class to match against the message. Obviously passing it
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
@@ -7,32 +7,28 @@ require File.dirname(__FILE__) + '/../lib/revactor'
7
7
  HOST = 'localhost'
8
8
  PORT = 4321
9
9
 
10
- # Before we can begin using actors we have to call Actor.start
11
- # Future versions of Revactor will hopefully eliminate this
12
- Actor.start do
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
- # Begin receiving connections
18
- loop do
19
- # Accept an incoming connection and start a new Actor
20
- # to handle it
21
- Actor.spawn(listener.accept) do |sock|
22
- puts "#{sock.remote_addr}:#{sock.remote_port} connected"
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
- # Begin echoing received data
25
- loop do
26
- begin
27
- # Write everything we read
28
- sock.write sock.read
29
- rescue EOFError
30
- puts "#{sock.remote_addr}:#{sock.remote_port} disconnected"
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
- # Break (and exit the current actor) if the connection
33
- # is closed, just like with a normal Ruby socket
34
- break
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
@@ -3,22 +3,20 @@
3
3
  require 'cgi'
4
4
  require File.dirname(__FILE__) + '/../lib/revactor'
5
5
 
6
- Actor.start do
7
- term = ARGV[0] || 'foobar'
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
- sock.write [
11
- "GET /search?q=#{CGI.escape(term)} HTTP/1.0",
12
- "Host: www.google.com",
13
- "\r\n"
14
- ].join("\r\n")
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
- loop do
17
- begin
18
- STDOUT.write sock.read
19
- STDOUT.flush
20
- rescue EOFError
21
- break
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
@@ -5,7 +5,7 @@ require File.dirname(__FILE__) + '/../lib/revactor/mongrel'
5
5
  ADDR = '127.0.0.1'
6
6
  PORT = 8080
7
7
 
8
- Actor.start do
8
+ Actor.spawn do
9
9
  server = Mongrel::HttpServer.new(ADDR, PORT)
10
10
  server.register '/', Mongrel::DirHandler.new(".")
11
11
  server.run
@@ -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
- require File.dirname(__FILE__) + '/revactor/actor'
25
- require File.dirname(__FILE__) + '/revactor/server'
26
- require File.dirname(__FILE__) + '/revactor/tcp'
27
- require File.dirname(__FILE__) + '/revactor/behaviors/server'
28
- require File.dirname(__FILE__) + '/revactor/filters/line'
29
- require File.dirname(__FILE__) + '/revactor/filters/packet'
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
@@ -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
- # Raised whenever any Actor-specific problems occur
11
- class ActorError < StandardError; end
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 < Fiber
25
- include Enumerable
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 new(*args, &block)
45
+ def spawn(*args, &block)
33
46
  raise ArgumentError, "no block given" unless block
34
- actor = super() do
47
+
48
+ fiber = Fiber.new do
35
49
  block.call(*args)
36
- Actor.current.instance_eval { @_dead = true }
50
+ Actor.current.instance_eval { @dead = true }
37
51
  end
38
-
39
- # For whatever reason #initialize is never called in subclasses of Fiber
40
- actor.instance_eval do
41
- @_dead = false
42
- @_mailbox = Mailbox.new
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
- actor = super
58
- raise ActorError, "current fiber is not an actor" unless actor.is_a? Actor
59
-
60
- actor
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
- unless current.is_a?(Actor)
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
- # Iterate over the actors in the global dictionary
99
- def each(&block)
100
- @@registered.each(&block)
101
- end
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
- @_dictionary[key]
133
+ @dictionary[key]
107
134
  end
108
135
 
109
136
  # Store a value in the actor's dictionary
110
137
  def []=(key, value)
111
- @_dictionary[key] = value
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
- @_dictionary.delete(key, &block)
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?; @_dead; end
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 complicates overall error handling too much to be worth it.
156
+ # from dead actors greatly overcomplicates overall error handling
133
157
  return message if dead?
134
158
 
135
- @_mailbox << message
136
- Scheduler << self
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