cod 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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