cod 0.3.1 → 0.4.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.
Files changed (50) hide show
  1. data/Gemfile +1 -1
  2. data/HISTORY.txt +5 -1
  3. data/README +12 -13
  4. data/Rakefile +7 -1
  5. data/examples/{ping.rb → ping_pong/ping.rb} +1 -1
  6. data/examples/{pong.rb → ping_pong/pong.rb} +1 -1
  7. data/examples/{presence_client.rb → presence/client.rb} +0 -0
  8. data/examples/{presence_server.rb → presence/server.rb} +0 -0
  9. data/examples/queue/README +9 -0
  10. data/examples/queue/client.rb +31 -0
  11. data/examples/queue/queue.rb +51 -0
  12. data/examples/queue/send.rb +9 -0
  13. data/examples/service.rb +2 -2
  14. data/examples/tcp.rb +1 -1
  15. data/lib/cod.rb +47 -82
  16. data/lib/cod/beanstalk.rb +7 -0
  17. data/lib/cod/beanstalk/channel.rb +170 -0
  18. data/lib/cod/beanstalk/serializer.rb +80 -0
  19. data/lib/cod/beanstalk/service.rb +53 -0
  20. data/lib/cod/channel.rb +54 -12
  21. data/lib/cod/pipe.rb +188 -0
  22. data/lib/cod/select.rb +47 -0
  23. data/lib/cod/select_group.rb +87 -0
  24. data/lib/cod/service.rb +55 -42
  25. data/lib/cod/simple_serializer.rb +19 -0
  26. data/lib/cod/tcp_client.rb +202 -0
  27. data/lib/cod/tcp_server.rb +124 -0
  28. data/lib/cod/work_queue.rb +129 -0
  29. metadata +31 -45
  30. data/examples/pubsub/README +0 -12
  31. data/examples/pubsub/client.rb +0 -13
  32. data/examples/pubsub/directory.rb +0 -13
  33. data/examples/service_directory.rb +0 -32
  34. data/lib/cod/channel/base.rb +0 -185
  35. data/lib/cod/channel/beanstalk.rb +0 -69
  36. data/lib/cod/channel/pipe.rb +0 -137
  37. data/lib/cod/channel/tcp.rb +0 -16
  38. data/lib/cod/channel/tcpconnection.rb +0 -67
  39. data/lib/cod/channel/tcpserver.rb +0 -84
  40. data/lib/cod/client.rb +0 -81
  41. data/lib/cod/connection/beanstalk.rb +0 -77
  42. data/lib/cod/directory.rb +0 -98
  43. data/lib/cod/directory/countdown.rb +0 -31
  44. data/lib/cod/directory/subscription.rb +0 -59
  45. data/lib/cod/object_io.rb +0 -6
  46. data/lib/cod/objectio/connection.rb +0 -106
  47. data/lib/cod/objectio/reader.rb +0 -98
  48. data/lib/cod/objectio/serializer.rb +0 -26
  49. data/lib/cod/objectio/writer.rb +0 -27
  50. data/lib/cod/topic.rb +0 -95
data/lib/cod/service.rb CHANGED
@@ -1,60 +1,73 @@
1
-
2
1
  module Cod
3
- # A service that receives requests and answers. A service has always at
4
- # least one provider that creates an instance of this class and waits in
5
- # #one or #each. Clients then instantiate Cod::Client and #call the service.
6
- #
7
- # Example:
8
- # # service side
9
- # service = Cod::Service.new(incoming_channel)
10
- # service.one { |msg| 'answer' }
2
+ # Cod::Service abstracts the pattern where you send a request to a central
3
+ # location (with possibly multiple workers handling requests) and receive an
4
+ # answer. It solves problems related to timeouts, getting _your_ answer and
5
+ # not any kind of answer, etc...
11
6
  #
12
- # # client side
13
- # client = Cod::Client.new(incoming_channel, service_channel)
14
- # client.call('call message') # => 'answer'
7
+ # Synopsis:
8
+ # # On the server end:
9
+ # service = Cod.service(central_location)
10
+ # service.one { |request| :answer }
11
+ #
12
+ # # On the client end:
13
+ # service = Cod.client(central_location, answer_here)
14
+ #
15
+ # # asynchronous, no answer
16
+ # service.notify [:a, :request] # => nil
17
+ # # has an answer:
18
+ # service.call [:a, :request] # => :answer
19
+ #
20
+ # Depending on the setup of the channels, this class can be used to
21
+ # implement intra- and interprocess communication, very close to RPC. There
22
+ # are two ways to build on this:
15
23
  #
