revactor 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,20 @@
1
+ 0.1.3:
2
+
3
+ * Removed case gem usage in Revactor internals
4
+
5
+ * Add a guaranteed blocking idle state for the Actor scheduler, fixing a bug
6
+ where an idle root Actor would spin on the scheduler when calling receive
7
+
8
+ * Fixed bug where subclasses of Actor still create Actors when spawned
9
+
10
+ * Implement initial HttpClient on top of Rev::HttpClient
11
+
12
+ * Optimize scheduler loop and message dispatch for a 25% speed boost
13
+
14
+ * Fix bug with the toplevel Actor never resuming if a newly spawend Actor
15
+ registeres interest in Rev events. Toplevel Actor is now rescheduled if
16
+ the event loop isn't running.
17
+
1
18
  0.1.2:
2
19
 
3
20
  * Change Revactor::TCP::Socket#active to #active? (same for Listener)
data/README CHANGED
@@ -350,12 +350,9 @@ Revactor is still in its infancy. Erlang and its Open Telcom Platform are an
350
350
  extremely feature rich platform, and many features can be borrowed and
351
351
  incorporated into Revactor.
352
352
 
353
- The first and foremost feature to be added is the concept of linked Actors.
354
- This idea is somewhat difficult to explain for the uninitiated, but the general
355
- concept is that interdependent Actors are linked in a graph. When one Actor
356
- dies, it kills all linked Actors is well. However, special Actors trap the
357
- exit messages from the Actors they're linked with. These Actors are generally
358
- supervisors and restart any linked Actor graphs which crash.
353
+ Short term goals include adding thread safety. This will allow Actors in
354
+ different threads to send meassages to each other, making it easy to spin off
355
+ long-term blocking tasks into separate threads.
359
356
 
360
357
  Next on the agenda is implementing DRb. This should be possible simply by
361
358
  monkeypatching the existing DRb implementation to run on top of Revactor::TCP.
@@ -363,10 +360,6 @@ Once DRb has been implemented it should be fairly trivial to implement
363
360
  distributed Actor networks over TCP using DRb as the underlying message
364
361
  passing protocol.
365
362
 
366
- Long term items include implementation of more Filters, Behaviors, and protocol
363
+ Long term items include implementation of more Filters and protocol support
367
364
  modules. These include an HTTP client (subclassed from the client in Rev),
368
- an HTTP server adapter (using the Mongrel parser), and the gen_fsm behavior from
369
- Erlang. Additional areas of concern are addressing the problems of mutable
370
- state in conjunction with Server and FSM behaviors. A possible solution is
371
- to implement a tuple which stores immutable collections of primitive types
372
- like numbers, strings, and symbols, but this approach is likely to be slow.
365
+ as well as an HTTP server adapter (using the Mongrel parser).
@@ -0,0 +1,93 @@
1
+ #
2
+ # A simple chat server implemented using 1 <-> N actors
3
+ # 1 server, N client managers, plus a listener
4
+ #
5
+ # The server handles all message formatting, traffic routing, and connection tracking
6
+ # Client managers handle connection handshaking as well as low-level network interaction
7
+ # The listener spawns new client managers for each incoming connection
8
+ #
9
+
10
+ require File.dirname(__FILE__) + '/../lib/revactor'
11
+
12
+ HOST = 'localhost'
13
+ PORT = 4321
14
+
15
+ # Open a listen socket. All traffic on new connections will be run through
16
+ # the "line" filter, so incoming messages are delimited by newlines.
17
+
18
+ listener = Actor::TCP.listen(HOST, PORT, :filter => :line)
19
+ puts "Listening on #{HOST}:#{PORT}"
20
+
21
+ # Spawn the server
22
+ server = Actor.spawn do
23
+ clients = {}
24
+
25
+ # A proc to broadcast a message to all connected clients. If the server
26
+ # were encapsulated into an object this could be a method
27
+ broadcast = proc do |msg|
28
+ clients.keys.each { |client| client << T[:write, msg] }
29
+ end
30
+
31
+ # Server's main loop. The server handles incoming messages from the
32
+ # client managers and dispatches them to other client managers.
33
+ loop do
34
+ Actor.receive do |filter|
35
+ filter.when(T[:register]) do |_, client, nickname|
36
+ clients[client] = nickname
37
+ broadcast.call "*** #{nickname} joined"
38
+ client << T[:write, "*** Users: " + clients.values.join(', ')]
39
+ end
40
+
41
+ filter.when(T[:say]) do |_, client, msg|
42
+ nickname = clients[client]
43
+ broadcast.call "<#{nickname}> #{msg}"
44
+ end
45
+
46
+ filter.when(T[:disconnected]) do |_, client|
47
+ nickname = clients.delete client
48
+ broadcast.call "*** #{nickname} left" if nickname
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # The main loop handles incoming connections
55
+ loop do
56
+ # Spawn a new actor for each incoming connection
57
+ Actor.spawn(listener.accept) do |sock|
58
+ puts "#{sock.remote_addr}:#{sock.remote_port} connected"
59
+
60
+ # Connection handshaking
61
+ sock.write "Please enter a nickname:"
62
+ nickname = sock.read
63
+
64
+ server << T[:register, Actor.current, nickname]
65
+
66
+ # Flip the socket into asynchronous "active" mode
67
+ # This means the Actor can receive messages from
68
+ # the socket alongside other events.
69
+ sock.controller = Actor.current
70
+ sock.active = :once
71
+
72
+ # Main message loop
73
+ loop do
74
+ Actor.receive do |filter|
75
+ filter.when(T[:tcp, sock]) do |_, _, message|
76
+ server << T[:say, Actor.current, message]
77
+ sock.active = :once
78
+ end
79
+
80
+ filter.when(T[:write]) do |_, message|
81
+ sock.write message
82
+ end
83
+
84
+ filter.when(T[:tcp_closed, sock]) do
85
+ raise EOFError
86
+ end
87
+ end
88
+ end
89
+ rescue EOFError
90
+ puts "#{sock.remote_addr}:#{sock.remote_port} disconnected"
91
+ server << T[:disconnected, Actor.current]
92
+ end
93
+ end
data/examples/mongrel.rb CHANGED
@@ -5,10 +5,8 @@ require File.dirname(__FILE__) + '/../lib/revactor/mongrel'
5
5
  ADDR = '127.0.0.1'
