cod 0.4.2 → 0.4.3
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/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