16
- # == Topology
24
+ # * Using method_missing, implement real RPC on top. This is usually rather
25
+ # simple (since Cod does a lot of work), see github.com/kschiess/zack for
26
+ # an example of this.
17
27
  #
18
- # A service always has (potentially) multiple clients and depending on the
19
- # transport layer used, one or more workers handling the clients request.
20
- # They will always receive the messages in a round robin fashion; the
21
- # service corresponds in this case to the channel address; clients need not
22
- # know the workers involved.
28
+ # * Using the 'case' gem, implement servers in an (erlang) actor like
29
+ # fashion.
23
30
  #
24
31
  class Service
25
- # Incoming channel for requests.
26
- attr_reader :incoming
27
-
28
32
  def initialize(channel)
29
- @incoming = channel
33
+ @channel = channel
30
34
  end
31
35
 
32
- # Calls the given block with the next request and returns the block answer
33
- # to the service client.
36
+ # Waits until a request arrives on the service channel. Then reads that
37
+ # request and hands it to the block given. The block return value
38
+ # will be returned to the service client.
39
+ #
40
+ # Use Cod::Client to perform the service call. This will keep track of
41
+ # messages sent and answers received and a couple of other things.
42
+ #
34
43
  #
35
44
  def one
36
- request_id, message, answer_channel, needs_answer = incoming.get
45
+ rq, answer_chan = @channel.get
46
+ res = yield(rq)
47
+ answer_chan.put res if answer_chan
48
+ end
49
+
50
+ # A service client.
51
+ #
52
+ class Client
53
+ def initialize(server_chan, answer_chan=nil)
54
+ @server_chan, @answer_chan = server_chan, answer_chan || server_chan
55
+ end
37
56
 
38
- answer = yield(message)
57
+ def call(rq)
58
+ @server_chan.put [rq, @answer_chan]
59
+ @answer_chan.get
60
+ end
39
61
 
40
- if needs_answer
41
- answer_channel.put [request_id, answer]
62
+ def notify(rq)
63
+ @server_chan.put [rq, nil]
42
64
  end
43
- end
44
-
45
- # Loops forever, yielding requests to the block given and returning the
46
- # answers to the client.
47
- #
48
- def each(&block)
49
- loop do
50
- one(&block)
65
+
66
+ def close
67
+ @server_chan.close
68
+ @answer_chan.close
51
69
  end
52
70
  end
53
-
54
- # Releases all resources held by the service.
55
- #
56
- def close
57
- incoming.close
58
- end
59
71
  end
72
+
60
73
  end
