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.
@@ -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
- Complete rewrite of the code: Did away with some of the complexities that
27
- stemmed from early design work. The functionality that is there is much like
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
- At version 0.4.0
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 'sdoc'
19
-
20
- # Generate documentation
21
- RDoc::Task.new do |rdoc|
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
@@ -9,7 +9,7 @@ service_channel = Cod.pipe
9
9
  answer_channel = Cod.pipe
10
10
 
11
11
  child_pid = fork do
12
- service = Cod.service(service_channel)
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 = Cod.client(service_channel, answer_channel)
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
- # The core concept of Cod are 'channels'. (Cod::Channel::Base) You can create
2
- # such channels on top of the various transport layers. Once you have such a
3
- # channel, you #put messages into it and you #get messages out of it. Messages
4
- # are retrieved in FIFO manner, making channels look like a communication pipe
5
- # most of the time.
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. (see Cod::Pipe)
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
- # (see Cod::TcpClient)
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. (see
54
- # Cod::TcpServer)
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(bind_to)
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. (see
62
- # Cod::Beanstalk::Channel)
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
- # NOTE: Beanstalk channels cannot currently be used in Cod.select. This is
4
- # due to limitations inherent in the beanstalkd protocol. We'll probably
5
- # try to get a patch into beanstalkd to change this.
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
- # NOTE: If you embed a beanstalk channel into one of your messages, you will
8
- # get a channel that connects to the same server and the same tube on the
9
- # other end. This behaviour is useful for Cod::Service.
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
- def bs_delete(msg_id) # :nodoc:
179
+ # @private
180
+ def bs_delete(msg_id)
138
181
  bs_command([:delete, msg_id], :deleted)
139
182
  end
140
- def bs_release(msg_id) # :nodoc:
183
+ # @private
184
+ def bs_release(msg_id)
141
185
  bs_command([:release, msg_id, JOB_PRIORITY, 0], :released)
142
186
  end
143
- def bs_release_with_delay(msg_id, seconds) # :nodoc:
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?
@@ -36,6 +36,8 @@ module Cod::Beanstalk
36
36
 
37
37
  def de(io)
38
38
  str = io.gets("\r\n")
39
+ raise Cod::ConnectionLost unless str
40
+
39
41
  raw = str.split
40
42
 
41
43
  cmd = convert_cmd(raw.first)
@@ -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
@@ -1,33 +1,39 @@
1
1
  module Cod
2
- # Channels transport ruby objects from one end to the other. The
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 (Cod::Channel) is the abstract superclass of all Cod channels.
17
- # It doesn't have a transport by its own, but implements the whole interface
18
- # for documentation purposes.
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
@@ -6,11 +6,11 @@ module Cod
6
6
  #
7
7
  # Synopsis:
8
8
  # # On the server end:
9
- # service = Cod.service(central_location)
9
+ # service = server_channel.service
10
10
  # service.one { |request| :answer }
11
11
  #
12
12
  # # On the client end:
13
- # service = Cod.client(central_location, answer_here)
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)
@@ -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
- # 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?
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
- # If it is not connected, objects sent will queue up and once the internal
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
- @connection.try_connect
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
@@ -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 = SimpleSerializer.new
37
+ @serializer = serializer
38
38
  end
39
39
 
40
40
  # Receives one object from the channel.
@@ -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? && @predicate && @predicate.call
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
- sleep 0.01
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.
@@ -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
- conn.pump_synchronized
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.2
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-15 00:00:00.000000000 Z
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: &70132592401200 !ruby/object:Gem::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: *70132592401200
24
+ version_requirements: *70357075424760
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70132592400460 !ruby/object:Gem::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: *70132592400460
35
+ version_requirements: *70357075424320
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: flexmock
38
- requirement: &70132592399720 !ruby/object:Gem::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: *70132592399720
46
+ version_requirements: *70357075423900
47
47
  - !ruby/object:Gem::Dependency
48
- name: sdoc
49
- requirement: &70132592399020 !ruby/object:Gem::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: *70132592399020
57
+ version_requirements: *70357075423480
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: guard
60
- requirement: &70132592398400 !ruby/object:Gem::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: *70132592398400
68
+ version_requirements: *70357075423060
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: growl
71
- requirement: &70132592397740 !ruby/object:Gem::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: *70132592397740
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: -1652399252235665508
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: -1652399252235665508
152
+ hash: -4089674409401429686
153
153
  requirements: []
154
154
  rubyforge_project:
155
- rubygems_version: 1.8.10
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: []
@@ -1,9 +0,0 @@
1
- $:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
2
- require 'cod'
3
-
4
- pipe = Cod.beanstalk('localhost:11300', 'pingpong')
5
-
6
- loop do
7
- pipe.put Time.now
8
- sleep 1
9
- end
@@ -1,9 +0,0 @@
1
- $:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
2
- require 'cod'
3
-
4
- pipe = Cod.beanstalk('localhost:11300', "pingpong")
5
-
6
- loop do
7
- puts "Received: "+pipe.get.inspect
8
- end
9
-