cod 0.4.2 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.txt +9 -0
- data/README +11 -4
- data/Rakefile +8 -14
- data/examples/bs_ping_pong/ping.rb +15 -0
- data/examples/bs_ping_pong/pong.rb +13 -0
- data/examples/service.rb +2 -2
- data/lib/cod.rb +47 -15
- data/lib/cod/beanstalk/channel.rb +55 -9
- data/lib/cod/beanstalk/serializer.rb +2 -0
- data/lib/cod/beanstalk/service.rb +37 -1
- data/lib/cod/channel.rb +20 -6
- data/lib/cod/service.rb +2 -2
- data/lib/cod/simple_serializer.rb +15 -0
- data/lib/cod/tcp_client.rb +65 -8
- data/lib/cod/tcp_server.rb +2 -2
- data/lib/cod/work_queue.rb +20 -3
- data/lib/tcp_proxy.rb +37 -4
- metadata +21 -21
- data/examples/ping_pong/ping.rb +0 -9
- data/examples/ping_pong/pong.rb +0 -9
data/HISTORY.txt
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
= 0.4.3 / 28Feb2012
|
2
|
+
|
3
|
+
! Fixes a few bugs when using tcp channels (connection was not made or not
|
4
|
+
closed properly).
|
5
|
+
|
6
|
+
+ Cod::ConnectionLost should be raised when the connection goes down. This
|
7
|
+
allows simply reconnecting on error, see
|
8
|
+
examples/bs_ping_pong/{ping,pong}.rb on how to do this.
|
9
|
+
|
1
10
|
= 0.4.2 / 15Feb2012
|
2
11
|
|
3
12
|
! Actively removes clients that have closed the sockets on their end
|
data/README
CHANGED
@@ -23,11 +23,18 @@ SYNOPSIS
|
|
23
23
|
|
24
24
|
STATUS
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
that we had before, but some things have been designed more logically.
|
26
|
+
Working library. Some rough edges and potential for growth. Have a look at the
|
27
|
+
Cod module to get started.
|
29
28
|
|
30
|
-
|
29
|
+
Working transports include:
|
30
|
+
|
31
|
+
* process (spawn, connecting to $stdout and $stdin)
|
32
|
+
* stdio (connects to $stdout and $stdin of current process)
|
33
|
+
* pipe (akin to IO.pipe)
|
34
|
+
* tcp (server and client)
|
35
|
+
* beanstalk (connects to beanstalkd)
|
36
|
+
|
37
|
+
At version 0.4.3
|
31
38
|
|
32
39
|
(c) 2011 Kaspar Schiess
|
33
40
|
|
data/Rakefile
CHANGED
@@ -2,6 +2,7 @@ require "rubygems"
|
|
2
2
|
require "rdoc/task"
|
3
3
|
require 'rspec/core/rake_task'
|
4
4
|
require 'rubygems/package_task'
|
5
|
+
require 'rake/clean'
|
5
6
|
|
6
7
|
desc "Run all tests: Exhaustive."
|
7
8
|
RSpec::Core::RakeTask.new
|
@@ -15,22 +16,12 @@ task :stats do
|
|
15
16
|
end
|
16
17
|
end
|
17
18
|
|
18
|
-
require '
|
19
|
-
|
20
|
-
#
|
21
|
-
|
22
|
-
rdoc.title = "cod - IPC made really simple."
|
23
|
-
rdoc.options << '--line-numbers'
|
24
|
-
rdoc.options << '--fmt' << 'shtml' # explictly set shtml generator
|
25
|
-
rdoc.template = 'direct' # lighter template used on railsapi.com
|
26
|
-
rdoc.main = "README"
|
27
|
-
rdoc.rdoc_files.include("README", "lib/**/*.rb")
|
28
|
-
rdoc.rdoc_dir = "rdoc"
|
19
|
+
require 'yard'
|
20
|
+
YARD::Rake::YardocTask.new do |t|
|
21
|
+
# t.files = ['lib/**/*.rb']
|
22
|
+
# t.options = ['--any', '--extra', '--opts'] # optional
|
29
23
|
end
|
30
24
|
|
31
|
-
desc 'Clear out RDoc'
|
32
|
-
task :clean => [:clobber_rdoc, :clobber_package]
|
33
|
-
|
34
25
|
# This task actually builds the gem.
|
35
26
|
task :gem => :spec
|
36
27
|
spec = eval(File.read('cod.gemspec'))
|
@@ -39,3 +30,6 @@ desc "Generate the gem package."
|
|
39
30
|
Gem::PackageTask.new(spec) do |pkg|
|
40
31
|
# pkg.need_tar = true
|
41
32
|
end
|
33
|
+
|
34
|
+
CLEAN << 'pkg'
|
35
|
+
CLEAN << 'doc'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
|
2
|
+
require 'cod'
|
3
|
+
|
4
|
+
begin
|
5
|
+
pipe ||= Cod.beanstalk('pingpong', 'localhost:11300')
|
6
|
+
|
7
|
+
loop do
|
8
|
+
pipe.put Time.now
|
9
|
+
sleep 1
|
10
|
+
end
|
11
|
+
rescue Cod::ConnectionLost
|
12
|
+
pipe.close
|
13
|
+
pipe = nil
|
14
|
+
retry
|
15
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
|
2
|
+
require 'cod'
|
3
|
+
|
4
|
+
begin
|
5
|
+
pipe ||= Cod.beanstalk("pingpong", 'localhost:11300')
|
6
|
+
|
7
|
+
loop do
|
8
|
+
puts "Received: "+pipe.get.inspect
|
9
|
+
end
|
10
|
+
rescue Cod::ConnectionLost
|
11
|
+
pipe.close; pipe = nil
|
12
|
+
retry
|
13
|
+
end
|
data/examples/service.rb
CHANGED
@@ -9,7 +9,7 @@ service_channel = Cod.pipe
|
|
9
9
|
answer_channel = Cod.pipe
|
10
10
|
|
11
11
|
child_pid = fork do
|
12
|
-
service =
|
12
|
+
service = service_channel.service()
|
13
13
|
service.one { |call|
|
14
14
|
puts "Service got called with #{call.inspect}"
|
15
15
|
time = Time.now
|
@@ -17,7 +17,7 @@ child_pid = fork do
|
|
17
17
|
time }
|
18
18
|
end
|
19
19
|
|
20
|
-
client =
|
20
|
+
client = service_channel.client(answer_channel)
|
21
21
|
puts "Calling service..."
|
22
22
|
answer = client.call('42')
|
23
23
|
|
data/lib/cod.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
# The core concept of Cod are 'channels'. (see {Cod::Channel::Base}) You can
|
4
|
+
# create such channels on top of the various transport layers. Once you have
|
5
|
+
# such a channel, you #put messages into it and you #get messages out of it.
|
6
|
+
# Messages are retrieved in FIFO manner, making channels look like a
|
7
|
+
# communication pipe most of the time.
|
6
8
|
#
|
7
9
|
# Cod also brings a few abstractions layered on top of channels: You can use
|
8
10
|
# channels to present 'services' (Cod::Service) to the network: A service is a
|
@@ -13,6 +15,12 @@
|
|
13
15
|
# channel! This is really powerful and used extensively in constructing the
|
14
16
|
# higher order primitives.
|
15
17
|
#
|
18
|
+
# All Cod channels have a serializer. If you don't specify your own
|
19
|
+
# serializer, they will use Marshal.dump and Marshal.load. (see
|
20
|
+
# {Cod::SimpleSerializer}) This allows to send Ruby objects and not just
|
21
|
+
# strings by default. If you want to, you can of course go back to very strict
|
22
|
+
# wire formats, see {Cod::ProtocolBuffersSerializer} for an example of that.
|
23
|
+
#
|
16
24
|
# The goal of Cod is that you have to know only very few things about the
|
17
25
|
# network (the various transports) to be able to construct complex things. It
|
18
26
|
# handles reconnection and reliability for you. It also translates cryptic OS
|
@@ -22,18 +30,24 @@
|
|
22
30
|
# Contribute your observations and we'll come up with a way of dealing with
|
23
31
|
# most of the tricky stuff!
|
24
32
|
#
|
33
|
+
# @see Cod::Channel
|
34
|
+
#
|
25
35
|
module Cod
|
26
36
|
# Creates a pipe connection that is visible to this process and its
|
27
|
-
# children.
|
37
|
+
# children.
|
38
|
+
#
|
39
|
+
# @see Cod::Pipe
|
28
40
|
#
|
29
41
|
def pipe(serializer=nil, pipe_pair=nil)
|
30
42
|
Cod::Pipe.new(serializer)
|
31
43
|
end
|
32
44
|
module_function :pipe
|
33
45
|
|
34
|
-
# Creates two channels based on Cod.pipe (unidirectional IO.pipe) and links
|
46
|
+
# Creates two channels based on {Cod.pipe} (unidirectional IO.pipe) and links
|
35
47
|
# things up so that you communication is bidirectional. Writes go to
|
36
|
-
# #out and reads come from #in.
|
48
|
+
# #out and reads come from #in.
|
49
|
+
#
|
50
|
+
# @see Cod::BidirPipe
|
37
51
|
#
|
38
52
|
def bidir_pipe(serializer=nil, pipe_pair=nil)
|
39
53
|
Cod::BidirPipe.new(serializer, pipe_pair)
|
@@ -41,7 +55,8 @@ module Cod
|
|
41
55
|
module_function :bidir_pipe
|
42
56
|
|
43
57
|
# Creates a tcp connection to the destination and returns a channel for it.
|
44
|
-
#
|
58
|
+
#
|
59
|
+
# @see Cod::TcpClient
|
45
60
|
#
|
46
61
|
def tcp(destination, serializer=nil)
|
47
62
|
Cod::TcpClient.new(
|
@@ -50,16 +65,20 @@ module Cod
|
|
50
65
|
end
|
51
66
|
module_function :tcp
|
52
67
|
|
53
|
-
# Creates a tcp listener on bind_to and returns a channel for it.
|
54
|
-
#
|
68
|
+
# Creates a tcp listener on bind_to and returns a channel for it.
|
69
|
+
#
|
70
|
+
# @see Cod::TcpServer
|
55
71
|
#
|
56
|
-
def tcp_server(bind_to)
|
57
|
-
Cod::TcpServer.new(
|
72
|
+
def tcp_server(bind_to, serializer=nil)
|
73
|
+
Cod::TcpServer.new(
|
74
|
+
bind_to,
|
75
|
+
serializer || SimpleSerializer.new)
|
58
76
|
end
|
59
77
|
module_function :tcp_server
|
60
78
|
|
61
|
-
# Creates a channel based on the beanstalkd messaging queue.
|
62
|
-
#
|
79
|
+
# Creates a channel based on the beanstalkd messaging queue.
|
80
|
+
#
|
81
|
+
# @see Cod::Beanstalk::Channel
|
63
82
|
#
|
64
83
|
def beanstalk(tube_name, server=nil)
|
65
84
|
Cod::Beanstalk::Channel.new(tube_name, server||'localhost:11300')
|
@@ -71,6 +90,9 @@ module Cod
|
|
71
90
|
#
|
72
91
|
# Example:
|
73
92
|
# pid, channel = Cod.process('cat')
|
93
|
+
#
|
94
|
+
# @see Cod::Process
|
95
|
+
#
|
74
96
|
def process(command, serializer=nil)
|
75
97
|
Cod::Process.new(command, serializer)
|
76
98
|
end
|
@@ -80,6 +102,8 @@ module Cod
|
|
80
102
|
# pipes #put method will print to stdout, and the #get method will read from
|
81
103
|
# stdin.
|
82
104
|
#
|
105
|
+
# @see Cod::Pipe
|
106
|
+
#
|
83
107
|
def stdio(serializer=nil)
|
84
108
|
Cod::Pipe.new(serializer, [$stdin, $stdout])
|
85
109
|
end
|
@@ -102,6 +126,14 @@ module Cod
|
|
102
126
|
super("This channel is read only, attempted write operation.")
|
103
127
|
end
|
104
128
|
end
|
129
|
+
|
130
|
+
# Indicates that a standing connection was lost and must be reconnected.
|
131
|
+
#
|
132
|
+
class ConnectionLost < StandardError
|
133
|
+
def initialize
|
134
|
+
super "Connection lost."
|
135
|
+
end
|
136
|
+
end
|
105
137
|
end
|
106
138
|
|
107
139
|
require 'cod/select_group'
|
@@ -1,14 +1,19 @@
|
|
1
1
|
module Cod::Beanstalk
|
2
2
|
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
3
|
+
# A channel based on a beanstalkd tube. A {#put} will insert messages into
|
4
|
+
# the tube, and a {#get} will fetch the next message that is pending on the
|
5
|
+
# tube.
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
7
|
+
# @note Beanstalk channels cannot currently be used in Cod.select. This is
|
8
|
+
# due to limitations inherent in the beanstalkd protocol. We'll probably
|
9
|
+
# try to get a patch into beanstalkd to change this.
|
10
|
+
#
|
11
|
+
# @note If you embed a beanstalk channel into one of your messages, you will
|
12
|
+
# get a channel that connects to the same server and the same tube on the
|
13
|
+
# other end. This behaviour is useful for {Cod::Service}.
|
10
14
|
#
|
11
15
|
class Channel < Cod::Channel
|
16
|
+
# All messages get inserted into the beanstalk queue as this priority.
|
12
17
|
JOB_PRIORITY = 0
|
13
18
|
|
14
19
|
# Which tube this channel is connected to
|
@@ -55,6 +60,7 @@ module Cod::Beanstalk
|
|
55
60
|
@transport.close
|
56
61
|
end
|
57
62
|
|
63
|
+
# @private
|
58
64
|
def to_read_fds
|
59
65
|
fail "Cod.select not supported with beanstalkd channels.\n"+
|
60
66
|
"To support this, we will have to extend the beanstalkd protocol."
|
@@ -69,6 +75,40 @@ module Cod::Beanstalk
|
|
69
75
|
end
|
70
76
|
|
71
77
|
# -------------------------------------------------------- queue interface
|
78
|
+
|
79
|
+
# Like {#get}, read next message from the channel but reserve the right
|
80
|
+
# to put it back. This uses beanstalkds flow control features to be able
|
81
|
+
# to control message flow in the case of exceptions and the like.
|
82
|
+
#
|
83
|
+
# If the block given to this message raises an exception, the message
|
84
|
+
# is released unless a control command has been given. This means that
|
85
|
+
# other workers on the same tube will get the chance to see the message.
|
86
|
+
#
|
87
|
+
# If the block is exited without specifying a fate for the message, it
|
88
|
+
# is deleted from the tube.
|
89
|
+
#
|
90
|
+
# @yield [Object, Cod::Beanstalk::Channel::Control]
|
91
|
+
# @return the blocks return value
|
92
|
+
#
|
93
|
+
# @example All the flow control that beanstalkd allows
|
94
|
+
# channel.try_get { |msg, ctl|
|
95
|
+
# if msg == 1
|
96
|
+
# ctl.release # don't handle messages of type 1
|
97
|
+
# else
|
98
|
+
# ctl.bury # for example
|
99
|
+
# end
|
100
|
+
# }
|
101
|
+
#
|
102
|
+
# @example Exceptions release the message
|
103
|
+
# # Will release the message and allow other connected channels to
|
104
|
+
# # #get it.
|
105
|
+
# channel.try_get { |msg, ctl|
|
106
|
+
# fail "No such message handler"
|
107
|
+
# }
|
108
|
+
#
|
109
|
+
#
|
110
|
+
# @see Cod::Beanstalk::Channel::Control
|
111
|
+
#
|
72
112
|
def try_get
|
73
113
|
fail "No block given to #try_get" unless block_given?
|
74
114
|
|
@@ -124,25 +164,31 @@ module Cod::Beanstalk
|
|
124
164
|
end
|
125
165
|
|
126
166
|
# ---------------------------------------------------------- serialization
|
167
|
+
# @private
|
127
168
|
def _dump(level) # :nodoc:
|
128
169
|
Marshal.dump(
|
129
170
|
[@tube_name, @server_url])
|
130
171
|
end
|
172
|
+
# @private
|
131
173
|
def self._load(str) # :nodoc:
|
132
174
|
tube_name, server_url = Marshal.load(str)
|
133
175
|
Cod.beanstalk(tube_name, server_url)
|
134
176
|
end
|
135
177
|
|
136
178
|
# ----------------------------------------------------- beanstalk commands
|
137
|
-
|
179
|
+
# @private
|
180
|
+
def bs_delete(msg_id)
|
138
181
|
bs_command([:delete, msg_id], :deleted)
|
139
182
|
end
|
140
|
-
|
183
|
+
# @private
|
184
|
+
def bs_release(msg_id)
|
141
185
|
bs_command([:release, msg_id, JOB_PRIORITY, 0], :released)
|
142
186
|
end
|
143
|
-
|
187
|
+
# @private
|
188
|
+
def bs_release_with_delay(msg_id, seconds)
|
144
189
|
bs_command([:release, msg_id, JOB_PRIORITY, seconds], :released)
|
145
190
|
end
|
191
|
+
# @private
|
146
192
|
def bs_bury(msg_id)
|
147
193
|
# NOTE: Why I need to assign a priority when burying I fail to
|
148
194
|
# understand. Like a priority for rapture?
|
@@ -1,4 +1,14 @@
|
|
1
1
|
module Cod::Beanstalk
|
2
|
+
# Beanstalk services specialize for the beanstalk channels in that they
|
3
|
+
# support a second service block argument, the control. Using this argument,
|
4
|
+
# your service can refuse to accept a request or bury it for debugging
|
5
|
+
# inspection.
|
6
|
+
#
|
7
|
+
# @example Additional block argument
|
8
|
+
# service.one { |request, control|
|
9
|
+
# control.retry_in(1) # release message with delay
|
10
|
+
# }
|
11
|
+
#
|
2
12
|
class Service < Cod::Service
|
3
13
|
def one(&block)
|
4
14
|
@channel.try_get { |(rq, answer_chan), control|
|
@@ -21,6 +31,12 @@ module Cod::Beanstalk
|
|
21
31
|
@channel_control = channel_control
|
22
32
|
end
|
23
33
|
|
34
|
+
# Releases the request and instructs the beanstalkd server to hand it
|
35
|
+
# to us again in seconds seconds.
|
36
|
+
#
|
37
|
+
# @param [Fixnum] seconds
|
38
|
+
# @return [void]
|
39
|
+
#
|
24
40
|
def retry_in(seconds)
|
25
41
|
fail ArgumentError,
|
26
42
|
"#retry_in accepts only an integer number of seconds." \
|
@@ -28,16 +44,36 @@ module Cod::Beanstalk
|
|
28
44
|
|
29
45
|
@channel_control.release_with_delay(seconds)
|
30
46
|
end
|
47
|
+
|
48
|
+
# Releases the request for immediate consumption by someone else.
|
49
|
+
#
|
50
|
+
# @return [void]
|
51
|
+
#
|
31
52
|
def retry
|
32
53
|
@channel_control.release
|
33
54
|
end
|
55
|
+
|
56
|
+
# Buries the message for later inspection. (see beanstalkd manual)
|
57
|
+
#
|
58
|
+
# @return [void]
|
59
|
+
#
|
34
60
|
def bury
|
35
61
|
@channel_control.bury
|
36
62
|
end
|
63
|
+
|
64
|
+
# Deletes the request. This is how you accept a request.
|
65
|
+
#
|
66
|
+
# @return [void]
|
67
|
+
#
|
37
68
|
def delete
|
38
69
|
@channel_control.delete
|
39
70
|
end
|
40
|
-
|
71
|
+
|
72
|
+
# Returns true if a flow control command has already been given as answer
|
73
|
+
# to the current request. Multiple flow control commands are not allowed.
|
74
|
+
#
|
75
|
+
# @return [Boolean]
|
76
|
+
#
|
41
77
|
def command_issued?
|
42
78
|
@channel_control.command_given?
|
43
79
|
end
|
data/lib/cod/channel.rb
CHANGED
@@ -1,33 +1,39 @@
|
|
1
1
|
module Cod
|
2
|
-
# Channels transport
|
2
|
+
# Channels transport Ruby objects from one end to the other. The
|
3
3
|
# communication setup varies a bit depending on the transport used for the
|
4
4
|
# channel, but the interface you interact with doesn't vary. You can #put
|
5
5
|
# messages into a channel and you then #get them out of it.
|
6
6
|
#
|
7
|
-
# Synopsis
|
7
|
+
# = Synopsis
|
8
|
+
#
|
8
9
|
# channel.put [:a, :ruby, :object]
|
9
10
|
# channel.get # => [:a, :ruby, :object]
|
10
11
|
#
|
11
12
|
# By default, channels will serialize the messages you give them using
|
12
13
|
# Marshal.dump and Marshal.load. You can change this by passing your own
|
13
|
-
# serializer to the channel upon construction; see SimpleSerializer for a
|
14
|
+
# serializer to the channel upon construction; see {SimpleSerializer} for a
|
14
15
|
# description of the interface such a serializer needs to implement.
|
15
16
|
#
|
16
|
-
# This class
|
17
|
-
#
|
18
|
-
#
|
17
|
+
# This class is the abstract superclass of all Cod channels. It doesn't have
|
18
|
+
# a transport by its own, but implements the whole interface for
|
19
|
+
# documentation purposes.
|
19
20
|
#
|
20
21
|
class Channel
|
21
22
|
# Obtains one message from the channel. If the channel is empty, but
|
22
23
|
# theoretically able to receive more messages, blocks forever. But if the
|
23
24
|
# channel is somehow broken, an exception is raised.
|
24
25
|
#
|
26
|
+
# @return [Object] the next message waiting in the channel
|
27
|
+
#
|
25
28
|
def get
|
26
29
|
abstract_method_error
|
27
30
|
end
|
28
31
|
|
29
32
|
# Puts one message into a channel.
|
30
33
|
#
|
34
|
+
# @param msg [Object] any Ruby object that should be sent as message
|
35
|
+
# @return [void]
|
36
|
+
#
|
31
37
|
def put(msg)
|
32
38
|
abstract_method_error
|
33
39
|
end
|
@@ -35,6 +41,9 @@ module Cod
|
|
35
41
|
# Interact with a channel by first writing msg to it, then reading back
|
36
42
|
# the other ends answer.
|
37
43
|
#
|
44
|
+
# @param msg [Object] any Ruby object that should be sent as message
|
45
|
+
# @return [Object] the answer from the other end.
|
46
|
+
#
|
38
47
|
def interact(msg)
|
39
48
|
put msg
|
40
49
|
get
|
@@ -42,6 +51,8 @@ module Cod
|
|
42
51
|
|
43
52
|
# Produces a service that has this channel as communication point.
|
44
53
|
#
|
54
|
+
# @return [Cod::Service]
|
55
|
+
#
|
45
56
|
def service
|
46
57
|
abstract_method_error
|
47
58
|
end
|
@@ -49,6 +60,9 @@ module Cod
|
|
49
60
|
# Produces a service client that connects to this channel and receives
|
50
61
|
# service answers to the channel indicated by answers_to.
|
51
62
|
#
|
63
|
+
# @param answers_to [Cod::Channel] where to send the answers
|
64
|
+
# @return [Cod::Service::Client]
|
65
|
+
#
|
52
66
|
def client(answers_to)
|
53
67
|
abstract_method_error
|
54
68
|
end
|
data/lib/cod/service.rb
CHANGED
@@ -6,11 +6,11 @@ module Cod
|
|
6
6
|
#
|
7
7
|
# Synopsis:
|
8
8
|
# # On the server end:
|
9
|
-
# service =
|
9
|
+
# service = server_channel.service
|
10
10
|
# service.one { |request| :answer }
|
11
11
|
#
|
12
12
|
# # On the client end:
|
13
|
-
# service =
|
13
|
+
# service = answer_channel.client(server_channel)
|
14
14
|
#
|
15
15
|
# # asynchronous, no answer
|
16
16
|
# service.notify [:a, :request] # => nil
|
@@ -4,10 +4,25 @@ module Cod
|
|
4
4
|
# format serializers.
|
5
5
|
#
|
6
6
|
class SimpleSerializer
|
7
|
+
# Serializes obj into a format that can be transmitted via the wire. In
|
8
|
+
# this implementation, it will use Marshal.dump to turn the obj into a
|
9
|
+
# string.
|
10
|
+
#
|
11
|
+
# @param obj [Object] to dump
|
12
|
+
# @return [String] transmitted over the wire
|
13
|
+
#
|
7
14
|
def en(obj)
|
8
15
|
Marshal.dump(obj)
|
9
16
|
end
|
10
17
|
|
18
|
+
# Reads as many bytes as needed from io to reconstruct one message. Turns
|
19
|
+
# the message back into a Ruby object according to the rules of the serializer.
|
20
|
+
# In this implementation, it will use Marshal.load to turn the object
|
21
|
+
# from a String to a Ruby Object.
|
22
|
+
#
|
23
|
+
# @param io [IO] to read one message from
|
24
|
+
# @return [Object] that has been deserialized
|
25
|
+
#
|
11
26
|
def de(io)
|
12
27
|
if block_given?
|
13
28
|
Marshal.load(io, Proc.new)
|
data/lib/cod/tcp_client.rb
CHANGED
@@ -4,7 +4,23 @@ module Cod
|
|
4
4
|
# Acts as a channel that connects to a tcp listening socket on the other
|
5
5
|
# end.
|
6
6
|
#
|
7
|
+
# Connection negotiation has three phases, as follows:
|
8
|
+
# 1) Connection is establishing. Sent messages are buffered and really sent
|
9
|
+
# down the wire once the connection stands. Reading from the channel
|
10
|
+
# will block the client forever.
|
11
|
+
#
|
12
|
+
# 2) Connection is established: Sending and receiving are immediate and
|
13
|
+
# no buffering is done.
|
14
|
+
#
|
15
|
+
# 3) Connection is down because of an interruption or exception. Sending and
|
16
|
+
# receiving messages no longer works, instead a ConnectionLost error is
|
17
|
+
# raised.
|
18
|
+
#
|
7
19
|
class TcpClient < Channel
|
20
|
+
# Constructs a tcp client channel. destination may either be a socket,
|
21
|
+
# in which case phase 1) of connection negotiation is skipped, or a string
|
22
|
+
# that contains an 'address:port' part.
|
23
|
+
#
|
8
24
|
def initialize(destination, serializer)
|
9
25
|
@serializer = serializer
|
10
26
|
@destination = destination
|
@@ -23,10 +39,7 @@ module Cod
|
|
23
39
|
|
24
40
|
# The predicate for allowing sends: Is the connection up?
|
25
41
|
@work_queue.predicate {
|
26
|
-
|
27
|
-
# so no useless connections are made
|
28
|
-
@connection.try_connect
|
29
|
-
@connection.established?
|
42
|
+
cached_connection_established?
|
30
43
|
}
|
31
44
|
end
|
32
45
|
|
@@ -35,12 +48,12 @@ module Cod
|
|
35
48
|
# reconnection attempts.
|
36
49
|
#
|
37
50
|
def close
|
38
|
-
@work_queue.shutdown
|
51
|
+
@work_queue.shutdown if @work_queue
|
39
52
|
@connection.close
|
40
53
|
end
|
41
54
|
|
42
|
-
# Sends an object to the other end of the channel, if it is connected.
|
43
|
-
#
|
55
|
+
# Sends an object to the other end of the channel, if it is connected. If
|
56
|
+
# it is not connected, objects sent will queue up and once the internal
|
44
57
|
# storage reaches the high watermark, they will be dropped silently.
|
45
58
|
#
|
46
59
|
# Example:
|
@@ -55,6 +68,12 @@ module Cod
|
|
55
68
|
send(obj)
|
56
69
|
}
|
57
70
|
|
71
|
+
@work_queue.exclusive {
|
72
|
+
# If we're connected, shut down the worker thread, do all the work
|
73
|
+
# here.
|
74
|
+
check_connection_state
|
75
|
+
}
|
76
|
+
|
58
77
|
@work_queue.try_work
|
59
78
|
end
|
60
79
|
|
@@ -62,8 +81,16 @@ module Cod
|
|
62
81
|
# Options include:
|
63
82
|
#
|
64
83
|
def get(opts={})
|
65
|
-
@
|
84
|
+
while @work_queue.size > 0
|
85
|
+
@work_queue.try_work
|
86
|
+
end
|
87
|
+
|
88
|
+
check_connection_state
|
89
|
+
|
66
90
|
@connection.read(@serializer)
|
91
|
+
rescue Errno::ECONNRESET, EOFError
|
92
|
+
# Connection reset by peer
|
93
|
+
raise ConnectionLost
|
67
94
|
end
|
68
95
|
|
69
96
|
# --------------------------------------------------------- service/client
|
@@ -100,9 +127,39 @@ module Cod
|
|
100
127
|
OtherEnd.new(params)
|
101
128
|
end
|
102
129
|
private
|
130
|
+
# Checks to see in which of the three connection phases we're in. If we're
|
131
|
+
# past 1), shuts down the background worker thread.
|
132
|
+
#
|
133
|
+
def check_connection_state
|
134
|
+
# Has the connection been established in the meantime? If yes, shut
|
135
|
+
# down the work queues thread, all work will be done in this thread
|
136
|
+
# from now on.
|
137
|
+
if cached_connection_established?
|
138
|
+
@work_queue.shutdown
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns false until the connection has been established once, at which
|
143
|
+
# point it always returns true.
|
144
|
+
#
|
145
|
+
def cached_connection_established?
|
146
|
+
@cached_connection_established ||= begin
|
147
|
+
# NOTE This will not be called unless we have some messages to send,
|
148
|
+
# so no useless connections are made
|
149
|
+
@connection.try_connect
|
150
|
+
@connection.established?
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Actual sending of messages. This is invoked from either the main thread
|
155
|
+
# or the worker thread, but never both at the same time.
|
156
|
+
#
|
103
157
|
def send(msg)
|
104
158
|
@connection.write(
|
105
159
|
@serializer.en(msg))
|
160
|
+
rescue Exception => e
|
161
|
+
warn e
|
162
|
+
raise
|
106
163
|
end
|
107
164
|
|
108
165
|
# A connection that can be down. This allows elegant handling of
|
data/lib/cod/tcp_server.rb
CHANGED
@@ -29,12 +29,12 @@ module Cod
|
|
29
29
|
# as part of the message you send.
|
30
30
|
#
|
31
31
|
class TcpServer
|
32
|
-
def initialize(bind_to)
|
32
|
+
def initialize(bind_to, serializer)
|
33
33
|
@socket = TCPServer.new(*bind_to.split(':'))
|
34
34
|
@client_sockets = []
|
35
35
|
@round_robin_index = 0
|
36
36
|
@messages = Array.new
|
37
|
-
@serializer =
|
37
|
+
@serializer = serializer
|
38
38
|
end
|
39
39
|
|
40
40
|
# Receives one object from the channel.
|
data/lib/cod/work_queue.rb
CHANGED
@@ -27,6 +27,7 @@ module Cod
|
|
27
27
|
@try_work_exclusive_section = ExclusiveSection.new
|
28
28
|
|
29
29
|
@thread = Thread.start(&method(:thread_main))
|
30
|
+
@thread.priority = Thread.current.priority - 10
|
30
31
|
end
|
31
32
|
|
32
33
|
# The internal thread that is used to work on scheduled items in the
|
@@ -37,13 +38,26 @@ module Cod
|
|
37
38
|
@try_work_exclusive_section.enter {
|
38
39
|
# NOTE if predicate is nil or not set, no work will be accomplished.
|
39
40
|
# This is the way I need it.
|
40
|
-
while !@queue.empty? &&
|
41
|
+
while !@queue.empty? && predicate?
|
41
42
|
wi = @queue.shift
|
42
43
|
wi.call
|
43
44
|
end
|
44
45
|
}
|
45
46
|
end
|
46
47
|
|
48
|
+
# Allows to perform other work mutually exclusive with the work performed
|
49
|
+
# in the queue. (see ExclusiveSection)
|
50
|
+
#
|
51
|
+
def exclusive
|
52
|
+
@try_work_exclusive_section.enter { yield }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Evaluates the predicate. If the predicate is not set, this returns nil.
|
56
|
+
#
|
57
|
+
def predicate?
|
58
|
+
@predicate && @predicate.call
|
59
|
+
end
|
60
|
+
|
47
61
|
# Before any kind of work is attempted, this predicate must evaluate to
|
48
62
|
# true. It is tested repeatedly.
|
49
63
|
#
|
@@ -65,8 +79,11 @@ module Cod
|
|
65
79
|
# Shuts down the queue properly, without waiting for work to be completed.
|
66
80
|
#
|
67
81
|
def shutdown
|
82
|
+
return unless @thread
|
83
|
+
|
68
84
|
@shutdown_requested = true
|
69
85
|
@thread.join
|
86
|
+
@thread = nil
|
70
87
|
end
|
71
88
|
|
72
89
|
# Returns the size of the queue.
|
@@ -86,8 +103,8 @@ module Cod
|
|
86
103
|
Thread.current.abort_on_exception = true
|
87
104
|
|
88
105
|
loop do
|
89
|
-
|
90
|
-
|
106
|
+
Thread.pass
|
107
|
+
|
91
108
|
try_work
|
92
109
|
|
93
110
|
# Signal the outside world that we've been around this loop once.
|
data/lib/tcp_proxy.rb
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
|
2
|
+
# Proxies tcp connections on a machine from one port to the next. This is used
|
3
|
+
# for debugging and writing specifications for cod. This is not officially
|
4
|
+
# part of cod's API.
|
5
|
+
#
|
1
6
|
class TCPProxy
|
2
7
|
attr_reader :connections
|
3
8
|
|
@@ -17,6 +22,8 @@ class TCPProxy
|
|
17
22
|
@thread = Thread.start(&method(:thread_main))
|
18
23
|
end
|
19
24
|
|
25
|
+
# Closes the proxy, disrupting every connection made to it.
|
26
|
+
#
|
20
27
|
def close
|
21
28
|
@shutdown = true
|
22
29
|
|
@@ -28,16 +35,27 @@ class TCPProxy
|
|
28
35
|
@connections.each do |connection|
|
29
36
|
connection.close
|
30
37
|
end
|
31
|
-
@ins.close
|
38
|
+
@ins.close if @ins
|
32
39
|
end
|
33
40
|
|
41
|
+
# Disallows new connections.
|
42
|
+
#
|
34
43
|
def block
|
35
44
|
@accept_new = false
|
45
|
+
@ins.close
|
46
|
+
@ins = nil
|
36
47
|
end
|
48
|
+
|
49
|
+
# Allows new connections to be made. This is the default, so you only need
|
50
|
+
# this to reenable connections after a {#block}.
|
51
|
+
#
|
37
52
|
def allow
|
53
|
+
@ins = TCPServer.new(@host, @from_port)
|
38
54
|
@accept_new = true
|
39
55
|
end
|
40
56
|
|
57
|
+
# Drops all established connections.
|
58
|
+
#
|
41
59
|
def drop_all
|
42
60
|
# Copy the connections and then empty the collection
|
43
61
|
connections = @connections_m.synchronize {
|
@@ -51,6 +69,8 @@ class TCPProxy
|
|
51
69
|
|
52
70
|
# Inside the background thread ----------------------------------------
|
53
71
|
|
72
|
+
# Internal thread that pumps messages from and to ports.
|
73
|
+
# @private
|
54
74
|
def thread_main
|
55
75
|
loop do
|
56
76
|
accept_connections if @accept_new
|
@@ -67,6 +87,7 @@ class TCPProxy
|
|
67
87
|
raise
|
68
88
|
end
|
69
89
|
|
90
|
+
# @private
|
70
91
|
class Connection
|
71
92
|
def initialize(in_sock, out_sock)
|
72
93
|
@m = Mutex.new
|
@@ -96,10 +117,10 @@ class TCPProxy
|
|
96
117
|
buf = socket.read_nonblock(16*1024)
|
97
118
|
|
98
119
|
if socket == @in_sock
|
99
|
-
puts "--> #{buf.size}"
|
120
|
+
puts "--> #{buf.size}" if $DEBUG
|
100
121
|
@out_sock.write(buf)
|
101
122
|
else
|
102
|
-
puts "<-- #{buf.size}"
|
123
|
+
puts "<-- #{buf.size}" if $DEBUG
|
103
124
|
@in_sock.write(buf)
|
104
125
|
end
|
105
126
|
end
|
@@ -111,6 +132,7 @@ class TCPProxy
|
|
111
132
|
end
|
112
133
|
end
|
113
134
|
|
135
|
+
# @private
|
114
136
|
def accept_connections
|
115
137
|
loop do
|
116
138
|
in_sock = @ins.accept_nonblock
|
@@ -123,10 +145,21 @@ class TCPProxy
|
|
123
145
|
# No more connections pending, stop accepting new connections
|
124
146
|
end
|
125
147
|
|
148
|
+
# @private
|
126
149
|
def forward_data
|
127
150
|
connections = @connections_m.synchronize { @connections.dup }
|
151
|
+
remove_list = []
|
128
152
|
connections.each do |conn|
|
129
|
-
|
153
|
+
begin
|
154
|
+
conn.pump_synchronized
|
155
|
+
rescue EOFError
|
156
|
+
# Socket was closed, remove from the collection.
|
157
|
+
remove_list << conn
|
158
|
+
end
|
130
159
|
end
|
160
|
+
|
161
|
+
@connections_m.synchronize {
|
162
|
+
@connections -= remove_list
|
163
|
+
}
|
131
164
|
end
|
132
165
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cod
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
12
|
+
date: 2012-02-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
16
|
-
requirement: &
|
16
|
+
requirement: &70357075424760 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70357075424760
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &70357075424320 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70357075424320
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: flexmock
|
38
|
-
requirement: &
|
38
|
+
requirement: &70357075423900 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70357075423900
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
49
|
-
requirement: &
|
48
|
+
name: yard
|
49
|
+
requirement: &70357075423480 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70357075423480
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: guard
|
60
|
-
requirement: &
|
60
|
+
requirement: &70357075423060 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70357075423060
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: growl
|
71
|
-
requirement: &
|
71
|
+
requirement: &70357075422640 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,7 +76,7 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70357075422640
|
80
80
|
description:
|
81
81
|
email: kaspar.schiess@absurd.li
|
82
82
|
executables: []
|
@@ -108,12 +108,12 @@ files:
|
|
108
108
|
- lib/cod/work_queue.rb
|
109
109
|
- lib/cod.rb
|
110
110
|
- lib/tcp_proxy.rb
|
111
|
+
- examples/bs_ping_pong/ping.rb
|
112
|
+
- examples/bs_ping_pong/pong.rb
|
111
113
|
- examples/example_scaffold.rb
|
112
114
|
- examples/master_child.rb
|
113
115
|
- examples/netcat/README
|
114
116
|
- examples/netcat/server.rb
|
115
|
-
- examples/ping_pong/ping.rb
|
116
|
-
- examples/ping_pong/pong.rb
|
117
117
|
- examples/presence/client.rb
|
118
118
|
- examples/presence/server.rb
|
119
119
|
- examples/protocol-buffers/master_child.rb
|
@@ -140,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
140
|
version: '0'
|
141
141
|
segments:
|
142
142
|
- 0
|
143
|
-
hash: -
|
143
|
+
hash: -4089674409401429686
|
144
144
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
145
|
none: false
|
146
146
|
requirements:
|
@@ -149,11 +149,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
149
|
version: '0'
|
150
150
|
segments:
|
151
151
|
- 0
|
152
|
-
hash: -
|
152
|
+
hash: -4089674409401429686
|
153
153
|
requirements: []
|
154
154
|
rubyforge_project:
|
155
|
-
rubygems_version: 1.8.
|
155
|
+
rubygems_version: 1.8.16
|
156
156
|
signing_key:
|
157
157
|
specification_version: 3
|
158
|
-
summary: Really simple IPC.
|
158
|
+
summary: Really simple IPC. Pipes, TCP sockets, beanstalkd, ...
|
159
159
|
test_files: []
|
data/examples/ping_pong/ping.rb
DELETED