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 +10 -0
- data/examples/ring.rb +40 -0
- data/examples/trap_exit.rb +22 -0
- data/lib/revactor.rb +3 -2
- data/lib/revactor/actor.rb +2 -2
- data/lib/revactor/filters/line.rb +2 -2
- data/lib/revactor/filters/packet.rb +2 -2
- data/lib/revactor/http_client.rb +24 -9
- data/lib/revactor/http_fetcher.rb +100 -0
- data/lib/revactor/scheduler.rb +1 -1
- data/lib/revactor/tcp.rb +39 -25
- data/lib/revactor/unix.rb +400 -0
- data/revactor.gemspec +3 -3
- data/spec/actor_spec.rb +2 -2
- data/spec/line_filter_spec.rb +1 -1
- data/spec/packet_filter_spec.rb +1 -1
- data/spec/tcp_spec.rb +25 -2
- data/spec/unix_spec.rb +65 -0
- metadata +14 -8
- data/spec/delegator_spec.rb +0 -46
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 :(
|
data/examples/ring.rb
ADDED
@@ -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 } }
|
data/lib/revactor.rb
CHANGED
@@ -27,12 +27,12 @@ end
|
|
27
27
|
T = Tuple unless defined? T
|
28
28
|
|
29
29
|
module Revactor
|
30
|
-
Revactor::VERSION = '0.1.
|
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
|
data/lib/revactor/actor.rb
CHANGED
@@ -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.
|
144
|
+
current.instance_variable_set :@dead, true
|
145
145
|
end
|
146
146
|
|
147
147
|
actor = new(fiber)
|
148
|
-
fiber.
|
148
|
+
fiber.instance_variable_set :@_actor, actor
|
149
149
|
end
|
150
150
|
end
|
151
151
|
|
@@ -23,7 +23,7 @@ module Revactor
|
|
23
23
|
@data_size = 0
|
24
24
|
|
25
25
|
@mode = :prefix
|
26
|
-
@buffer =
|
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.
|
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
|
data/lib/revactor/http_client.rb
CHANGED
@@ -27,7 +27,7 @@ module Revactor
|
|
27
27
|
class << self
|
28
28
|
def connect(host, port = 80)
|
29
29
|
client = super
|
30
|
-
client.
|
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 = {}
|
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
|
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
|
-
#
|
72
|
-
|
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.
|
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
|
data/lib/revactor/scheduler.rb
CHANGED
@@ -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.
|
61
|
+
actor.instance_variable_set :@dead, true
|
62
62
|
handle_exit(actor)
|
63
63
|
rescue => ex
|
64
64
|
handle_exit(actor, ex)
|
data/lib/revactor/tcp.rb
CHANGED
@@ -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(
|
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(
|
108
|
+
@filterset ||= [*initialize_filter(options[:filter])]
|
103
109
|
|
104
110
|
@receiver = @controller
|
105
|
-
@read_buffer =
|
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
|
241
|
-
def initialize_filter(
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
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
|
-
|
263
|
+
name.new(*args)
|
257
264
|
when Symbol
|
258
|
-
symbol_to_filter(
|
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.
|
275
|
-
a.
|
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.
|
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
|
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.
|
5
|
+
s.version = "0.1.5"
|
6
6
|
s.authors = "Tony Arcieri"
|
7
7
|
s.email = "tony@medioh.com"
|
8
|
-
s.date = "2008-
|
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.
|
17
|
+
s.add_dependency("rev", ">= 0.3.1")
|
18
18
|
s.add_dependency("case", ">= 0.4")
|
19
19
|
|
20
20
|
# RubyForge info
|
data/spec/actor_spec.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# See file LICENSE for details
|
5
5
|
#++
|
6
6
|
|
7
|
-
require File.dirname(__FILE__) + '/../lib/revactor
|
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
|
data/spec/line_filter_spec.rb
CHANGED
@@ -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.
|
34
|
+
chunks.inject([]) { |a, chunk| a + @filter.decode(chunk) }.should == %w{foobar baz quux}
|
35
35
|
end
|
36
36
|
end
|
data/spec/packet_filter_spec.rb
CHANGED
@@ -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.
|
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
|
data/spec/tcp_spec.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# See file LICENSE for details
|
5
5
|
#++
|
6
6
|
|
7
|
-
require File.dirname(__FILE__) + '/../lib/revactor
|
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
|
data/spec/unix_spec.rb
ADDED
@@ -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
|
+
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-
|
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.
|
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.
|
102
|
+
rubygems_version: 1.3.5
|
97
103
|
signing_key:
|
98
|
-
specification_version:
|
104
|
+
specification_version: 3
|
99
105
|
summary: Revactor is an Actor implementation for writing high performance concurrent programs
|
100
106
|
test_files: []
|
101
107
|
|
data/spec/delegator_spec.rb
DELETED
@@ -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
|