6
6
  PORT = 8080
7
7
 
8
- Actor.spawn do
9
- server = Mongrel::HttpServer.new(ADDR, PORT)
10
- server.register '/', Mongrel::DirHandler.new(".")
11
- server.run
8
+ server = Mongrel::HttpServer.new(ADDR, PORT)
9
+ server.register '/', Mongrel::DirHandler.new(".")
12
10
 
13
- puts "Running on #{ADDR}:#{PORT}"
14
- end
11
+ puts "Running on #{ADDR}:#{PORT}"
12
+ server.start
data/lib/revactor.rb CHANGED
@@ -5,7 +5,6 @@
5
5
  #++
6
6
 
7
7
  require 'rev'
8
- require 'case'
9
8
 
10
9
  # This is mostly in hopes of a bright future with Rubinius
11
10
  # The recommended container for all datagrams sent between
@@ -14,18 +13,27 @@ unless defined? Tuple
14
13
  # A Tuple class. Will (eventually) be a subset of Array
15
14
  # with fixed size and faster performance, at least that's
16
15
  # the hope with Rubinius...
17
- class Tuple < Array; end
16
+ class Tuple < Array
17
+ def ===(obj)
18
+ return false unless obj.is_a? Array
19
+ size.times { |n| return false unless self[n] === obj[n] }
20
+ true
21
+ end
22
+ end
18
23
  end
19
24
 
20
25
  # Shortcut Tuple as T
21
26
  T = Tuple unless defined? T
22
27
 
23
28
  module Revactor
24
- Revactor::VERSION = '0.1.2' unless defined? Revactor::VERSION
29
+ Revactor::VERSION = '0.1.3' unless defined? Revactor::VERSION
25
30
  def self.version() VERSION end
26
31
  end
27
32
 
28
- %w{actor scheduler mailbox delegator tcp filters/line filters/packet}.each do |file|
33
+ %w{
34
+ actor scheduler mailbox delegator tcp http_client
35
+ filters/line filters/packet
36
+ }.each do |file|
29
37
  require File.dirname(__FILE__) + '/revactor/' + file
30
38
  end
31
39
 
@@ -34,4 +42,5 @@ class Actor
34
42
  Actor::TCP = Revactor::TCP unless defined? Actor::TCP
35
43
  Actor::Filter = Revactor::Filter unless defined? Actor::Filter
36
44
  Actor::Delegator = Revactor::Delegator unless defined? Actor::Delegator
45
+ Actor::HttpClient = Revactor::HttpClient unless defined? Actor::HttpClient
37
46
  end
@@ -99,6 +99,11 @@ class Actor
99
99
  receive { |filter| filter.after(seconds) }
100
100
  end
101
101
 
102
+ # Run the event loop and return after processing all outstanding messages
103
+ def tick
104
+ sleep 0
105
+ end
106
+
102
107
  # Wait for messages matching a given filter. The filter object is yielded
103
108
  # to be block passed to receive. You can then invoke the when argument
104
109
  # which takes a parameter and a block. Messages are compared (using ===)
@@ -140,7 +145,7 @@ class Actor
140
145
  current.instance_eval { @dead = true }
141
146
  end
142
147
 
143
- actor = Actor.new(fiber)
148
+ actor = new(fiber)
144
149
  fiber.instance_eval { @_actor = actor }
145
150
  end
146
151
  end
@@ -181,7 +186,11 @@ class Actor
181
186
 
182
187
  # Send a message to an actor
183
188
  def <<(message)
184
- return "can't send messages to actors across threads" unless @thread == Thread.current
189
+ # Use the scheduler mailbox to send messages across threads
190
+ unless @thread == Thread.current
191
+ scheduler.mailbox.send(self, message)
192
+ return message
193
+ end
185
194
 
186
195
  # Erlang discards messages sent to dead actors, and if Erlang does it,
187
196
  # it must be the right thing to do, right? Hooray for the Erlang
@@ -14,7 +14,7 @@ module Revactor
14
14
  # length prefix followed by a message body, such as DRb. Either 16-bit
15
15
  # or 32-bit prefixes are supported.
16
16
  class Packet
17
- def initialize(size = 2)
17
+ def initialize(size = 4)
18
18
  unless size == 2 or size == 4
19
19
  raise ArgumentError, 'only 2 or 4 byte prefixes are supported'
20
20
  end
