revactor 0.1.0 → 0.1.1

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 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