cod 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/HISTORY.txt +5 -1
- data/README +12 -13
- data/Rakefile +7 -1
- data/examples/{ping.rb → ping_pong/ping.rb} +1 -1
- data/examples/{pong.rb → ping_pong/pong.rb} +1 -1
- data/examples/{presence_client.rb → presence/client.rb} +0 -0
- data/examples/{presence_server.rb → presence/server.rb} +0 -0
- data/examples/queue/README +9 -0
- data/examples/queue/client.rb +31 -0
- data/examples/queue/queue.rb +51 -0
- data/examples/queue/send.rb +9 -0
- data/examples/service.rb +2 -2
- data/examples/tcp.rb +1 -1
- data/lib/cod.rb +47 -82
- data/lib/cod/beanstalk.rb +7 -0
- data/lib/cod/beanstalk/channel.rb +170 -0
- data/lib/cod/beanstalk/serializer.rb +80 -0
- data/lib/cod/beanstalk/service.rb +53 -0
- data/lib/cod/channel.rb +54 -12
- data/lib/cod/pipe.rb +188 -0
- data/lib/cod/select.rb +47 -0
- data/lib/cod/select_group.rb +87 -0
- data/lib/cod/service.rb +55 -42
- data/lib/cod/simple_serializer.rb +19 -0
- data/lib/cod/tcp_client.rb +202 -0
- data/lib/cod/tcp_server.rb +124 -0
- data/lib/cod/work_queue.rb +129 -0
- metadata +31 -45
- data/examples/pubsub/README +0 -12
- data/examples/pubsub/client.rb +0 -13
- data/examples/pubsub/directory.rb +0 -13
- data/examples/service_directory.rb +0 -32
- data/lib/cod/channel/base.rb +0 -185
- data/lib/cod/channel/beanstalk.rb +0 -69
- data/lib/cod/channel/pipe.rb +0 -137
- data/lib/cod/channel/tcp.rb +0 -16
- data/lib/cod/channel/tcpconnection.rb +0 -67
- data/lib/cod/channel/tcpserver.rb +0 -84
- data/lib/cod/client.rb +0 -81
- data/lib/cod/connection/beanstalk.rb +0 -77
- data/lib/cod/directory.rb +0 -98
- data/lib/cod/directory/countdown.rb +0 -31
- data/lib/cod/directory/subscription.rb +0 -59
- data/lib/cod/object_io.rb +0 -6
- data/lib/cod/objectio/connection.rb +0 -106
- data/lib/cod/objectio/reader.rb +0 -98
- data/lib/cod/objectio/serializer.rb +0 -26
- data/lib/cod/objectio/writer.rb +0 -27
- data/lib/cod/topic.rb +0 -95
data/lib/cod/service.rb
CHANGED
@@ -1,60 +1,73 @@
|
|
1
|
-
|
2
1
|
module Cod
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
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
|
-
#
|
13
|
-
#
|
14
|
-
#
|
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
|
-
#
|
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
|
-
#
|
19
|
-
#
|
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
|
-
@
|
33
|
+
@channel = channel
|
30
34
|
end
|
31
35
|
|
32
|
-
#
|
33
|
-
# to the
|
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
|
-
|
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
|
-
|
57
|
+
def call(rq)
|
58
|
+
@server_chan.put [rq, @answer_chan]
|
59
|
+
@answer_chan.get
|
60
|
+
end
|
39
61
|
|
40
|
-
|
41
|
-
|
62
|
+
def notify(rq)
|
63
|
+
@server_chan.put [rq, nil]
|
42
64
|
end
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|