@@ -0,0 +1,19 @@
1
+ module Cod
2
+ # The simplest of all serializers, one that uses Marshal.dump and
3
+ # Marshal.load as a message format. Use this as a template for your own wire
4
+ # format serializers.
5
+ #
6
+ class SimpleSerializer
7
+ def en(obj)
8
+ Marshal.dump(obj)
9
+ end
10
+
11
+ def de(io)
12
+ if block_given?
13
+ Marshal.load(io, Proc.new)
14
+ else
15
+ Marshal.load(io)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,202 @@
1
+ require 'cod/work_queue'
2
+
3
+ module Cod
4
+ # Acts as a channel that connects to a tcp listening socket on the other
5
+ # end.
6
+ #
7
+ class TcpClient < Channel
8
+ def initialize(destination, serializer)
9
+ @serializer = serializer
10
+ @destination = destination
11
+
12
+ # TcpClient handles two cases: Construction via an url (destination is a
13
+ # string) and construction via a connection that has been
14
+ # preestablished (destination is a socket):
15
+ if destination.respond_to?(:read)
16
+ # destination seems to be a socket, wrap it with Connection
17
+ @connection = Connection.new(destination)
18
+ else
19
+ @connection = RobustConnection.new(destination)
20
+ end
21
+
22
+ @work_queue = WorkQueue.new
23
+
24
+ # The predicate for allowing sends: Is the connection up?
25
+ @work_queue.predicate {
26
+ # NOTE This will not be called unless we have some messages to send,
27
+ # so no useless connections are made
28
+ @connection.try_connect
29
+ @connection.established?
30
+ }
31
+ end
32
+
33
+ # Closes all underlying connections. You should only call this if you
34
+ # don't want to use the channel again, since it will also stop
35
+ # reconnection attempts.
36
+ #
37
+ def close
38
+ @work_queue.shutdown
39
+ @connection.close
40
+ end
41
+
42
+ # Sends an object to the other end of the channel, if it is connected.
43
+ # If it is not connected, objects sent will queue up and once the internal
44
+ # storage reaches the high watermark, they will be dropped silently.
45
+ #
46
+ # Example:
47
+ # channel.put :object
48
+ # # Really, any Ruby object that the current serializer can turn into
49
+ # # a string!
50
+ #
51
+ def put(obj)
52
+ # TODO high watermark check
53
+ # NOTE: predicate will call #try_connect
54
+ @work_queue.schedule {
55
+ send(obj)
56
+ }
57
+
58
+ @work_queue.try_work
59
+ end
60
+
61
+ # Receives a message. opts may contain various options, see below.
62
+ # Options include:
63
+ #
64
+ def get(opts={})
65
+ @connection.try_connect
66
+ @connection.read(@serializer)
67
+ end
68
+
69
+ # --------------------------------------------------------- service/client
70
+
71
+ def service
72
+ fail "A tcp client cannot be a service."
73
+ end
74
+ def client
75
+ # NOTE: Normally, it doesn't make sense to ask the client channel for
76
+ # something for a service connection, since the service needs to know
77
+ # where to send requests in addition to knowing where to receive
78
+ # answers. In the case of sockets, this is different: The service will
79
+ # send its answers back the same way it got the requests from, so this
80
+ # is really ok:
81
+ #
82
+ Service::Client.new(self, self)
83
+ end
84
+
85
+ # ---------------------------------------------------------- serialization
86
+
87
+ # A small structure that is constructed for a serialized tcp client on
88
+ # the other end (the deserializing end). What the deserializing code does
89
+ # with this is his problem.
90
+ #
91
+ OtherEnd = Struct.new(:destination) # :nodoc:
92
+
93
+ def _dump(level) # :nodoc:
94
+ @destination
95
+ end
96
+ def self._load(params) # :nodoc:
97
+ # Instead of a tcp client (no way to construct one at this point), we'll
98
+ # insert a kind of marker in the object stream that will be replaced
99
+ # with a valid client later on. (hopefully)
100
+ OtherEnd.new(params)
101
+ end
102
+ private
103
+ def send(msg)
104
+ @connection.write(
105
+ @serializer.en(msg))
106
+ end
107
+
108
+ # A connection that can be down. This allows elegant handling of
109
+ # reconnecting and delaying connections.
110
+ #
111
+ # Synopsis:
112
+ # connection = RobustConnection.new('foo:123')
113
+ # connection.try_connect
114
+ # connection.write('buffer')
115
+ # connection.established? # => false
116
+ # connection.close
117
+ #
118
+ class RobustConnection # :nodoc:
119
+ def initialize(destination)
120
+ @destination = destination
121
+ @socket = nil
122
+ end
123
+
124
+ attr_reader :destination
125
+ attr_reader :socket
126
+
127
+ # Returns true if a connection is currently running.
128
+ #
129
+ def established?
130
+ !! @socket
131
+ end
132
+
133
+ # Attempt to establish a connection. If there is already a connection
134
+ # and it still seems sound, does nothing.
135
+ #
136
+ def try_connect
137
+ return if established?
138
+
139
+ @socket = TCPSocket.new(*destination.split(':'))
140
+ rescue Errno::ECONNREFUSED
141
+ # No one listening? Well.. too bad.
142
+ @socket = nil
143
+ end
144
+
145
+ # Writes a buffer to the connection if it is established. Otherwise
146
+ # fails silently.
147
+ #
148
+ def write(buffer)
149
+ if @socket
150
+ @socket.write(buffer)
151
+ end
152
+ end
153
+
154
+ # Reads one message from the socket if possible.
155
+ #
156
+ def read(serializer)
157
+ return serializer.de(@socket) if @socket
158
+
159
+ # assert: @socket is still nil, because no connection could be made.
160
+ # Try to make one
161
+ loop do
162
+ try_connect
163
+ return serializer.de(@socket) if @socket
164
+ sleep 0.01
165
+ end
166
+ end
167
+
168
+ # Closes the connection and stops reconnection.
169
+ #
170
+ def close
171
+ @socket.close if @socket
172
+ @socket = nil
173
+ end
174
+ end
175
+
176
+ # Holds a connection that we don't create and therefore don't own. This
177
+ # is the case where a channel is created to communicate back to one of
178
+ # the TcpServers clients: the tcp server manages the back channels, so
179
+ # the created channel is lent its socket only.
180
+ #
181
+ class Connection # :nodoc:
182
+ def initialize(socket)
183
+ @socket = socket.dup
184
+ end
185
+ attr_reader :socket
186
+ def try_connect
187
+ end
188
+ def established?
189
+ true
190
+ end
191
+ def read(serializer)
192
+ serializer.de(@socket)
193
+ end
194
+ def write(buffer)
195
+ @socket.write(buffer)
196
+ end
197
+ def close
198
+ @socket.close
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,124 @@
1
+ module Cod
2
+ class TcpServer
3
+ def initialize(bind_to)
4
+ @socket = TCPServer.new(*bind_to.split(':'))
5
+ @client_sockets = []
6
+ @round_robin_index = 0
7
+ @messages = Array.new
8
+ @serializer = SimpleSerializer.new
9
+ end
10
+
11
+ # Receives one object from the channel.
12
+ #
13
+ # Example:
14
+ # channel.get # => object
15
+ #
16
+ def get(opts={})
17
+ msg, socket = _get(opts)
18
+ return msg
19
+ end
20
+
21
+ # Receives one object from the channel. Returns a tuple of
22
+ # <message,channel> where channel is a tcp channel that links back to the
23
+ # client that sent message.
24
+ #
25
+ # Using this method, the server can communicate back to its clients
26
+ # individually instead of collectively.
27
+ #
28
+ # Example:
29
+ # msg, chan = server.get_ext
30
+ # chan.put :answer
31
+ def get_ext(opts={})
32
+ msg, socket = _get(opts)
33
+ return [
34
+ msg,
35
+ TcpClient.new(socket, @serializer)]
36
+ end
37
+
38
+ # Closes the channel.
39
+ #
40
+ def close
41
+ @socket.close
42
+ @client_sockets.each { |io| io.close }
43
+ end
44
+
45
+ # Returns an array of IOs that Cod.select should select on.
46
+ #
47
+ def to_read_fds
48
+ @client_sockets
49
+ end
50
+
51
+ # --------------------------------------------------------- service/client
52
+
53
+ def service
54
+ Service.new(self)
55
+ end
56
+ # NOTE: It is really more convenient to just construct a Cod.tcp_client
57
+ # and ask that for a client object. In the case of TCP, this is enough.
58
+ #
59
+ def client(answers_to)
60
+ Service::Client.new(answers_to, answers_to)
61
+ end
62
+
63
+ private
64
+ def _get(opts)
65
+ loop do
66
+ # Check if there are pending connects
67
+ accept_new_connections
68
+
69
+ # shuffle the socket list around, so we don't always read from the
70
+ # same client.
71
+ socket_list = round_robin(@client_sockets)
72
+
73
+ # select for readiness
74
+ rr, rw, re = IO.select(socket_list, nil, nil, 0.1)
75
+ next unless rr
76
+
77
+ rr.each do |io|
78
+ consume_pending io, opts
79
+ end
80
+
81
+ return @messages.shift unless @messages.empty?
82
+ end
83
+ end
84
+
85
+ def consume_pending(io, opts)
86
+ until io.eof?
87
+ @messages << [
88
+ deserialize(io),
89
+ io]
90
+
91
+ # More messages from this socket?
92
+ return unless IO.select([io], nil, nil, 0.01)
93
+ end
94
+ end
95
+
96
+ def deserialize(io)
97
+ @serializer.de(io) { |obj|
98
+ obj.kind_of?(TcpClient::OtherEnd) ?
99
+ TcpClient.new(io, @serializer) :
100
+ obj
101
+ }
102
+ end
103
+
104
+ def round_robin(list)
105
+ @round_robin_index += 1
106
+ if @round_robin_index >= list.size
107
+ @round_robin_index = 0
108
+ end
109
+
110
+ # Create a duplicate of list that has its elements rotated by
111
+ # @round_robin_index
112
+ list = list.dup
113
+ list = list + list.shift(@round_robin_index)
114
+ end
115
+
116
+ def accept_new_connections
117
+ loop do
118
+ @client_sockets << @socket.accept_nonblock
119
+ end
120
+ rescue Errno::EAGAIN
121
+ # This means that there are no sockets to accept. Continue.
122
+ end
123
+ end
124
+ end