revactor 0.1.4 → 0.1.5

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