cod 0.2.0 → 0.3.0
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/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
|