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