cod 0.3.1 → 0.4.0

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