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.
- 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
|