@@ -0,0 +1,349 @@
1
+ #--
2
+ # Copyright (C)2007 Tony Arcieri
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ require File.dirname(__FILE__) + '/../revactor'
8
+ require 'uri'
9
+
10
+
11
+ module Revactor
12
+ # Thrown for all HTTP-specific errors
13
+ class HttpClientError < StandardError; end
14
+
15
+ # A high performance HTTP client which wraps the asynchronous client in Rev
16
+ class HttpClient < Rev::HttpClient
17
+ # Default timeout for HTTP requests (until the response header is received)
18
+ REQUEST_TIMEOUT = 60
19
+
20
+ # Read timeout for responses from the server
21
+ READ_TIMEOUT = 30
22
+
23
+ # Maximum number of HTTP redirects to follow
24
+ MAX_REDIRECTS = 10
25
+
26
+ # Statuses which indicate the request was redirected
27
+ REDIRECT_STATUSES = [301, 302, 303, 307]
28
+
29
+ class << self
30
+ def connect(host, port = 80)
31
+ client = super
32
+ client.instance_eval { @receiver = Actor.current }
33
+ client.attach Rev::Loop.default
34
+
35
+ Actor.receive do |filter|
36
+ filter.when(T[Object, client]) do |message, _|
37
+ case message
38
+ when :http_connected
39
+ client.disable
40
+ return client
41
+ when :http_connect_failed
42
+ raise TCP::ConnectError, "connection refused"
43
+ when :http_resolve_failed
44
+ raise TCP::ResolveError, "couldn't resolve #{host}"
45
+ else raise "unexpected message for #{client.inspect}: #{message.inspect}"
46
+ end
47
+ end
48
+
49
+ filter.after(TCP::CONNECT_TIMEOUT) do
50
+ raise TCP::ConnectError, "connection timed out"
51
+ end
52
+ end
53
+ end
54
+
55
+ # Perform an HTTP request for the given method and return a response object
56
+ def request(method, uri, options = {}, &block)
57
+ follow_redirects = options.has_key?(:follow_redirects) ? options[:follow_redirects] : true
58
+ uri = URI.parse(uri)
59
+
60
+ MAX_REDIRECTS.times do
61
+ raise URI::InvalidURIError, "invalid HTTP URI: #{uri}" unless uri.is_a? URI::HTTP
62
+ request_options = uri.is_a?(URI::HTTPS) ? options.merge(:ssl => true) : options
63
+
64
+ client = connect(uri.host, uri.port)
65
+ response = client.request(method, uri.request_uri, request_options, &block)
66
+
67
+ return response unless follow_redirects and REDIRECT_STATUSES.include? response.status
68
+ response.close
69
+
70
+ location = response.headers['location']
71
+ raise "redirect with no location header: #{uri}" if location.nil?
72
+
73
+ # Append host to relative URLs
74
+ if location[0] == '/'
75
+ location = "#{uri.scheme}://#{uri.host}" << location
76
+ end
77
+
78
+ uri = URI.parse(location)
79
+ end
80
+
81
+ raise HttpClientError, "exceeded maximum of #{MAX_REDIRECTS} redirects"
82
+ end
83
+
84
+ Rev::HttpClient::ALLOWED_METHODS.each do |meth|
85
+ module_eval <<-EOD
86
+ def #{meth}(uri, options = {}, &block)
87
+ request(:#{meth}, uri, options, &block)
88
+ end
89
+ EOD
90
+ end
91
+ end
92
+
93
+ def initialize(socket)
94
+ super
95
+ @controller = @receiver ||= Actor.current
96
+ end
97
+
98
+ # Change the controlling Actor for active mode reception
99
+ # Set the controlling actor
100
+ def controller=(controller)
101
+ raise ArgumentError, "controller must be an actor" unless controller.is_a? Actor
102
+
103
+ @receiver = controller if @receiver == @controller
104
+ @controller = controller
105
+ end
106
+
107
+ # Initiate an HTTP request for the given path using the given method
108
+ # Supports the following options:
109
+ #
110
+ # ssl: Boolean
111
+ # If true, an HTTPS request will be made
112
+ #
113
+ # head: {Key: Value, Key2: Value2}
114
+ # Specify HTTP headers, e.g. {'Connection': 'close'}
115
+ #
116
+ # query: {Key: Value}
117
+ # Specify query string parameters (auto-escaped)
118
+ #
119
+ # cookies: {Key: Value}
120
+ # Specify hash of cookies (auto-escaped)
121
+ #
122
+ # body: String
123
+ # Specify the request body (you must encode it for now)
124
+ #
125
+ def request(method, path, options = {})
126
+ if options[:ssl]
127
+ ssl_handshake
128
+
129
+ Actor.receive do |filter|
130
+ filter.when(T[:https_connected, self]) do
131
+ disable
132
+ end
133
+
134
+ filter.when(T[:http_closed, self]) do
135
+ raise EOFError, "SSL handshake failed"
136
+ end
137
+
138
+ filter.after(TCP::CONNECT_TIMEOUT) do
139
+ close unless closed?
140
+ raise TCP::ConnectError, "SSL handshake timed out"
141
+ end
142
+ end
143
+ end
144
+
145
+ super
146
+ enable
147
+
148
+ Actor.receive do |filter|
149
+ filter.when(T[:http_response_header, self]) do |_, _, response_header|
150
+ return HttpResponse.new(self, response_header)
151
+ end
152
+
153
+ filter.when(T[:http_error, self, Object]) do |_, _, reason|
154
+ close unless closed?
155
+ raise HttpClientError, reason
156
+ end
157
+
158
+ filter.when(T[:http_closed, self]) do
159
+ raise EOFError, "connection closed unexpectedly"
160
+ end
161
+
162
+ filter.after(REQUEST_TIMEOUT) do
163
+ @finished = true
164
+ close unless closed?
165
+
166
+ raise HttpClientError, "request timed out"
167
+ end
168
+ end
169
+ end
170
+
171
+ #########
172
+ protected
173
+ #########
174
+
175
+ def ssl_handshake
176
+ require 'rev/ssl'
177
+ extend Rev::SSL
178
+ ssl_client_start
179
+ end
180
+
181
+ def on_connect
182
+ super
183
+ @receiver << T[:http_connected, self]
184
+ end
185
+
186
+ def on_ssl_connect
187
+ @receiver << [:https_connected, self]
188
+ end
189
+
190
+ def on_connect_failed
191
+ super
192
+ @receiver << T[:http_connect_failed, self]
193
+ end
194
+
195
+ def on_resolve_failed
196
+ super
197
+ @receiver << T[:http_resolve_failed, self]
198
+ end
199
+
200
+ def on_response_header(response_header)
201
+ disable
202
+ @receiver << T[:http_response_header, self, response_header]
203
+ end
204
+
205
+ def on_body_data(data)
206
+ disable if enabled? and not @active
207
+ @receiver << T[:http, self, data]
208
+ end
209
+
210
+ def on_request_complete
211
+ @finished = true
212
+ @receiver << T[:http_request_complete, self]
213
+ close
214
+ end
215
+
216
+ def on_close
217
+ @receiver << T[:http_closed, self] unless @finished
218
+ end
219
+
220
+ def on_error(reason)
221
+ @finished = true
222
+ @receiver << T[:http_error, self, reason]
223
+ close
224
+ end
225
+ end
226
+
227
+ # An object representing a response to an HTTP request
228
+ class HttpResponse
229
+ def initialize(client, response_header)
230
+ @client = client
231
+
232
+ # Copy these out of the original Rev response object, then discard it
233
+ @status = response_header.status
234
+ @reason = response_header.http_reason
235
+ @version = response_header.http_version
236
+ @content_length = response_header.content_length
237
+ @chunked_encoding = response_header.chunked_encoding?
238
+
239
+ # Convert header fields hash from LIKE_THIS to like-this
240
+ @headers = response_header.reduce({}) { |h, (k, v)| h[k.split('_').map(&:downcase).join('-')] = v; h }
241
+
242
+ # Extract Transfer-Encoding if available
243
+ @transfer_encoding = @headers.delete('transfer-encoding')
244
+
245
+ # Extract Content-Type if available
246
+ @content_type = @headers.delete('content-type')
247
+
248
+ # Extract Content-Encoding if available
249
+ @content_encoding = @headers.delete('content-encoding') || 'identity'
250
+ end
251
+
252
+ # The response status as an integer (e.g. 200)
253
+ attr_reader :status
254
+
255
+ # The reason returned in the http response (e.g "OK", "File not found", etc.)
256
+ attr_reader :reason
257
+
258
+ # The HTTP version returned (e.g. "HTTP/1.1")
259
+ attr_reader :version
260
+
261
+ # The encoding of the transfer
262
+ attr_reader :transfer_encoding
263
+
264
+ # The encoding of the content. Gzip encoding will be processed automatically
265
+ attr_reader :content_encoding
266
+
267
+ # The MIME type of the response's content
268
+ attr_reader :content_type
269
+
270
+ # The content length as an integer, or nil if the length is unspecified or
271
+ # the response is using chunked transfer encoding
272
+ attr_reader :content_length
273
+
274
+ # Access to the raw header fields from the request
275
+ attr_reader :headers
276
+
277
+ # Is the request encoding chunked?
278
+ def chunked_encoding?; @chunked_encoding; end
279
+
280
+ # Incrementally read the response body
281
+ def read_body
282
+ @client.controller = Actor.current
283
+ @client.enable if @client.attached? and not @client.enabled?
284
+
285
+ Actor.receive do |filter|
286
+ filter.when(T[:http, @client]) do |_, _, data|
287
+ return data
288
+ end
289
+
290
+ filter.when(T[:http_request_complete, @client]) do
291
+ return nil
292
+ end
293
+
294
+ filter.when(T[:http_error, @client]) do |_, _, reason|
295
+ raise HttpClientError, reason
296
+ end
297
+
298
+ filter.when(T[:http_closed, @client]) do
299
+ raise EOFError, "connection closed unexpectedly"
300
+ end
301
+
302
+ filter.after(HttpClient::READ_TIMEOUT) do
303
+ @finished = true
304
+ @client.close unless @client.closed?
305
+ raise HttpClientError, "read timed out"
306
+ end
307
+ end
308
+ end
309
+
310
+ # Consume the entire response body and return it as a string.
311
+ # The body is stored for subsequent access.
312
+ # A maximum body length may optionally be specified
313
+ def body(maxlength = nil)
314
+ return @body if @body
315
+ @body = ""
316
+
317
+ begin
318
+ while (data = read_body)
319
+ @body << data
320
+
321
+ if maxlength and @body.size > maxlength
322
+ raise HttpClientError, "overlength body"
323
+ end
324
+ end
325
+ rescue EOFError => ex
326
+ # If we didn't get a Content-Length and encoding isn't chunked
327
+ # we have to depend on the socket closing to detect end-of-body
328
+ # Otherwise the EOFError was unexpected and should be raised
329
+ unless (content_length.nil? or content_length.zero?) and not chunked_encoding?
330
+ raise ex
331
+ end
332
+ end
333
+
334
+ if content_length and body.size != content_length
335
+ raise HttpClientError, "body size does not match Content-Length (#{body.size} of #{content_length})"
336
+ end
337
+
338
+ @body
339
+ end
340
+
341
+ # Explicitly close the connection
342
+ def close
343
+ return if @client.closed?
344
+ @finished = true
345
+ @client.controller = Actor.current
346
+ @client.close
347
+ end
348
+ end
349
+ end
@@ -30,7 +30,7 @@ class Actor
30
30
 
31
31
  # Clear mailbox processing variables
32
32
  action = matched_index = nil
33
- processed_upto = 0
33
+ at = 0
34
34
 
35
35
  # Clear timeout variables
36
36
  @timed_out = false
@@ -43,19 +43,13 @@ class Actor
43
43
 
44
44
  # Process incoming messages
45
45
  while action.nil?
46
- @queue[processed_upto..@queue.size].each_with_index do |message, index|
47
- unless (action = filter.match message)
48
- # The filter did not match an action for the current message
49
- # Keep track of which messages we've ran the filter across so it
50
- # isn't re-run against messages it already failed to match
51
- processed_upto += 1
52
- next
53
- end
54
-
55
- # We've found a matching action, so break out of the loop
56
- matched_index = processed_upto + index
46
+ at.upto(@queue.size - 1) do |i|
47
+ next unless action = filter.match(@queue[i])
48
+ matched_index = i
57
49
  break
58
50
  end
51
+
52
+ at = @queue.size
59
53
 
60
54
  # Ignore timeouts if we've matched a message
61
55
  if action
@@ -91,6 +85,11 @@ class Actor
91
85
  def empty?
92
86
  @queue.empty?
93
87
  end
88
+
89
+ # Clear the mailbox
90
+ def clear
91
+ @queue.clear
92
+ end
94
93
 
95
94
  #######
96
95
  private
@@ -121,7 +120,8 @@ class Actor
121
120
  # Provide a pattern to match against with === and a block to call
122
121
  # when the pattern is matched.
123
122
  def when(pattern, &action)
124
- raise ArgumentError, "no block given" unless action
123
+ # Don't explicitly require an action to be specified
124
+ action ||= proc {}
125
125
  @ruleset << [pattern, action]
126
126
  end
127
127
 
@@ -131,7 +131,7 @@ class Actor
131
131
  raise ArgumentError, "timeout already specified" if @mailbox.timer
132
132
  raise ArgumentError, "must be zero or positive" if seconds < 0
133
133
 
134
- # Don't explicitly require an action to be specified for a timeout
134
+ # Don't explicitly require an action to be specified
135
135
  @mailbox.timeout_action = action || proc {}
136
136
  @mailbox.timer = Timer.new(seconds, Actor.current).attach(Rev::Loop.default)
137
137
  end
@@ -29,33 +29,41 @@ module Mongrel
29
29
  @timeout = timeout
30
30
  end
31
31
 
32
- # Runs the thing. It returns the Actor the listener is running in.
33
- def run
34
- @acceptor = Actor.spawn do
35
- begin
36
- while true
37
- begin
38
- client = @socket.accept
39
- actor = Actor.spawn client, &method(:process_client)
40
- actor[:started_on] = Time.now
41
- rescue StopServer
42
- break
43
- rescue Errno::ECONNABORTED
44
- # client closed the socket even before accept
45
- client.close rescue nil
46
- rescue Object => e
47
- STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
48
- STDERR.puts e.backtrace.join("\n")
49
- end
32
+ # Start Mongrel. This method executes the Mongrel event loop, and will
33
+ # not return until interrupted or explicitly stopped.
34
+ def start
35
+ begin
36
+ while true
37
+ begin
38
+ client = @socket.accept
39
+ actor = Actor.spawn client, &method(:process_client)
40
+ actor[:started_on] = Time.now
41
+ rescue Interrupt, StopServer
42
+ break
43
+ rescue Errno::ECONNABORTED
44
+ # client closed the socket even before accept
45
+ client.close rescue nil
46
+ rescue Object => e
47
+ STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
48
+ STDERR.puts e.backtrace.join("\n")
50
49
  end
51
- graceful_shutdown
52
- ensure
53
- @socket.close
54
- # STDERR.puts "#{Time.now}: Closed socket."
55
50
  end
51
+ graceful_shutdown
52
+ ensure
53
+ @socket.close
54
+ # STDERR.puts "#{Time.now}: Closed socket."
56
55
  end
56
+ end
57
57
 
58
- return @acceptor
58
+ # Runs the thing. Returns the Thread the server is running in.
59
+ def run
60
+ @acceptor = Thread.new { start }
61
+ end
62
+
63
+ # Clean up after any dead workers
64
+ def reap_dead_workers(reason = 'unknown')
65
+ # FIXME This should signal all workers to die
66
+ 0
59
67
  end
60
68
  end
61
69
  end
@@ -4,6 +4,7 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
+ require 'thread'
7
8
  require File.dirname(__FILE__) + '/../revactor'
8
9
 
9
10
  class Actor
@@ -12,9 +13,13 @@ class Actor
12
13
  # processed their mailboxes then the scheduler waits for any outstanding
13
14
  # Rev events. If there are no active Rev watchers then the scheduler exits.
14
15
  class Scheduler
16
+ attr_reader :mailbox
17
+
15
18
  def initialize
16
19
  @queue = []
17
20
  @running = false
21
+ @mailbox = Mailbox.new
22
+ @mailbox.attach Rev::Loop.default
18
23
  end
19
24
 
20
25
  # Schedule an Actor to be executed, and run the scheduler if it isn't
@@ -25,16 +30,21 @@ class Actor
25
30
  @queue << actor unless @queue.last == actor
26
31
 
27
32
  unless @running
28
- # Persist the fiber the scheduler runs in
29
- @fiber ||= Fiber.new do
30
- loop { run; Fiber.yield }
31
- end
32
-
33
- # Resume the scheduler
34
- @fiber.resume
33
+ # Reschedule the current Actor for execution
34
+ @queue << Actor.current
35
+
36
+ # Start the scheduler
37
+ Fiber.new { run }.resume
35
38
  end
36
39
  end
37
-
40
+
41
+ # Is the scheduler running?
42
+ def running?; @running; end
43
+
44
+ #########
45
+ protected
46
+ #########
47
+
38
48
  # Run the scheduler
39
49
  def run
40
50
  return if @running
@@ -42,35 +52,24 @@ class Actor
42
52
  @running = true
43
53
  default_loop = Rev::Loop.default
44
54
 
45
- until @queue.empty? and not default_loop.has_active_watchers?
46
- @queue.each do |actor|
55
+ while true
56
+ while actor = @queue.shift
47
57
  begin
48
58
  actor.fiber.resume
49
59
  handle_exit(actor) if actor.dead?
50
60
  rescue FiberError
51
61
  # Handle Actors whose Fibers died after being scheduled
62
+ actor.instance_eval { @dead = true }
52
63
  handle_exit(actor)
53
64
  rescue => ex
54
65
  handle_exit(actor, ex)
55
66
  end
56
67
  end
57
68
 
58
- @queue.clear
59
- default_loop.run_once if default_loop.has_active_watchers?
69
+ default_loop.run_once
60
70
  end
61
-
62
- @running = false
63
- end
64
-
65
- # Is the scheduler running?
66
- def running?
67
- @running
68
71
  end
69
-
70
- #########
71
- protected
72
- #########
73
-
72
+
74
73
  def handle_exit(actor, ex = nil)
75
74
  actor.instance_eval do
76
75
  # Mark Actor as dead
@@ -92,5 +91,33 @@ class Actor
92
91
  # FIXME this should go to a real logger
93
92
  STDERR.puts "#{ex.class}: #{[ex, *ex.backtrace].join("\n\t")}"
94
93
  end
94
+
95
+ # The Scheduler Mailbox allows messages to be safely delivered across
96
+ # threads. If a thread is sleeping sending it a message will wake
97
+ # it up.
98
+ class Mailbox < Rev::AsyncWatcher
99
+ def initialize
100
+ super
101
+
102
+ @queue = []
103
+ @lock = Mutex.new
104
+ end
105
+
106
+ def send(actor, message)
107
+ @lock.synchronize { @queue << T[actor, message] }
108
+ signal
109
+ end
110
+
111
+ #########
112
+ protected
113
+ #########
114
+
115
+ def on_signal
116
+ @lock.synchronize do
117
+ @queue.each { |actor, message| actor << message }
118
+ @queue.clear
119
+ end
120
+ end
121
+ end
95
122
  end
96
123
  end
data/lib/revactor/tcp.rb CHANGED
@@ -31,7 +31,7 @@ module Revactor
31
31
  socket.attach Rev::Loop.default
32
32
 
33
33
  Actor.receive do |filter|
34
- filter.when(Case[Object, socket]) do |message, _|
34
+ filter.when(T[Object, socket]) do |message, _|
35
35
  case message
36
36
  when :tcp_connected
37
37
  return socket
@@ -56,6 +56,11 @@ module Revactor
56
56
  #
57
57
  # :controller - The controlling actor, default Actor.current
58
58
  #
59
+ # :filter - An symbol/class or array of symbols/classes which implement
60
+ # #encode and #decode methods to transform data sent and
61
+ # received data respectively via Revactor::TCP::Socket.
62
+ # See the "Filters" section in the README for more information
63
+ #
59
64
  def self.listen(addr, port, options = {})
60
65
  Listener.new(addr, port, options).attach(Rev::Loop.default).disable
61
66
  end
@@ -77,12 +82,13 @@ module Revactor
77
82
  # :filter - An symbol/class or array of symbols/classes which implement
78
83
  # #encode and #decode methods to transform data sent and
79
84
  # received data respectively via Revactor::TCP::Socket.
85
+ # See the "Filters" section in the README for more information
80
86
  #
81
87
  def connect(host, port, options = {})
82
88
  options[:active] ||= false
83
89
  options[:controller] ||= Actor.current
84
90
 
85
- super(host, port, options).instance_eval {
91
+ super.instance_eval {
86
92
  @active, @controller = options[:active], options[:controller]
87
93
  @filterset = initialize_filter(*options[:filter])
88
94
  self
@@ -112,6 +118,7 @@ module Revactor
112
118
  # false - Receiving data is disabled
113
119
  # :once - A single message will be sent to the controlling actor
114
120
  # then active mode will be disabled
121
+ #
115
122
  def active=(state)
116
123
  unless @receiver == @controller
117
124
  raise "cannot change active state during a synchronous call"
@@ -162,7 +169,7 @@ module Revactor
162
169
 
163
170
  loop do
164
171
  Actor.receive do |filter|
165
- filter.when(Case[:tcp, self, Object]) do |_, _, data|
172
+ filter.when(T[:tcp, self]) do |_, _, data|
166
173
  if length.nil?
167
174
  @receiver = @controller
168
175
  @active = active
@@ -182,7 +189,7 @@ module Revactor
182
189
  end
183
190
  end
184
191
 
185
- filter.when(Case[:tcp_closed, self]) do
192
+ filter.when(T[:tcp_closed, self]) do
186
193
  unless @receiver == @controller
187
194
  @receiver = @controller
188
195
  @receiver << T[:tcp_closed, self]
@@ -207,19 +214,16 @@ module Revactor
207
214
  super(encode(data))
208
215
 
209
216
  Actor.receive do |filter|
210
- filter.when(Case[:tcp_write_complete, self]) do
217
+ filter.when(T[:tcp_write_complete, self]) do
211
218
  @receiver = @controller
212
219
  @active = active
213
- enable if @active
220
+ enable if @active and not enabled?
214
221
 
215
222
  return data.size
216
223
  end
217
224
 
218
- filter.when(Case[:tcp_closed, self]) do
219
- @receiver = @controller
220
- @active = active
221
- enable if @active
222
-
225
+ filter.when(T[:tcp_closed, self]) do
226
+ @active = false
223
227
  raise EOFError, "connection closed"
224
228
  end
225
229
  end
@@ -307,7 +311,7 @@ module Revactor
307
311
 
308
312
  if message.is_a?(Array) and not message.empty?
309
313
  message.each { |msg| @receiver << T[:tcp, self, msg] }
310
- elsif message
314
+ elsif message and not message.empty?
311
315
  @receiver << T[:tcp, self, message]
312
316
  else return
313
317
  end
@@ -334,6 +338,11 @@ module Revactor
334
338
  #
335
339
  # :controller - The controlling actor, default Actor.current
336
340
  #
341
+ # :filter - An symbol/class or array of symbols/classes which implement
342
+ # #encode and #decode methods to transform data sent and
343
+ # received data respectively via Revactor::TCP::Socket.
344
+ # See the "Filters" section in the README for more information
345
+ #
337
346
  def initialize(host, port, options = {})
338
347
  super(host, port)
339
348
  opts = {
@@ -378,7 +387,7 @@ module Revactor
378
387
  enable
379
388
 
380
389
  Actor.receive do |filter|
381
- filter.when(Case[:tcp_connection, self, Object]) do |_, _, sock|
390
+ filter.when(T[:tcp_connection, self]) do |_, _, sock|
382
391
  @accepting = false
383
392
  return sock
384
393
  end
data/revactor.gemspec CHANGED
@@ -2,10 +2,10 @@ require 'rubygems'
2
2
 
3
3
  GEMSPEC = Gem::Specification.new do |s|
4
4
  s.name = "revactor"
5
- s.version = "0.1.2"
5
+ s.version = "0.1.3"
6
6
  s.authors = "Tony Arcieri"
7
7
  s.email = "tony@medioh.com"
8
- s.date = "2008-1-28"
8
+ s.date = "2008-1-29"
9
9
  s.summary = "Revactor is an Actor implementation for writing high performance concurrent programs"
10
10
  s.platform = Gem::Platform::RUBY
11
11
  s.required_ruby_version = '>= 1.9.0'
@@ -14,8 +14,7 @@ GEMSPEC = Gem::Specification.new do |s|
14
14
  s.files = Dir.glob("{lib,examples,tools,spec}/**/*") + ['Rakefile', 'revactor.gemspec']
15
15
 
16
16
  # Dependencies
17
- s.add_dependency("rev", ">= 0.1.4")
18
- s.add_dependency("case", ">= 0.4")
17
+ s.add_dependency("rev", ">= 0.2.0")
19
18
 
20
19
  # RubyForge info
21
20
  s.homepage = "http://revactor.org"
data/spec/actor_spec.rb CHANGED
@@ -44,6 +44,9 @@ describe Actor do
44
44
  end
45
45
 
46
46
  actor << :foo
47
+
48
+ # Hack to run the Actor scheduler once
49
+ Actor.sleep 0
47
50
  @actor_run.should be_true
48
51
  end
49
52
 
@@ -62,6 +65,9 @@ describe Actor do
62
65
  end
63
66
 
64
67
  ['first message', 'second message', 'third message'].each { |m| actor << m }
68
+
69
+ # Hack to run the Actor scheduler once
70
+ Actor.sleep 0
65
71
  @actor_run.should be_true
66
72
  end
67
73
 
@@ -73,6 +79,9 @@ describe Actor do
73
79
  end.should == :right
74
80
  @actor_run = true
75
81
  end
82
+
83
+ # Hack to run the Actor scheduler
84
+ Actor.sleep 0.02
76
85
  @actor_run.should be_true
77
86
  end
78
87
 
@@ -90,6 +99,9 @@ describe Actor do
90
99
  end
91
100
 
92
101
  [:foo, :bar, :baz].each { |m| actor << m }
102
+
103
+ # Hack to run the Actor scheduler once
104
+ Actor.sleep 0
93
105
  @actor_run.should be_true
94
106
  end
95
107
  end
@@ -139,6 +151,9 @@ describe Actor do
139
151
 
140
152
  actor.dead?.should be_false
141
153
  actor << :foobar
154
+
155
+ # Hack to run the Actor scheduler once
156
+ Actor.sleep 0
142
157
  actor.dead?.should be_true
143
158
  end
144
159
  end
metadata CHANGED
@@ -1,33 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.4
3
- specification_version: 1
4
2
  name: revactor
5
3
  version: !ruby/object:Gem::Version
6
- version: 0.1.2
7
- date: 2008-01-28 00:00:00 -07:00
8
- summary: Revactor is an Actor implementation for writing high performance concurrent programs
9
- require_paths:
10
- - lib
11
- email: tony@medioh.com
12
- homepage: http://revactor.org
13
- rubyforge_project: revactor
14
- description:
15
- autorequire:
16
- default_executable:
17
- bindir: bin
18
- has_rdoc: true
19
- required_ruby_version: !ruby/object:Gem::Version::Requirement
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 1.9.0
24
- version:
4
+ version: 0.1.3
25
5
  platform: ruby
26
- signing_key:
27
- cert_chain:
28
- post_install_message:
29
6
  authors:
30
7
  - Tony Arcieri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-01-29 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rev
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.2.0
23
+ version:
24
+ description:
25
+ email: tony@medioh.com
26
+ executables: []
27
+
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - LICENSE
32
+ - README
33
+ - CHANGES
31
34
  files:
32
35
  - lib/revactor
33
36
  - lib/revactor/actor.rb
@@ -35,15 +38,16 @@ files:
35
38
  - lib/revactor/filters
36
39
  - lib/revactor/filters/line.rb
37
40
  - lib/revactor/filters/packet.rb
41
+ - lib/revactor/http_client.rb
38
42
  - lib/revactor/mailbox.rb
39
43
  - lib/revactor/mongrel.rb
40
44
  - lib/revactor/scheduler.rb
41
45
  - lib/revactor/tcp.rb
42
46
  - lib/revactor.rb
47
+ - examples/chat_server.rb
43
48
  - examples/echo_server.rb
44
49
  - examples/google.rb
45
50
  - examples/mongrel.rb
46
- - tools/messaging_throughput.rb
47
51
  - spec/actor_spec.rb
48
52
  - spec/delegator_spec.rb
49
53
  - spec/line_filter_spec.rb
@@ -54,40 +58,35 @@ files:
54
58
  - LICENSE
55
59
  - README
56
60
  - CHANGES
57
- test_files: []
58
-
61
+ has_rdoc: true
62
+ homepage: http://revactor.org
63
+ post_install_message:
59
64
  rdoc_options:
60
65
  - --title
61
66
  - Revactor
62
67
  - --main
63
68
  - README
64
69
  - --line-numbers
65
- extra_rdoc_files:
66
- - LICENSE
67
- - README
68
- - CHANGES
69
- executables: []
70
-
71
- extensions: []
72
-
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 1.9.0
77
+ version:
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "0"
83
+ version:
73
84
  requirements: []
74
85
 
75
- dependencies:
76
- - !ruby/object:Gem::Dependency
77
- name: rev
78
- version_requirement:
79
- version_requirements: !ruby/object:Gem::Version::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: 0.1.4
84
- version:
85
- - !ruby/object:Gem::Dependency
86
- name: case
87
- version_requirement:
88
- version_requirements: !ruby/object:Gem::Version::Requirement
89
- requirements:
90
- - - ">="
91
- - !ruby/object:Gem::Version
92
- version: "0.4"
93
- version:
86
+ rubyforge_project: revactor
87
+ rubygems_version: 1.0.1
88
+ signing_key:
89
+ specification_version: 2
90
+ summary: Revactor is an Actor implementation for writing high performance concurrent programs
91
+ test_files: []
92
+
@@ -1,31 +0,0 @@
1
- require File.dirname(__FILE__) + '/../lib/revactor'
2
-
3
- NTIMES=100000
4
-
5
- begin_time = Time.now
6
-
7
- puts "#{begin_time.strftime('%H:%M:%S')} -- Sending #{NTIMES} messages"
8
-
9
- parent = Actor.current
10
- child = Actor.spawn do
11
- (NTIMES / 2).times do
12
- Actor.receive do |f|
13
- f.when(:foo) { parent << :bar }
14
- end
15
- end
16
- end
17
-
18
- child << :foo
19
- (NTIMES / 2).times do
20
- Actor.receive do |f|
21
- f.when(:bar) { child << :foo }
22
- end
23
- end
24
-
25
- end_time = Time.now
26
- duration = end_time - begin_time
27
- throughput = NTIMES / duration
28
-
29
- puts "#{end_time.strftime('%H:%M:%S')} -- Finished"
30
- puts "Duration: #{sprintf("%0.2f", duration)} seconds"
31
- puts "Throughput: #{sprintf("%0.2f", throughput)} messages per second"