revactor 0.1.4 → 0.1.5

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.5:
2
+
3
+ * Add Revactor::HttpFetcher, a concurrent HTTP fetcher using
4
+ Revactor::HttpClient
5
+
6
+ * Allow Revactor::HttpClient to take a block for requests, and handle
7
+ closing sockets automatically when the block has been evaluated
8
+
9
+ * Change Revactor::Filter setup to express initialize args as Tuples
10
+
1
11
  0.1.4:
2
12
 
3
13
  * Fix bungled 0.1.3 release :(
@@ -0,0 +1,40 @@
1
+ # An Actor ring example
2
+ #
3
+ # Here we construct a ring of interconnected Actors which each know the
4
+ # next Actor to send messages to. Any message sent from the parent Actor
5
+ # is delivered around the ring and back to the parent.
6
+
7
+ require 'rubygems'
8
+ require 'revactor'
9
+
10
+ NCHILDREN = 5
11
+ NAROUND = 5
12
+
13
+ class RingNode
14
+ extend Actorize
15
+
16
+ def initialize(next_node)
17
+ loop do
18
+ Actor.receive do |filter|
19
+ filter.when(Object) do |msg|
20
+ puts "#{Actor.current} got #{msg}"
21
+ next_node << msg
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ next_node = Actor.current
29
+ NCHILDREN.times { next_node = RingNode.spawn(next_node) }
30
+
31
+ next_node << NAROUND
32
+
33
+ loop do
34
+ Actor.receive do |filter|
35
+ filter.when(Object) do |n|
36
+ exit if n.zero?
37
+ next_node << n - 1
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # An example of trapping exit messages
2
+ #
3
+ # Here we create a new Actor which raises an unhandled exception
4
+ # whenever it receives the :die message.
5
+ #
6
+ # The parent Actor is linked to this one, and is set to trap exits
7
+ # When the child raises the unhandled exception, the exit message
8
+ # is delivered back to the parent.
9
+
10
+ require 'rubygems'
11
+ require 'revactor'
12
+
13
+ actor = Actor.spawn_link do
14
+ Actor.receive do |filter|
15
+ filter.when(:die) { raise "Aieeee!" }
16
+ end
17
+ end
18
+
19
+ Actor.current.trap_exit = true
20
+
21
+ actor << :die
22
+ p Actor.receive { |filter| filter.when(Object) { |msg| msg } }
@@ -27,12 +27,12 @@ end
27
27
  T = Tuple unless defined? T
28
28
 
29
29
  module Revactor
30
- Revactor::VERSION = '0.1.4' unless defined? Revactor::VERSION
30
+ Revactor::VERSION = '0.1.5' unless defined? Revactor::VERSION
31
31
  def self.version() VERSION end
32
32
  end
33
33
 
34
34
  %w{
35
- actor scheduler mailbox tcp http_client
35
+ actor scheduler mailbox tcp unix http_client
36
36
  filters/line filters/packet actorize
37
37
  }.each do |file|
38
38
  require File.dirname(__FILE__) + '/revactor/' + file
@@ -41,6 +41,7 @@ end
41
41
  # Place Revactor modules and classes under the Actor namespace
42
42
  class Actor
43
43
  Actor::TCP = Revactor::TCP unless defined? Actor::TCP
44
+ Actor::UNIX = Revactor::UNIX unless defined? Actor::UNIX
44
45
  Actor::Filter = Revactor::Filter unless defined? Actor::Filter
45
46
  Actor::HttpClient = Revactor::HttpClient unless defined? Actor::HttpClient
46
47
  end
@@ -141,11 +141,11 @@ class Actor
141
141
  def _spawn(*args, &block)
142
142
  fiber = Fiber.new do
143
143
  block.call(*args)
144
- current.instance_eval { @dead = true }
144
+ current.instance_variable_set :@dead, true
145
145
  end
146
146
 
147
147
  actor = new(fiber)
148
- fiber.instance_eval { @_actor = actor }
148
+ fiber.instance_variable_set :@_actor, actor
149
149
  end
150
150
  end
151
151
 
@@ -45,9 +45,9 @@ module Revactor
45
45
 
46
46
  # Encode lines using the current delimiter
47
47
  def encode(*data)
48
- data.reduce("") { |str, d| str << d << @delimiter }
48
+ data.inject("") { |str, d| str << d << @delimiter }
49
49
  end
50
50
  end
51
51
  end
52
52
  end
53
-
53
+
@@ -23,7 +23,7 @@ module Revactor
23
23
  @data_size = 0
24
24
 
25
25
  @mode = :prefix
26
- @buffer = Rev::Buffer.new
26
+ @buffer = IO::Buffer.new
27
27
  end
28
28
 
29
29
  # Callback for processing incoming frames
@@ -49,7 +49,7 @@ module Revactor
49
49
 
50
50
  # Send a packet with a specified size prefix
51
51
  def encode(*data)
52
- data.reduce('') do |s, d|
52
+ data.inject('') do |s, d|
53
53
  raise ArgumentError, 'packet too long for prefix length' if d.size >= 256 ** @prefix_size
54
54
  s << [d.size].pack(@prefix_size == 2 ? 'n' : 'N') << d
55
55
  end
@@ -27,7 +27,7 @@ module Revactor
27
27
  class << self
28
28
  def connect(host, port = 80)
29
29
  client = super
30
- client.instance_eval { @receiver = Actor.current }
30
+ client.instance_variable_set :@receiver, Actor.current
31
31
  client.attach Rev::Loop.default
32
32
 
33
33
  Actor.receive do |filter|
@@ -45,13 +45,14 @@ module Revactor
45
45
  end
46
46
 
47
47
  filter.after(TCP::CONNECT_TIMEOUT) do
48
+ client.close unless client.closed?
48
49
  raise TCP::ConnectError, "connection timed out"
49
50
  end
50
51
  end
51
52
  end
52
53
 
53
54
  # Perform an HTTP request for the given method and return a response object
54
- def request(method, uri, options = {}, &block)
55
+ def request(method, uri, options = {})
55
56
  follow_redirects = options.has_key?(:follow_redirects) ? options[:follow_redirects] : true
56
57
  uri = URI.parse(uri)
57
58
 
@@ -60,17 +61,29 @@ module Revactor
60
61
  request_options = uri.is_a?(URI::HTTPS) ? options.merge(:ssl => true) : options
61
62
 
62
63
  client = connect(uri.host, uri.port)
63
- response = client.request(method, uri.request_uri, request_options, &block)
64
+ response = client.request(method, uri.request_uri, request_options)
65
+
66
+ # Request complete
67
+ unless follow_redirects and REDIRECT_STATUSES.include? response.status
68
+ return response unless block_given?
69
+
70
+ begin
71
+ yield response
72
+ ensure
73
+ response.close
74
+ end
75
+
76
+ return
77
+ end
64
78
 
65
- return response unless follow_redirects and REDIRECT_STATUSES.include? response.status
66
79
  response.close
67
80
 
68
81
  location = response.headers['location']
69
82
  raise "redirect with no location header: #{uri}" if location.nil?
70
83
 
71
- # Append host to relative URLs
72
- if location[0] == '/'
73
- location = "#{uri.scheme}://#{uri.host}" << location
84
+ # Convert path-based redirects to URIs
85
+ unless /^[a-z]+:\/\// === location
86
+ location = "#{uri.scheme}://#{uri.host}" << File.expand_path(location, uri.path)
74
87
  end
75
88
 
76
89
  uri = URI.parse(location)
@@ -235,7 +248,9 @@ module Revactor
235
248
  @chunked_encoding = response_header.chunked_encoding?
236
249
 
237
250
  # Convert header fields hash from LIKE_THIS to like-this
238
- @headers = response_header.reduce({}) { |h, (k, v)| h[k.split('_').map(&:downcase).join('-')] = v; h }
251
+ @headers = response_header.inject({}) do |h, (k, v)|
252
+ h[k.split('_').map(&:downcase).join('-')] = v; h
253
+ end
239
254
 
240
255
  # Extract Transfer-Encoding if available
241
256
  @transfer_encoding = @headers.delete('transfer-encoding')
@@ -344,4 +359,4 @@ module Revactor
344
359
  @client.close
345
360
  end
346
361
  end
347
- end
362
+ end
@@ -0,0 +1,100 @@
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 'zlib'
8
+ require 'stringio'
9
+
10
+ module Revactor
11
+ # A concurrent HTTP fetcher, implemented using a central dispatcher which
12
+ # scatters requests to a worker pool.
13
+ #
14
+ # The HttpFetcher class is callback-driven and intended for subclassing.
15
+ # When a request completes successfully, the on_success callback is called.
16
+ # An on_failure callback represents non-200 HTTP responses, and on_error
17
+ # delivers any exceptions which occured during the fetch.
18
+ class HttpFetcher
19
+ def initialize(nworkers = 8)
20
+ @_nworkers = nworkers
21
+ @_workers, @_queue = [], []
22
+ nworkers.times { @_workers << Worker.spawn(Actor.current) }
23
+ end
24
+
25
+ def get(url, *args)
26
+ if @_workers.empty?
27
+ @_queue << T[url, args]
28
+ else
29
+ @_workers.shift << T[:fetch, url, args]
30
+ end
31
+ end
32
+
33
+ def run
34
+ while true
35
+ Actor.receive do |filter|
36
+ filter.when(T[:ready]) do |_, worker|
37
+ if @_queue.empty?
38
+ @_workers << worker
39
+ on_empty if @_workers.size == @_nworkers
40
+ else
41
+ worker << T[:fetch, *@_queue.shift]
42
+ end
43
+ end
44
+
45
+ filter.when(T[:fetched]) { |_, url, document, args| on_success url, document, *args }
46
+ filter.when(T[:failed]) { |_, url, status, args| on_failure url, status, *args }
47
+ filter.when(T[:error]) { |_, url, ex, args| on_error url, ex, *args }
48
+ end
49
+ end
50
+ end
51
+
52
+ def on_success(url, document, *args); end
53
+ def on_failure(url, status, *args); end
54
+ def on_error(url, ex, *args); end
55
+ def on_empty; exit; end
56
+
57
+ class Worker
58
+ extend Actorize
59
+
60
+ def initialize(fetcher)
61
+ @fetcher = fetcher
62
+ loop { wait_for_request }
63
+ end
64
+
65
+ def wait_for_request
66
+ Actor.receive do |filter|
67
+ filter.when(T[:fetch]) do |_, url, args|
68
+ begin
69
+ fetch url, args
70
+ rescue => ex
71
+ @fetcher << T[:error, url, ex, args]
72
+ end
73
+
74
+ # FIXME this should be unnecessary, but the HTTP client "leaks" messages
75
+ Actor.current.mailbox.clear
76
+ @fetcher << T[:ready, Actor.current]
77
+ end
78
+ end
79
+ end
80
+
81
+ def fetch(url, args)
82
+ Actor::HttpClient.get(url, :head => {'Accept-Encoding' => 'gzip'}) do |response|
83
+ if response.status == 200
84
+ @fetcher << T[:fetched, url, decode_body(response), args]
85
+ else
86
+ @fetcher << T[:failed, url, response.status, args]
87
+ end
88
+ end
89
+ end
90
+
91
+ def decode_body(response)
92
+ if response.content_encoding == 'gzip'
93
+ Zlib::GzipReader.new(StringIO.new(response.body)).read
94
+ else
95
+ response.body
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -58,7 +58,7 @@ class Actor
58
58
  handle_exit(actor) if actor.dead?
59
59
  rescue FiberError
60
60
  # Handle Actors whose Fibers died after being scheduled
61
- actor.instance_eval { @dead = true }
61
+ actor.instance_variable_set :@dead, true
62
62
  handle_exit(actor)
63
63
  rescue => ex
64
64
  handle_exit(actor, ex)
@@ -12,6 +12,12 @@ module Revactor
12
12
  # Number of seconds to wait for a connection
13
13
  CONNECT_TIMEOUT = 10
14
14
 
15
+ # Raised when a read from a client or server fails
16
+ class ReadError < StandardError; end
17
+
18
+ # Raised when a write to a client or server fails
19
+ class WriteError < StandardError; end
20
+
15
21
  # Raised when a connection to a remote server fails
16
22
  class ConnectError < StandardError; end
17
23
 
@@ -88,7 +94,7 @@ module Revactor
88
94
 
89
95
  super.instance_eval {
90
96
  @active, @controller = options[:active], options[:controller]
91
- @filterset = initialize_filter(*options[:filter])
97
+ @filterset = [*initialize_filter(options[:filter])]
92
98
  self
93
99
  }
94
100
  end
@@ -99,10 +105,10 @@ module Revactor
99
105
 
100
106
  @active ||= options[:active] || false
101
107
  @controller ||= options[:controller] || Actor.current
102
- @filterset ||= initialize_filter(*options[:filter])
108
+ @filterset ||= [*initialize_filter(options[:filter])]
103
109
 
104
110
  @receiver = @controller
105
- @read_buffer = Rev::Buffer.new
111
+ @read_buffer = IO::Buffer.new
106
112
  end
107
113
 
108
114
  def inspect
@@ -152,7 +158,7 @@ module Revactor
152
158
  # Read data from the socket synchronously. If a length is specified
153
159
  # then the call blocks until the given length has been read. Otherwise
154
160
  # the call blocks until it receives any data.
155
- def read(length = nil)
161
+ def read(length = nil, options = {})
156
162
  # Only one synchronous call allowed at a time
157
163
  raise "already being called synchronously" unless @receiver == @controller
158
164
 
@@ -195,12 +201,16 @@ module Revactor
195
201
 
196
202
  raise EOFError, "connection closed"
197
203
  end
204
+
205
+ if timeout = options[:timeout]
206
+ filter.after(timeout) { raise ReadError, "read timed out" }
207
+ end
198
208
  end
199
209
  end
200
210
  end
201
211
 
202
212
  # Write data to the socket. The call blocks until all data has been written.
203
- def write(data)
213
+ def write(data, options = {})
204
214
  # Only one synchronous call allowed at a time
205
215
  raise "already being called synchronously" unless @receiver == @controller
206
216
 
@@ -224,6 +234,10 @@ module Revactor
224
234
  @active = false
225
235
  raise EOFError, "connection closed"
226
236
  end
237
+
238
+ if timeout = options[:timeout]
239
+ filter.after(timeout) { raise WriteError, "write timed out" }
240
+ end
227
241
  end
228
242
  end
229
243
 
@@ -237,26 +251,26 @@ module Revactor
237
251
  # Filter setup
238
252
  #
239
253
 
240
- # Initialize filter change
241
- def initialize_filter(*filterset)
242
- return filterset if filterset.empty?
243
-
244
- filterset.map do |filter|
245
- case filter
246
- when Array
247
- name = filter.shift
248
- case name
249
- when Class
250
- name.new(*filter)
251
- when Symbol
252
- symbol_to_filter(name).new(*filter)
253
- else raise ArgumentError, "unrecognized filter type: #{name.class}"
254
- end
254
+ # Initialize filters
255
+ def initialize_filter(filter)
256
+ case filter
257
+ when NilClass
258
+ []
259
+ when Tuple
260
+ name, *args = filter
261
+ case name
255
262
  when Class
256
- filter.new
263
+ name.new(*args)
257
264
  when Symbol
258
- symbol_to_filter(filter).new
265
+ symbol_to_filter(name).new(*args)
266
+ else raise ArgumentError, "unrecognized filter type: #{name.class}"
259
267
  end
268
+ when Array
269
+ filter.map { |f| initialize_filter f }
270
+ when Class
271
+ filter.new
272
+ when Symbol
273
+ symbol_to_filter(filter).new
260
274
  end
261
275
  end
262
276
 
@@ -271,8 +285,8 @@ module Revactor
271
285
 
272
286
  # Decode data through the filter chain
273
287
  def decode(data)
274
- @filterset.reduce([data]) do |a, filter|
275
- a.reduce([]) do |a2, d|
288
+ @filterset.inject([data]) do |a, filter|
289
+ a.inject([]) do |a2, d|
276
290
  a2 + filter.decode(d)
277
291
  end
278
292
  end
@@ -280,7 +294,7 @@ module Revactor
280
294
 
281
295
  # Encode data through the filter chain
282
296
  def encode(message)
283
- @filterset.reverse.reduce(message) { |m, filter| filter.encode(*m) }
297
+ @filterset.reverse.inject(message) { |m, filter| filter.encode(*m) }
284
298
  end
285
299
 
286
300
  #
@@ -0,0 +1,400 @@
1
+ #--
2
+ # Copyright (C)2009 Eric Wong
3
+ # You can redistribute this under the terms of the Ruby license
4
+ # See file LICENSE for details
5
+ #++
6
+
7
+ module Revactor
8
+ # The UNIX module holds all Revactor functionality related to the
9
+ # UNIX domain sockets, including drop-in replacements
10
+ # for Ruby UNIX Sockets which can operate concurrently using Actors.
11
+ module UNIX
12
+ # Number of seconds to wait for a connection
13
+ CONNECT_TIMEOUT = 10
14
+
15
+ # Raised when a connection to a server fails
16
+ class ConnectError < StandardError; end
17
+
18
+ # Connect to the specified path for a UNIX domain socket
19
+ # Accepts the following options:
20
+ #
21
+ # :active - Controls how data is read from the socket. See the
22
+ # documentation for Revactor::UNIX::Socket#active=
23
+ #
24
+ def self.connect(path, options = {})
25
+ socket = begin
26
+ Socket.connect path, options
27
+ rescue SystemCallError
28
+ raise ConnectError, "connection refused"
29
+ end
30
+ socket.attach Rev::Loop.default
31
+ end
32
+
33
+ # Listen on the specified path. Accepts the following options:
34
+ #
35
+ # :active - Default active setting for new connections. See the
36
+ # documentation Rev::UNIX::Socket#active= for more info
37
+ #
38
+ # :controller - The controlling actor, default Actor.current
39
+ #
40
+ # :filter - An symbol/class or array of symbols/classes which implement
41
+ # #encode and #decode methods to transform data sent and
42
+ # received data respectively via Revactor::UNIX::Socket.
43
+ # See the "Filters" section in the README for more information
44
+ #
45
+ def self.listen(path, options = {})
46
+ Listener.new(path, options).attach(Rev::Loop.default).disable
47
+ end
48
+
49
+ # UNIX socket class, returned by Revactor::UNIX.connect and
50
+ # Revactor::UNIX::Listener#accept
51
+ class Socket < Rev::UNIXSocket
52
+ attr_reader :controller
53
+
54
+ class << self
55
+ # Connect to the specified path. Accepts the following options:
56
+ #
57
+ # :active - Controls how data is read from the socket. See the
58
+ # documentation for #active=
59
+ #
60
+ # :controller - The controlling actor, default Actor.current
61
+ #
62
+ # :filter - An symbol/class or array of symbols/classes which
63
+ # implement #encode and #decode methods to transform
64
+ # data sent and received data respectively via
65
+ # Revactor::UNIX::Socket. See the "Filters" section
66
+ # in the README for more information
67
+ #
68
+ def connect(path, options = {})
69
+ options[:active] ||= false
70
+ options[:controller] ||= Actor.current
71
+
72
+ super.instance_eval {
73
+ @active, @controller = options[:active], options[:controller]
74
+ @filterset = [*initialize_filter(options[:filter])]
75
+ self
76
+ }
77
+ end
78
+ end
79
+
80
+ def initialize(socket, options = {})
81
+ super(socket)
82
+
83
+ @active ||= options[:active] || false
84
+ @controller ||= options[:controller] || Actor.current
85
+ @filterset ||= [*initialize_filter(options[:filter])]
86
+
87
+ @receiver = @controller
88
+ @read_buffer = IO::Buffer.new
89
+ end
90
+
91
+ def inspect
92
+ "#<#{self.class}:0x#{object_id.to_s(16)} #@address_family:#@path"
93
+ end
94
+
95
+ # Enable or disable active mode data reception. State can be any
96
+ # of the following:
97
+ #
98
+ # true - All received data is sent to the controlling actor
99
+ # false - Receiving data is disabled
100
+ # :once - A single message will be sent to the controlling actor
101
+ # then active mode will be disabled
102
+ #
103
+ def active=(state)
104
+ unless @receiver == @controller
105
+ raise "cannot change active state during a synchronous call"
106
+ end
107
+
108
+ unless [true, false, :once].include? state
109
+ raise ArgumentError, "must be true, false, or :once"
110
+ end
111
+
112
+ if [true, :once].include?(state)
113
+ unless @read_buffer.empty?
114
+ @receiver << [:unix, self, @read_buffer.read]
115
+ return if state == :once
116
+ end
117
+
118
+ enable unless enabled?
119
+ end
120
+
121
+ @active = state
122
+ end
123
+
124
+ # Is the socket in active mode?
125
+ def active?; @active; end
126
+
127
+ # Set the controlling actor
128
+ def controller=(controller)
129
+ Actor === controller or
130
+ raise ArgumentError, "controller must be an actor"
131
+
132
+ @receiver = controller if @receiver == @controller
133
+ @controller = controller
134
+ end
135
+
136
+ # Read data from the socket synchronously. If a length is specified
137
+ # then the call blocks until the given length has been read. Otherwise
138
+ # the call blocks until it receives any data.
139
+ def read(length = nil)
140
+ # Only one synchronous call allowed at a time
141
+ @receiver == @controller or
142
+ raise "already being called synchronously"
143
+
144
+ unless @read_buffer.empty? or (length and @read_buffer.size < length)
145
+ return @read_buffer.read(length)
146
+ end
147
+
148
+ active = @active
149
+ @active = :once
150
+ @receiver = Actor.current
151
+ enable unless enabled?
152
+
153
+ loop do
154
+ Actor.receive do |filter|
155
+ filter.when(T[:unix, self]) do |_, _, data|
156
+ if length.nil?
157
+ @receiver = @controller
158
+ @active = active
159
+ enable if @active
160
+
161
+ return data
162
+ end
163
+
164
+ @read_buffer << data
165
+
166
+ if @read_buffer.size >= length
167
+ @receiver = @controller
168
+ @active = active
169
+ enable if @active
170
+
171
+ return @read_buffer.read(length)
172
+ end
173
+ end
174
+
175
+ filter.when(T[:unix_closed, self]) do
176
+ unless @receiver == @controller
177
+ @receiver = @controller
178
+ @receiver << T[:unix_closed, self]
179
+ end
180
+
181
+ raise EOFError, "connection closed"
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # Write data to the socket. The call blocks until all data has been
188
+ # written.
189
+ def write(data)
190
+ # Only one synchronous call allowed at a time
191
+ @receiver == @controller or
192
+ raise "already being called synchronously"
193
+
194
+ active = @active
195
+ @active = false
196
+ @receiver = Actor.current
197
+ disable if @active
198
+
199
+ super(encode(data))
200
+
201
+ Actor.receive do |filter|
202
+ filter.when(T[:unix_write_complete, self]) do
203
+ @receiver = @controller
204
+ @active = active
205
+ enable if @active and not enabled?
206
+
207
+ return data.size
208
+ end
209
+
210
+ filter.when(T[:unix_closed, self]) do
211
+ @active = false
212
+ raise EOFError, "connection closed"
213
+ end
214
+ end
215
+ end
216
+
217
+ alias_method :<<, :write
218
+
219
+ #########
220
+ protected
221
+ #########
222
+
223
+ #
224
+ # Filter setup
225
+ #
226
+
227
+ # Initialize filters
228
+ def initialize_filter(filter)
229
+ case filter
230
+ when NilClass
231
+ []
232
+ when Tuple
233
+ name, *args = filter
234
+ case name
235
+ when Class
236
+ name.new(*args)
237
+ when Symbol
238
+ symbol_to_filter(name).new(*args)
239
+ else raise ArgumentError, "unrecognized filter type: #{name.class}"
240
+ end
241
+ when Array
242
+ filter.map { |f| initialize_filter f }
243
+ when Class
244
+ filter.new
245
+ when Symbol
246
+ symbol_to_filter(filter).new
247
+ end
248
+ end
249
+
250
+ # Lookup filters referenced as symbols
251
+ def symbol_to_filter(filter)
252
+ case filter
253
+ when :line then Revactor::Filter::Line
254
+ when :packet then Revactor::Filter::Packet
255
+ else raise ArgumentError, "unrecognized filter type: #{filter}"
256
+ end
257
+ end
258
+
259
+ # Decode data through the filter chain
260
+ def decode(data)
261
+ @filterset.inject([data]) do |a, filter|
262
+ a.inject([]) do |a2, d|
263
+ a2 + filter.decode(d)
264
+ end
265
+ end
266
+ end
267
+
268
+ # Encode data through the filter chain
269
+ def encode(message)
270
+ @filterset.reverse.inject(message) { |m, filter| filter.encode(*m) }
271
+ end
272
+
273
+ #
274
+ # Rev::UNIXSocket callback
275
+ #
276
+
277
+ def on_connect
278
+ @receiver << T[:unix_connected, self]
279
+ end
280
+
281
+ def on_connect_failed
282
+ @receiver << T[:unix_connect_failed, self]
283
+ end
284
+
285
+ def on_close
286
+ @receiver << T[:unix_closed, self]
287
+ end
288
+
289
+ def on_read(data)
290
+ # Run incoming message through the filter chain
291
+ message = decode(data)
292
+
293
+ if message.is_a?(Array) and not message.empty?
294
+ message.each { |msg| @receiver << T[:unix, self, msg] }
295
+ elsif message and not message.empty?
296
+ @receiver << T[:unix, self, message]
297
+ else return
298
+ end
299
+
300
+ if @active == :once
301
+ @active = false
302
+ disable
303
+ end
304
+ end
305
+
306
+ def on_write_complete
307
+ @receiver << T[:unix_write_complete, self]
308
+ end
309
+ end
310
+
311
+ # UNIX Listener returned from Revactor::UNIX.listen
312
+ class Listener < Rev::UNIXListener
313
+ attr_reader :controller
314
+
315
+ # Listen on the specified path. Accepts the following options:
316
+ #
317
+ # :active - Default active setting for new connections. See the
318
+ # documentation Rev::UNIX::Socket#active= for more info
319
+ #
320
+ # :controller - The controlling actor, default Actor.current
321
+ #
322
+ # :filter - An symbol/class or array of symbols/classes which implement
323
+ # #encode and #decode methods to transform data sent and
324
+ # received data respectively via Revactor::UNIX::Socket.
325
+ # See the "Filters" section in the README for more information
326
+ #
327
+ def initialize(path, options = {})
328
+ super(path)
329
+ opts = {
330
+ :active => false,
331
+ :controller => Actor.current
332
+ }.merge(options)
333
+
334
+ @active, @controller = opts[:active], opts[:controller]
335
+ @filterset = options[:filter]
336
+
337
+ @accepting = false
338
+ end
339
+
340
+ def inspect
341
+ "#<#{self.class}:0x#{object_id.to_s(16)}>"
342
+ end
343
+
344
+ # Change the default active setting for newly accepted connections
345
+ def active=(state)
346
+ unless [true, false, :once].include? state
347
+ raise ArgumentError, "must be true, false, or :once"
348
+ end
349
+
350
+ @active = state
351
+ end
352
+
353
+ # Will newly accepted connections be active?
354
+ def active?; @active; end
355
+
356
+ # Change the default controller for newly accepted connections
357
+ def controller=(controller)
358
+ Actor === controller or
359
+ raise ArgumentError, "controller must be an actor"
360
+ @controller = controller
361
+ end
362
+
363
+ # Accept an incoming connection
364
+ def accept
365
+ raise "another actor is already accepting" if @accepting
366
+
367
+ @accepting = true
368
+ @receiver = Actor.current
369
+ enable
370
+
371
+ Actor.receive do |filter|
372
+ filter.when(T[:unix_connection, self]) do |_, _, sock|
373
+ @accepting = false
374
+ return sock
375
+ end
376
+ end
377
+ end
378
+
379
+ #########
380
+ protected
381
+ #########
382
+
383
+ #
384
+ # Rev::UNIXListener callbacks
385
+ #
386
+
387
+ def on_connection(socket)
388
+ sock = Socket.new(socket,
389
+ :controller => @controller,
390
+ :active => @active,
391
+ :filter => @filterset
392
+ )
393
+ sock.attach(evloop)
394
+
395
+ @receiver << T[:unix_connection, self, sock]
396
+ disable
397
+ end
398
+ end
399
+ end
400
+ end
@@ -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.4"
5
+ s.version = "0.1.5"
6
6
  s.authors = "Tony Arcieri"
7
7
  s.email = "tony@medioh.com"
8
- s.date = "2008-3-28"
8
+ s.date = "2008-5-27"
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,7 +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.2.0")
17
+ s.add_dependency("rev", ">= 0.3.1")
18
18
  s.add_dependency("case", ">= 0.4")
19
19
 
20
20
  # RubyForge info
@@ -4,7 +4,7 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../lib/revactor/actor'
7
+ require File.dirname(__FILE__) + '/../lib/revactor'
8
8
 
9
9
  describe Actor do
10
10
  describe "creation" do
@@ -156,4 +156,4 @@ describe Actor do
156
156
  Actor.sleep 0
157
157
  actor.dead?.should be_true
158
158
  end
159
- end
159
+ end
@@ -31,6 +31,6 @@ describe Revactor::Filter::Line do
31
31
  chunks[2] = msg2.slice(1, msg2.size - 1)
32
32
  chunks[3] = msg2.slice(msg2.size, 1) << msg3
33
33
 
34
- chunks.reduce([]) { |a, chunk| a + @filter.decode(chunk) }.should == %w{foobar baz quux}
34
+ chunks.inject([]) { |a, chunk| a + @filter.decode(chunk) }.should == %w{foobar baz quux}
35
35
  end
36
36
  end
@@ -48,7 +48,7 @@ describe Revactor::Filter::Packet do
48
48
  chunks[2] = packet2.slice(1, packet2.size - 1)
49
49
  chunks[3] = packet2.slice(packet2.size, 1) << packet3
50
50
 
51
- chunks.reduce([]) { |a, chunk| a + filter.decode(chunk) }.should == [msg1, msg2, msg3]
51
+ chunks.inject([]) { |a, chunk| a + filter.decode(chunk) }.should == [msg1, msg2, msg3]
52
52
  end
53
53
 
54
54
  it "raises an exception for overlength frames" do
@@ -4,7 +4,7 @@
4
4
  # See file LICENSE for details
5
5
  #++
6
6
 
7
- require File.dirname(__FILE__) + '/../lib/revactor/tcp'
7
+ require File.dirname(__FILE__) + '/../lib/revactor'
8
8
 
9
9
  TEST_HOST = '127.0.0.1'
10
10
 
@@ -52,6 +52,29 @@ describe Revactor::TCP do
52
52
  s1.close
53
53
  end
54
54
 
55
+ it "times out on read" do
56
+ s1 = Revactor::TCP.connect(TEST_HOST, RANDOM_PORT)
57
+ s2 = @server.accept
58
+
59
+ proc {
60
+ s1.read(6, :timeout => 0.1)
61
+ }.should raise_error(Revactor::TCP::ReadError)
62
+
63
+ s1.close
64
+ end
65
+
66
+ it "times out on write" do
67
+ s1 = Revactor::TCP.connect(TEST_HOST, RANDOM_PORT)
68
+ s2 = @server.accept
69
+
70
+ # typically needs to write several thousand kilobytes before it blocks
71
+ buf = ' ' * 16384
72
+ proc {
73
+ loop { s1.write(buf, :timeout => 0.1) }
74
+ }.should raise_error(Revactor::TCP::WriteError)
75
+ s1.close
76
+ end
77
+
55
78
  it "writes data" do
56
79
  s1 = Revactor::TCP.connect(TEST_HOST, RANDOM_PORT)
57
80
  s2 = @server.accept
@@ -61,4 +84,4 @@ describe Revactor::TCP do
61
84
 
62
85
  s1.close
63
86
  end
64
- end
87
+ end
@@ -0,0 +1,65 @@
1
+ #--
2
+ # Copyright (C)2009 Eric Wong
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__) + '/../lib/revactor'
8
+ require 'tempfile'
9
+
10
+ describe Revactor::UNIX do
11
+ before :each do
12
+ @actor_run = false
13
+ @tmp = Tempfile.new('unix.sock')
14
+ File.unlink(@tmp.path)
15
+ @server = UNIXServer.new(@tmp.path)
16
+ end
17
+
18
+ after :each do
19
+ @server.close unless @server.closed?
20
+ File.unlink(@tmp.path)
21
+ end
22
+
23
+ it "connects to remote servers" do
24
+ sock = Revactor::UNIX.connect(@tmp.path)
25
+ sock.should be_an_instance_of(Revactor::UNIX::Socket)
26
+ @server.accept.should be_an_instance_of(UNIXSocket)
27
+
28
+ sock.close
29
+ end
30
+
31
+ it "listens for remote connections" do
32
+ # Don't use their server for this one...
33
+ @server.close
34
+ File.unlink(@tmp.path)
35
+
36
+ server = Revactor::UNIX.listen(@tmp.path)
37
+ server.should be_an_instance_of(Revactor::UNIX::Listener)
38
+
39
+ s1 = UNIXSocket.open(@tmp.path)
40
+ s2 = server.accept
41
+
42
+ server.close
43
+ s2.close
44
+ end
45
+
46
+ it "reads data" do
47
+ s1 = Revactor::UNIX.connect(@tmp.path)
48
+ s2 = @server.accept
49
+
50
+ s2.write 'foobar'
51
+ s1.read(6).should == 'foobar'
52
+
53
+ s1.close
54
+ end
55
+
56
+ it "writes data" do
57
+ s1 = Revactor::UNIX.connect(@tmp.path)
58
+ s2 = @server.accept
59
+
60
+ s1.write 'foobar'
61
+ s2.read(6).should == 'foobar'
62
+
63
+ s1.close
64
+ end
65
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: revactor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tony Arcieri
@@ -9,20 +9,22 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-03-28 00:00:00 -06:00
12
+ date: 2008-05-27 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rev
17
+ type: :runtime
17
18
  version_requirement:
18
19
  version_requirements: !ruby/object:Gem::Requirement
19
20
  requirements:
20
21
  - - ">="
21
22
  - !ruby/object:Gem::Version
22
- version: 0.2.0
23
+ version: 0.3.1
23
24
  version:
24
25
  - !ruby/object:Gem::Dependency
25
26
  name: case
27
+ type: :runtime
26
28
  version_requirement:
27
29
  version_requirements: !ruby/object:Gem::Requirement
28
30
  requirements:
@@ -41,27 +43,29 @@ extra_rdoc_files:
41
43
  - README
42
44
  - CHANGES
43
45
  files:
44
- - lib/revactor
45
46
  - lib/revactor/actor.rb
46
47
  - lib/revactor/actorize.rb
47
- - lib/revactor/filters
48
48
  - lib/revactor/filters/line.rb
49
49
  - lib/revactor/filters/packet.rb
50
50
  - lib/revactor/http_client.rb
51
+ - lib/revactor/http_fetcher.rb
51
52
  - lib/revactor/mailbox.rb
52
53
  - lib/revactor/mongrel.rb
53
54
  - lib/revactor/scheduler.rb
54
55
  - lib/revactor/tcp.rb
56
+ - lib/revactor/unix.rb
55
57
  - lib/revactor.rb
