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.
@@ -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=nil)
20
+ def initialize(connection, name)
19
21
  @connection = connection
20
- @tube_name = (name || gen_anonymous_name('beanstalk')).freeze
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
@@ -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
- @waiting_messages = []
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
- @waiting_messages = []
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
- buffer = transport_pack(message)
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
- process_inbound_nonblock
47
- queued?
48
- rescue Cod::Channel::CommunicationError
49
- # Gets raised whenever communication fails permanently. This means that
50
- # we probably wont be able to return any more messages. The only messages
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
- return @waiting_messages.shift if queued?
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
- # NEVER REACHED
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
- direction_error 'Cannot get data from pipe. Already closed that end?'
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 queued?
87
- not @waiting_messages.empty?
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
- buffer = fds.r.read_nonblock(1024*1024*1024)
107
-
108
- while buffer.size > 0
109
- @waiting_messages << transport_unpack(buffer)
110
- end
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
- # Catch and ignore this: fds.r is not ready and read would block.
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
- @connection = Beanstalk::Connection.new(url)
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
- watch(name) do
27
- !! connection.peek_ready
28
- end
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
- connection.watch(name)
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