cod 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/HISTORY.txt +9 -0
- data/README +2 -2
- data/Rakefile +4 -4
- data/examples/example_scaffold.rb +15 -0
- data/examples/ping.rb +1 -1
- data/examples/pong.rb +1 -1
- data/examples/tcp.rb +21 -0
- data/lib/at_fork.rb +37 -13
- data/lib/cod.rb +69 -3
- data/lib/cod/channel/abstract.rb +32 -0
- data/lib/cod/channel/base.rb +96 -33
- data/lib/cod/channel/beanstalk.rb +8 -9
- data/lib/cod/channel/pipe.rb +46 -46
- data/lib/cod/channel/tcp.rb +16 -0
- data/lib/cod/channel/tcpconnection.rb +73 -0
- data/lib/cod/channel/tcpserver.rb +84 -0
- data/lib/cod/connection/beanstalk.rb +28 -7
- data/lib/cod/context.rb +8 -55
- data/lib/cod/object_io.rb +3 -0
- data/lib/cod/objectio/connection.rb +0 -0
- data/lib/cod/objectio/reader.rb +129 -0
- data/lib/cod/objectio/serializer.rb +26 -0
- data/lib/cod/objectio/writer.rb +32 -0
- metadata +56 -51
@@ -6,6 +6,8 @@ rescue LoadError
|
|
6
6
|
end
|
7
7
|
|
8
8
|
module Cod
|
9
|
+
# Also see Channel::Base for more documentation.
|
10
|
+
#
|
9
11
|
class Channel::Beanstalk < Channel::Base
|
10
12
|
NONBLOCK_TIMEOUT = 0.01
|
11
13
|
|
@@ -15,18 +17,19 @@ module Cod
|
|
15
17
|
# Name of the queue on the beanstalk server
|
16
18
|
attr_reader :tube_name
|
17
19
|
|
18
|
-
def initialize(connection, name
|
20
|
+
def initialize(connection, name)
|
19
21
|
@connection = connection
|
20
|
-
@tube_name =
|
22
|
+
@tube_name = name.freeze
|
23
|
+
@serializer = ObjectIO::Serializer.new
|
21
24
|
end
|
22
25
|
|
23
26
|
def initialize_copy(from)
|
24
|
-
@connection = from.connection
|
27
|
+
@connection = from.connection.dup
|
25
28
|
@tube_name = from.tube_name
|
26
29
|
end
|
27
30
|
|
28
31
|
def put(message)
|
29
|
-
buffer = serialize(message)
|
32
|
+
buffer = @serializer.serialize(message)
|
30
33
|
connection.put(tube_name, buffer)
|
31
34
|
end
|
32
35
|
|
@@ -37,7 +40,7 @@ module Cod
|
|
37
40
|
def get(opts={})
|
38
41
|
message = connection.get(tube_name,
|
39
42
|
:timeout => opts[:timeout])
|
40
|
-
return deserialize(message)
|
43
|
+
return @serializer.deserialize(nil, message)
|
41
44
|
rescue Beanstalk::TimedOut
|
42
45
|
raise Channel::TimeoutError, "No messages waiting in #{tube_name}."
|
43
46
|
end
|
@@ -49,10 +52,6 @@ module Cod
|
|
49
52
|
def identifier
|
50
53
|
identifier_class.new(connection.url, tube_name)
|
51
54
|
end
|
52
|
-
private
|
53
|
-
def gen_anonymous_name(base)
|
54
|
-
base + ".anonymous"
|
55
|
-
end
|
56
55
|
end
|
57
56
|
|
58
57
|
class Channel::Beanstalk::Identifier
|
data/lib/cod/channel/pipe.rb
CHANGED
@@ -3,6 +3,8 @@ module Cod
|
|
3
3
|
# you can only communicate within a single process hierarchy using this,
|
4
4
|
# since the file descriptors are not visible to the outside world.
|
5
5
|
#
|
6
|
+
# Also see Channel::Base for more documentation.
|
7
|
+
#
|
6
8
|
class Channel::Pipe < Channel::Base
|
7
9
|
# A tuple storing the read and the write end of a IO.pipe.
|
8
10
|
#
|
@@ -12,7 +14,8 @@ module Cod
|
|
12
14
|
|
13
15
|
def initialize(name=nil)
|
14
16
|
@fds = Fds.new(*IO.pipe)
|
15
|
-
|
17
|
+
|
18
|
+
init_in_and_out
|
16
19
|
end
|
17
20
|
|
18
21
|
def initialize_copy(old)
|
@@ -26,7 +29,7 @@ module Cod
|
|
26
29
|
old_fds.r.dup,
|
27
30
|
old_fds.w.dup)
|
28
31
|
|
29
|
-
|
32
|
+
init_in_and_out
|
30
33
|
end
|
31
34
|
|
32
35
|
def put(message)
|
@@ -35,43 +38,37 @@ module Cod
|
|
35
38
|
unless fds.w
|
36
39
|
direction_error 'Cannot put data to pipe. Already closed that end?'
|
37
40
|
end
|
38
|
-
|
39
|
-
|
40
|
-
fds.w.write(buffer)
|
41
|
+
|
42
|
+
@out.put(message)
|
41
43
|
rescue Errno::EPIPE
|
42
44
|
direction_error "You should #dup before writing; Looks like no other copy exists currently."
|
43
45
|
end
|
44
46
|
|
45
47
|
def waiting?
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
#
|
50
|
-
|
51
|
-
# remaining in such a situation would be those queued.
|
52
|
-
return queued?
|
48
|
+
@in.waiting?
|
49
|
+
rescue EOFError
|
50
|
+
# We've just hit end of file in the pipe. That means that all write
|
51
|
+
# ends have been closed.
|
52
|
+
@in.queued?
|
53
53
|
end
|
54
54
|
|
55
55
|
def get(opts={})
|
56
56
|
close_write
|
57
57
|
|
58
|
-
|
59
|
-
|
60
|
-
start_time = Time.now
|
61
|
-
loop do
|
62
|
-
IO.select([fds.r], nil, [fds.r], 0.1)
|
63
|
-
process_inbound_nonblock
|
64
|
-
return @waiting_messages.shift if queued?
|
65
|
-
|
66
|
-
if opts[:timeout] && (Time.now-start_time) > opts[:timeout]
|
67
|
-
raise Cod::Channel::TimeoutError,
|
68
|
-
"No messages waiting in pipe."
|
69
|
-
end
|
58
|
+
unless fds.r
|
59
|
+
direction_error 'Cannot get data from pipe. Already closed that end?'
|
70
60
|
end
|
71
|
-
|
61
|
+
|
62
|
+
@in.get(opts)
|
63
|
+
rescue EOFError
|
64
|
+
# We've just hit end of file in the pipe. That means that all write
|
65
|
+
# ends have been closed.
|
66
|
+
communication_error "All write ends for this pipe have been closed. "+
|
67
|
+
"Further #get's would block forever." \
|
68
|
+
unless @in.queued?
|
72
69
|
|
73
|
-
rescue Errno::EPIPE
|
74
|
-
|
70
|
+
# rescue Errno::EPIPE
|
71
|
+
# direction_error 'Cannot get data from pipe. Already closed that end?'
|
75
72
|
end
|
76
73
|
|
77
74
|
def close
|
@@ -79,44 +76,47 @@ module Cod
|
|
79
76
|
close_read
|
80
77
|
end
|
81
78
|
|
82
|
-
def identifier
|
83
|
-
identifier_class.new(self)
|
84
|
-
end
|
85
79
|
private
|
86
|
-
def
|
87
|
-
|
80
|
+
def init_in_and_out
|
81
|
+
serializer = ObjectIO::Serializer.new
|
82
|
+
@in = ObjectIO::Reader.new(serializer) { fds.r }
|
83
|
+
@out = ObjectIO::Writer.new(serializer) { fds.w }
|
88
84
|
end
|
89
|
-
|
85
|
+
|
90
86
|
def close_write
|
91
87
|
return unless fds.w
|
92
88
|
fds.w.close
|
89
|
+
|
93
90
|
fds.w = nil
|
91
|
+
@out = nil
|
94
92
|
end
|
95
93
|
|
96
94
|
def close_read
|
97
95
|
return unless fds.r
|
98
96
|
fds.r.close
|
97
|
+
|
99
98
|
fds.r = nil
|
99
|
+
@in = nil
|
100
100
|
end
|
101
101
|
|
102
|
-
# Tries hard to empty the pipe and to store incoming messages in
|
103
|
-
# @waiting_messages.
|
104
|
-
#
|
105
|
-
def process_inbound_nonblock
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
102
|
+
# # Tries hard to empty the pipe and to store incoming messages in
|
103
|
+
# # @waiting_messages.
|
104
|
+
# #
|
105
|
+
# def process_inbound_nonblock
|
106
|
+
# buffer = fds.r.read_nonblock(1024*1024*1024)
|
107
|
+
#
|
108
|
+
# while buffer.size > 0
|
109
|
+
# @waiting_messages << transport_unpack(buffer)
|
110
|
+
# end
|
111
111
|
rescue EOFError
|
112
112
|
# We've just hit end of file in the pipe. That means that all write
|
113
113
|
# ends have been closed.
|
114
114
|
communication_error "All write ends for this pipe have been closed. "+
|
115
115
|
"Further #get's would block forever." \
|
116
116
|
unless queued?
|
117
|
-
rescue Errno::EAGAIN
|
118
|
-
|
119
|
-
end
|
117
|
+
# rescue Errno::EAGAIN
|
118
|
+
# # Catch and ignore this: fds.r is not ready and read would block.
|
119
|
+
# end
|
120
120
|
end
|
121
121
|
|
122
122
|
class Channel::Pipe::Identifier
|
@@ -125,7 +125,7 @@ module Cod
|
|
125
125
|
end
|
126
126
|
|
127
127
|
def resolve
|
128
|
-
ObjectSpace._id2ref(@objid)
|
128
|
+
ObjectSpace._id2ref(@objid).dup
|
129
129
|
rescue RangeError
|
130
130
|
raise Cod::InvalidIdentifier,
|
131
131
|
"Could not reference channel. Either it was garbage collected "+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Cod
|
4
|
+
module Channel::TCP
|
5
|
+
def split_uri(uri)
|
6
|
+
colon = uri.index(':')
|
7
|
+
raise ArgumentError,
|
8
|
+
"TCP points must include a port number, #{uri.inspect} does not." \
|
9
|
+
unless colon
|
10
|
+
|
11
|
+
[
|
12
|
+
colon == 0 ? nil : uri[0...colon],
|
13
|
+
Integer(uri[colon+1..-1])]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'cod/channel/tcp'
|
2
|
+
|
3
|
+
module Cod
|
4
|
+
# A channel based on a tcp connection.
|
5
|
+
#
|
6
|
+
# Also see Channel::Base for more documentation.
|
7
|
+
#
|
8
|
+
class Channel::TCPConnection < Channel::Base
|
9
|
+
include Channel::TCP
|
10
|
+
|
11
|
+
# A <host, port> tuple: The target of this connection.
|
12
|
+
#
|
13
|
+
attr_reader :destination
|
14
|
+
|
15
|
+
def initialize(destination_or_connection)
|
16
|
+
if destination_or_connection.respond_to?(:to_str)
|
17
|
+
@destination = split_uri(destination_or_connection)
|
18
|
+
@connection = nil
|
19
|
+
else
|
20
|
+
@destination = nil
|
21
|
+
@connection = destination_or_connection
|
22
|
+
end
|
23
|
+
|
24
|
+
serializer = ObjectIO::Serializer.new
|
25
|
+
@writer = ObjectIO::Writer.new(serializer, &method(:reconnect))
|
26
|
+
@reader = ObjectIO::Reader.new(serializer) { reconnect }
|
27
|
+
end
|
28
|
+
|
29
|
+
def put(message)
|
30
|
+
@writer.put(message)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(opts={})
|
34
|
+
@reader.get(opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
def close
|
38
|
+
@connection.close if @connection
|
39
|
+
@connection = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def identifier
|
43
|
+
Identifier.new(destination)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
# Establishes connection in @connection. If a previous connection is
|
48
|
+
# in error state, it attempts to make a new connection.
|
49
|
+
#
|
50
|
+
def reconnect
|
51
|
+
@connection ||= TCPSocket.new(*destination)
|
52
|
+
rescue Errno::ECONNREFUSED
|
53
|
+
# The other end doesn't exist as of this moment. Have the caller retry
|
54
|
+
# later on.
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Channel::TCPConnection::Identifier
|
60
|
+
def initialize(destination)
|
61
|
+
@destination = destination
|
62
|
+
end
|
63
|
+
|
64
|
+
def resolve
|
65
|
+
# If we've been sent to our own server end, assume the role of the
|
66
|
+
# socket on that side. This is achieved by inserting the self into
|
67
|
+
# the stream of deserialized objects and having the transformer
|
68
|
+
# (attached to the serializer, see ObjectIO::Serializer) transform
|
69
|
+
# it into a valid connection object.
|
70
|
+
self
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'cod/channel/tcp'
|
2
|
+
|
3
|
+
module Cod
|
4
|
+
# A channel based on a passive tcp server (listening).
|
5
|
+
#
|
6
|
+
# Also see Channel::Base for more documentation.
|
7
|
+
#
|
8
|
+
class Channel::TCPServer < Channel::Base
|
9
|
+
include Channel::TCP
|
10
|
+
|
11
|
+
# The <host, port> tuple this server is bound to.
|
12
|
+
attr_reader :bind_to
|
13
|
+
|
14
|
+
# The actual TCPServer instance
|
15
|
+
attr_reader :server
|
16
|
+
|
17
|
+
# Sockets that are open to clients
|
18
|
+
attr_reader :connections
|
19
|
+
|
20
|
+
def initialize(bind_to)
|
21
|
+
@bind_to = split_uri(bind_to)
|
22
|
+
@server = TCPServer.new(*@bind_to)
|
23
|
+
|
24
|
+
serializer = ObjectIO::Serializer.new(self)
|
25
|
+
@reader = ObjectIO::Reader.new(serializer) {
|
26
|
+
accept_connections(server)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(opts={})
|
31
|
+
# Read a message from the wire and transform all contained objects.
|
32
|
+
@reader.get(opts)
|
33
|
+
end
|
34
|
+
|
35
|
+
def put(message)
|
36
|
+
communication_error "You cannot write to the server directly, transmit a "
|
37
|
+
"channel to the server instead."
|
38
|
+
end
|
39
|
+
|
40
|
+
def waiting?
|
41
|
+
@reader.waiting?
|
42
|
+
end
|
43
|
+
|
44
|
+
def close
|
45
|
+
@reader.close if @reader
|
46
|
+
server.close if server
|
47
|
+
|
48
|
+
@server = nil
|
49
|
+
@reader = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def transform(socket, obj)
|
53
|
+
# p [:tcp_server_deserialize, obj]
|
54
|
+
if obj.kind_of?(Channel::TCPConnection::Identifier)
|
55
|
+
# We've been sent 'a' tcp channel. Assume that it's our own client end
|
56
|
+
# that we've been sent and turn it into a channel that communicates
|
57
|
+
# back there.
|
58
|
+
return Channel::TCPConnection.new(socket)
|
59
|
+
end
|
60
|
+
|
61
|
+
return obj
|
62
|
+
end
|
63
|
+
|
64
|
+
def identifier
|
65
|
+
communication_error "TCP server channels cannot be transmitted."
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# Accept all pending connects and stores them in the connections array.
|
71
|
+
#
|
72
|
+
def accept_connections(server)
|
73
|
+
connections = []
|
74
|
+
loop do
|
75
|
+
connection = server.accept_nonblock
|
76
|
+
connections << connection
|
77
|
+
end
|
78
|
+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
|
79
|
+
# Means that no more connects are pending. Ignore, since this is exactly
|
80
|
+
# one of the termination conditions for this method.
|
81
|
+
return connections
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -2,6 +2,8 @@ module Cod
|
|
2
2
|
# Wraps the lower level beanstalk connection and exposes only methods that
|
3
3
|
# we need; also makes tube handling a bit more predictable.
|
4
4
|
#
|
5
|
+
# This class is NOT thread safe.
|
6
|
+
#
|
5
7
|
class Connection::Beanstalk
|
6
8
|
# The url that was used to connect to the beanstalk server.
|
7
9
|
attr_reader :url
|
@@ -11,27 +13,35 @@ module Cod
|
|
11
13
|
|
12
14
|
def initialize(url)
|
13
15
|
@url = url
|
14
|
-
|
16
|
+
connect
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize_copy(from)
|
20
|
+
@url = from.url
|
21
|
+
connect
|
15
22
|
end
|
16
23
|
|
17
24
|
# Writes a raw message as a job to the tube given by name.
|
18
25
|
#
|
19
26
|
def put(name, message)
|
20
27
|
connection.use name
|
28
|
+
# TODO throws EOFError when the beanstalkd server goes away
|
21
29
|
connection.put message
|
22
30
|
end
|
23
31
|
|
24
32
|
# Returns true if there are jobs waiting in the tube given by 'name'
|
25
33
|
def waiting?(name)
|
26
|
-
|
27
|
-
|
28
|
-
|
34
|
+
connection.stats_tube(name)['current-jobs-ready'] > 0
|
35
|
+
rescue Beanstalk::NotFoundError
|
36
|
+
# Tube could not be found. No one has written to it! Nothing is waiting.
|
37
|
+
false
|
29
38
|
end
|
30
39
|
|
31
40
|
# Removes and returns the next message waiting in the tube given by name.
|
32
41
|
#
|
33
42
|
def get(name, opts={})
|
34
43
|
watch(name) do
|
44
|
+
# TODO throws EOFError when the beanstalkd queue goes away.
|
35
45
|
job = connection.reserve(opts[:timeout])
|
36
46
|
job.delete
|
37
47
|
|
@@ -42,15 +52,26 @@ module Cod
|
|
42
52
|
# Closes the connection
|
43
53
|
#
|
44
54
|
def close
|
55
|
+
connection.close
|
45
56
|
@connection = nil
|
46
57
|
end
|
47
58
|
|
59
|
+
# Creates a connection
|
60
|
+
#
|
61
|
+
def connect
|
62
|
+
# TODO throws Errno::ECONNREFUSED if the beanstalkd doesn't answer
|
63
|
+
@connection = Beanstalk::Connection.new(url)
|
64
|
+
@watching = nil
|
65
|
+
end
|
48
66
|
private
|
49
67
|
def watch(name)
|
50
|
-
|
68
|
+
unless @watching == name
|
69
|
+
connection.ignore(@watching)
|
70
|
+
connection.watch(name)
|
71
|
+
@watching = name
|
72
|
+
end
|
73
|
+
|
51
74
|
yield
|
52
|
-
ensure
|
53
|
-
connection.ignore(name)
|
54
75
|
end
|
55
76
|
end
|
56
77
|
end
|