56
58
  - examples/chat_server.rb
57
59
  - examples/echo_server.rb
58
60
  - examples/google.rb
59
61
  - examples/mongrel.rb
62
+ - examples/ring.rb
63
+ - examples/trap_exit.rb
60
64
  - spec/actor_spec.rb
61
- - spec/delegator_spec.rb
62
65
  - spec/line_filter_spec.rb
63
66
  - spec/packet_filter_spec.rb
64
67
  - spec/tcp_spec.rb
68
+ - spec/unix_spec.rb
65
69
  - Rakefile
66
70
  - revactor.gemspec
67
71
  - LICENSE
@@ -69,6 +73,8 @@ files:
69
73
  - CHANGES
70
74
  has_rdoc: true
71
75
  homepage: http://revactor.org
76
+ licenses: []
77
+
72
78
  post_install_message:
73
79
  rdoc_options:
74
80
  - --title
@@ -93,9 +99,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
99
  requirements: []
94
100
 
95
101
  rubyforge_project: revactor
96
- rubygems_version: 1.0.1
102
+ rubygems_version: 1.3.5
97
103
  signing_key:
98
- specification_version: 2
104
+ specification_version: 3
99
105
  summary: Revactor is an Actor implementation for writing high performance concurrent programs
100
106
  test_files: []
101
107
 
@@ -1,46 +0,0 @@
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__) + '/../lib/revactor'
8
-
9
- describe Actor::Delegator do
10
- before :each do
11
- @obj = mock(:obj)
12
- @delegator = Actor::Delegator.new(@obj)
13
- end
14
-
15
- it "delegates calls to the given object" do
16
- @obj.should_receive(:foo).with(1)
17
- @obj.should_receive(:bar).with(2)
18
- @obj.should_receive(:baz).with(3)
19
-
20
- @delegator.foo(1)
21
- @delegator.bar(2)
22
- @delegator.baz(3)
23
- end
24
-
25
- it "returns the value from calls to the delegate object" do
26
- input_value = 42
27
- output_value = 420
28
-
29
- @obj.should_receive(:spiffy).with(input_value).and_return(input_value * 10)
30
- @delegator.spiffy(input_value).should == output_value
31
- end
32
-
33
- it "captures exceptions in the delegate and raises them for the caller" do
34
- ex = "crash!"
35
-
36
- @obj.should_receive(:crashy_method).and_raise(ex)
37
- proc { @delegator.crashy_method }.should raise_error(ex)
38
- end
39
-
40
- it "passes blocks along to the delegate" do
41
- prc = proc { "yay" }
42
-
43
- @obj.should_receive(:blocky).with(&prc)
44
- @delegator.blocky(&prc)
45
- end
46
- end