cod 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,5 +1,6 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ gem 'uuid'
3
4
  gem 'beanstalk-client'
4
5
 
5
6
  group :test do
@@ -9,6 +10,7 @@ group :test do
9
10
  gem 'guard'
10
11
  gem 'guard-rspec'
11
12
 
13
+ gem 'rdoc'
12
14
  gem 'sdoc'
13
15
 
14
16
  gem 'growl'
data/HISTORY.txt CHANGED
@@ -1,4 +1,17 @@
1
1
 
2
+ == 0.3.1 / ???
3
+
4
+ + Large improvements in resilience to crashes, reconnects are now attempted
5
+ but can be improved further.
6
+
7
+ + Both sides of an 1:n directory:topic structure can now crash without
8
+ permanent message loss.
9
+
10
+ + tcp channels now have a #connected? method that indicates permanent
11
+ unrecoverable disconnection.
12
+
13
+ - at_fork extension is now permanently gone.
14
+
2
15
  == 0.3 / 20Jul2011
3
16
 
4
17
  + Cod.tcpserver and Cod.tcp channels. Allows direct connection via
data/README CHANGED
@@ -19,8 +19,8 @@ SYNOPSIS
19
19
  service.one { |msg| :response } # Process A
20
20
  client.call :ruby_object # => :response # Process B
21
21
 
22
- # And more: Publish/Subscribe, easy construction of more advanced distributed
23
- # communication.
22
+ # And more: Publish/Subscribe, easy construction of more advanced
23
+ # distributed communication.
24
24
 
25
25
  STATUS
26
26
 
@@ -28,6 +28,6 @@ Becoming more useful by the day. Most things will work nicely already,
28
28
  although error handling is not production quality. Toy around with it now
29
29
  and give me feedback!
30
30
 
31
- At version 0.3
31
+ At version 0.3.1
32
32
 
33
33
  (c) 2011 Kaspar Schiess
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'psych'
2
2
  require "rubygems"
3
- require "rake/rdoctask"
3
+ require "rdoc/task"
4
4
  require 'rspec/core/rake_task'
5
5
  require 'rubygems/package_task'
6
6
 
@@ -12,7 +12,7 @@ task :default => :spec
12
12
  require 'sdoc'
13
13
 
14
14
  # Generate documentation
15
- Rake::RDocTask.new do |rdoc|
15
+ RDoc::Task.new do |rdoc|
16
16
  rdoc.title = "parslet - construction of parsers made easy"
17
17
  rdoc.options << '--line-numbers'
18
18
  rdoc.options << '--fmt' << 'shtml' # explictly set shtml generator
@@ -0,0 +1,15 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'cod'
3
+
4
+
5
+ raise unless ARGV.first
6
+
7
+ client = Cod.tcp('localhost:12345')
8
+ client.put [client, ARGV.first]
9
+
10
+ puts "Waiting..."
11
+ $stdin.gets
12
+ client.close
13
+ $stdin.gets
14
+
15
+
@@ -0,0 +1,30 @@
1
+
2
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
3
+ require 'cod'
4
+
5
+
6
+ present = {}
7
+ server = Cod.tcpserver('localhost:12345')
8
+
9
+ loop do
10
+ # Process connection requests
11
+ while server.waiting?
12
+ connection, attributes = server.get
13
+
14
+ present[connection] = attributes
15
+ end
16
+
17
+ # Check if all connections are alive
18
+ remove = []
19
+ present.each do |conn, attrs|
20
+ if conn.connected?
21
+ puts "Alive: #{attrs.inspect}"
22
+ else
23
+ puts "Dead: #{attrs.inspect}"
24
+ remove << conn
25
+ end
26
+ end
27
+
28
+ remove.each { |conn| present.delete(conn) }
29
+ sleep 1
30
+ end
@@ -0,0 +1,12 @@
1
+
2
+ This is an example of PUB/SUB style messaging. You should run one directory
3
+ and any number of clients. All clients should receive timestamps from the directory, once every second.
4
+
5
+ Experiments that can be made using this setup:
6
+
7
+ 1) Disconnect a client, restart it. It should start receiving updates
8
+ immediately.
9
+
10
+ 2) Disconnect the directory, restart it. It should start redistributing
11
+ updates to all clients within 5 seconds.
12
+
@@ -0,0 +1,13 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
2
+ require 'cod'
3
+
4
+ channels = Struct.new(:directory, :answers).new(
5
+ Cod.beanstalk('localhost:11300', 'directory'),
6
+ Cod.beanstalk('localhost:11300', 'directory.'+Cod.uuid))
7
+
8
+ topic = Cod::Topic.new('', channels.directory, channels.answers, :renew => 5)
9
+
10
+ loop do
11
+ puts topic.get
12
+ end
13
+
@@ -0,0 +1,13 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../../lib")
2
+ require 'cod'
3
+
4
+ channels = Struct.new(:directory).new(
5
+ Cod.beanstalk('localhost:11300', 'directory'))
6
+
7
+ directory = Cod::Directory.new(channels.directory)
8
+
9
+ loop do
10
+ directory.publish '', Time.now
11
+ sleep 1
12
+ end
13
+
data/lib/cod.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'at_fork'
1
+ require 'uuid'
2
2
 
3
3
  # The core concept of Cod are 'channels'. (Cod::Channel::Base) You can create
4
4
  # such channels on top of the various transport layers. Once you have such a
@@ -45,7 +45,9 @@ module Cod
45
45
  # chan = Cod.beanstalk('localhost:11300', 'my_tube')
46
46
  #
47
47
  def beanstalk(url, name)
48
- context.beanstalk(url, name)
48
+ Cod::Channel::Beanstalk.new(
49
+ Connection::Beanstalk.new(url),
50
+ name)
49
51
  end
50
52
  module_function :beanstalk
51
53
 
@@ -66,31 +68,43 @@ module Cod
66
68
  # chan = Cod.pipe
67
69
  #
68
70
  def pipe(name=nil)
69
- context.pipe(name)
71
+ Cod::Channel::Pipe.new(name)
70
72
  end
71
73
  module_function :pipe
72
74
 
75
+ # Creates a tcp connection to the destination and returns a channel for it.
76
+ #
73
77
  def tcp(destination)
74
- context.tcp(destination)
78
+ Cod::Channel::TCPConnection.new(destination)
75
79
  end
76
80
  module_function :tcp
77
81
 
82
+ # Creates a tcp listener on bind_to and returns a channel for it.
83
+ #
78
84
  def tcpserver(bind_to)
79
- context.tcpserver(bind_to)
85
+ Cod::Channel::TCPServer.new(bind_to)
80
86
  end
81
87
  module_function :tcpserver
82
88
 
83
- def context # :nodoc:
84
- @convenience_context ||= Context.new
85
- end
86
- module_function :context
87
-
88
- # For testing mainly
89
+ # Returns a UUID that should be unique for this machine (based on MAC), this
90
+ # Thread and Process. Even after a fork.
91
+ #
92
+ # This is used to create identity on the network. Internal method.
89
93
  #
90
- def reset # :nodoc:
91
- @convenience_context = nil
94
+ def uuid
95
+ uuid_generator.generate
96
+ end
97
+ def uuid_generator
98
+ pid, generator = Thread.current[:_cod_uuid_generator]
99
+
100
+ if pid && Process.pid == pid
101
+ return generator
102
+ end
103
+
104
+ pid, generator = Thread.current[:_cod_uuid_generator] = [Process.pid, UUID.new]
105
+ return generator
92
106
  end
93
- module_function :reset
107
+ module_function :uuid, :uuid_generator
94
108
  end
95
109
 
96
110
  module Cod::Connection; end
@@ -105,7 +119,6 @@ require 'cod/channel/beanstalk'
105
119
  require 'cod/channel/tcpconnection'
106
120
  require 'cod/channel/tcpserver'
107
121
 
108
- require 'cod/context'
109
122
  require 'cod/client'
110
123
 
111
124
  require 'cod/service'
@@ -40,6 +40,11 @@ module Cod
40
40
  # channel.close
41
41
  #
42
42
  class Channel::Base
43
+ def initialize(reader, writer)
44
+ @reader = reader
45
+ @writer = writer
46
+ end
47
+
43
48
  # Writes a Ruby object (the 'message') to the channel. This object will
44
49
  # be queued in the channel and become available for #get in a FIFO manner.
45
50
  #
@@ -51,7 +56,8 @@ module Cod
51
56
  # chan.put :symbol
52
57
  #
53
58
  def put(message)
54
- not_implemented
59
+ # TODO Errno::EPIPE raised after a while when the receiver goes away.
60
+ @writer.put(message)
55
61
  end
56
62
 
57
63
  # Reads a Ruby object (a message) from the channel. Some channels may not
@@ -60,17 +66,30 @@ module Cod
60
66
  # <code>:timeout</code> :: Time to wait before throwing Cod::Channel::TimeoutError.
61
67
  #
62
68
  def get(opts={})
63
- not_implemented
69
+ @reader.get(opts)
64
70
  end
65
71
 
66
72
  # Returns true if there are messages waiting in the channel.
67
73
  #
68
74
  def waiting?
75
+ # TODO EOFError is thrown when the other end has gone away
76
+ @reader.waiting?
77
+ end
78
+
79
+ # Returns true if the channel is connected, and false if all hope must be
80
+ # given up of reconnecting this channel.
81
+ #
82
+ def connected?
69
83
  not_implemented
70
84
  end
71
85
 
86
+ # Closes reader and writer.
87
+ #
72
88
  def close
73
- not_implemented
89
+ @reader.close if @reader
90
+ @writer.close if @writer
91
+
92
+ @reader = @writer = nil
74
93
  end
75
94
 
76
95
  # Returns the Identifier class below the current channel class. This is
@@ -154,9 +173,13 @@ module Cod
154
173
  end
155
174
 
156
175
  def not_implemented
157
- raise NotImplementedError,
176
+ trace = caller.reject {|l| l =~ %r{#{Regexp.escape(__FILE__)}}} # blatantly stolen from dependencies.rb in activesupport
177
+ exception = NotImplementedError.new(
158
178
  "You called a method in Cod::Channel::Base. Missing implementation in "+
159
- "the subclass!"
179
+ "the subclass #{self.class.name}!")
180
+ exception.set_backtrace trace
181
+
182
+ raise exception
160
183
  end
161
184
  end
162
185
  end
@@ -46,7 +46,7 @@ module Cod
46
46
  end
47
47
 
48
48
  def close
49
- @connection = @reference = nil
49
+ connection.close
50
50
  end
51
51
 
52
52
  def identifier
@@ -79,8 +79,10 @@ module Cod
79
79
  private
80
80
  def init_in_and_out
81
81
  serializer = ObjectIO::Serializer.new
82
- @in = ObjectIO::Reader.new(serializer) { fds.r }
83
- @out = ObjectIO::Writer.new(serializer) { fds.w }
82
+ read_pool = ObjectIO::Connection::Single.new { fds.r }
83
+ write_pool = ObjectIO::Connection::Single.new { fds.w }
84
+ @in = ObjectIO::Reader.new(serializer, read_pool)
85
+ @out = ObjectIO::Writer.new(serializer, write_pool) { fds.w }
84
86
  end
85
87
 
86
88
  def close_write
@@ -12,6 +12,8 @@ module Cod
12
12
  #
13
13
  attr_reader :destination
14
14
 
15
+ attr_reader :connection_pool
16
+
15
17
  def initialize(destination_or_connection)
16
18
  if destination_or_connection.respond_to?(:to_str)
17
19
  @destination = split_uri(destination_or_connection)
@@ -20,23 +22,18 @@ module Cod
20
22
  @destination = nil
21
23
  @connection = destination_or_connection
22
24
  end
23
-
25
+
24
26
  serializer = ObjectIO::Serializer.new
25
- @writer = ObjectIO::Writer.new(serializer, &method(:reconnect))
26
- @reader = ObjectIO::Reader.new(serializer) { reconnect }
27
- end
28
-
29
- def put(message)
30
- @writer.put(message)
31
- end
32
-
33
- def get(opts={})
34
- @reader.get(opts)
27
+ @connection_pool = ObjectIO::Connection::Single.new { connect }
28
+
29
+ super(
30
+ ObjectIO::Reader.new(serializer, @connection_pool),
31
+ ObjectIO::Writer.new(serializer, @connection_pool))
35
32
  end
36
-
37
- def close
38
- @connection.close if @connection
39
- @connection = nil
33
+
34
+ def connected?
35
+ waiting?
36
+ connection_pool.size > 0
40
37
  end
41
38
 
42
39
  def identifier
@@ -44,15 +41,12 @@ module Cod
44
41
  end
45
42
 
46
43
  private
47
- # Establishes connection in @connection. If a previous connection is
48
- # in error state, it attempts to make a new connection.
49
- #
50
- def reconnect
51
- @connection ||= TCPSocket.new(*destination)
52
- rescue Errno::ECONNREFUSED
53
- # The other end doesn't exist as of this moment. Have the caller retry
54
- # later on.
55
- nil
44
+ def connect
45
+ if destination
46
+ TCPSocket.new(*destination)
47
+ else
48
+ @connection
49
+ end
56
50
  end
57
51
  end
58
52
 
@@ -20,37 +20,33 @@ module Cod
20
20
  def initialize(bind_to)
21
21
  @bind_to = split_uri(bind_to)
22
22
  @server = TCPServer.new(*@bind_to)
23
-
23
+
24
+ connection_pool = ObjectIO::Connection::Pool.new { accept_connections(server) }
24
25
  serializer = ObjectIO::Serializer.new(self)
25
- @reader = ObjectIO::Reader.new(serializer) {
26
- accept_connections(server)
27
- }
28
- end
29
-
30
- def get(opts={})
31
- # Read a message from the wire and transform all contained objects.
32
- @reader.get(opts)
26
+
27
+ super(
28
+ ObjectIO::Reader.new(serializer, connection_pool),
29
+ nil)
33
30
  end
34
-
31
+
32
+ # Sending a message to the server side of a socket is not supported.
33
+ # Transmit the client side channel instance to be able to write back to it
34
+ # on the server side. This overwrites #put in Base for the sole purpose of
35
+ # raising a better error.
36
+ #
35
37
  def put(message)
36
38
  communication_error "You cannot write to the server directly, transmit a "
37
39
  "channel to the server instead."
38
40
  end
39
41
 
40
- def waiting?
41
- @reader.waiting?
42
- end
43
-
44
42
  def close
45
- @reader.close if @reader
43
+ super
44
+
46
45
  server.close if server
47
-
48
46
  @server = nil
49
- @reader = nil
50
47
  end
51
48
 
52
49
  def transform(socket, obj)
53
- # p [:tcp_server_deserialize, obj]
54
50
  if obj.kind_of?(Channel::TCPConnection::Identifier)
55
51
  # We've been sent 'a' tcp channel. Assume that it's our own client end
56
52
  # that we've been sent and turn it into a channel that communicates
@@ -72,13 +68,17 @@ module Cod
72
68
  def accept_connections(server)
73
69
  connections = []
74
70
  loop do
75
- connection = server.accept_nonblock
76
- connections << connection
71
+ # Try connecting more sockets.
72
+ begin
73
+ connections << server.accept_nonblock
74
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
75
+ # Means that no more connects are pending. Ignore, since this is exactly
76
+ # one of the termination conditions for this method.
77
+ return connections
78
+ end
77
79
  end
78
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK, Errno::ECONNABORTED, Errno::EPROTO, Errno::EINTR
79
- # Means that no more connects are pending. Ignore, since this is exactly
80
- # one of the termination conditions for this method.
81
- return connections
80
+
81
+ fail "NOTREACHED: return should be from loop."
82
82
  end
83
83
  end
84
84
  end
@@ -52,7 +52,7 @@ module Cod
52
52
  # Closes the connection
53
53
  #
54
54
  def close
55
- connection.close
55
+ connection.close if connection
56
56
  @connection = nil
57
57
  end
58
58
 
data/lib/cod/directory.rb CHANGED
@@ -8,19 +8,40 @@ module Cod
8
8
  #
9
9
  attr_reader :channel
10
10
 
11
+ # Subscriptions this directory handles.
12
+ #
13
+ attr_reader :subscriptions
14
+
11
15
  def initialize(channel)
12
16
  @channel = channel
13
- @subscriptions = []
17
+ @subscriptions = Set.new
14
18
  end
15
19
 
16
- # Sends the message to all subscribers that listen to this topic.
20
+ # Sends the message to all subscribers that listen to this topic. Returns
21
+ # the number of subscribers this message has been sent to.
17
22
  #
18
23
  def publish(topic, message)
19
- handle_subscriptions
20
-
24
+ process_control_messages
25
+
26
+ n = 0
27
+ failed_subscriptions = []
21
28
  for subscription in @subscriptions
22
- subscription.put message if subscription === topic
29
+ begin
30
+ if subscription === topic
31
+ subscription.put message
32
+ n += 1
33
+ end
34
+ # TODO reenable this with more specific exception list.
35
+ rescue Cod::Channel::DirectionError
36
+ # Writing message failed; remove the subscription.
37
+ failed_subscriptions << subscription
38
+ end
23
39
  end
40
+
41
+ process_control_messages
42
+
43
+ remove_subscriptions { |sub| failed_subscriptions.include?(sub) }
44
+ return n
24
45
  end
25
46
 
26
47
  # Closes all resources used by the directory.
@@ -28,17 +49,50 @@ module Cod
28
49
  def close
29
50
  channel.close
30
51
  end
52
+
53
+ # Internal use: Subscribe a new topic to this directory.
54
+ #
55
+ def subscribe(subscription, status=:new)
56
+ if status == :new && subscriptions.include?(subscription)
57
+ raise "UUID collision? I already have a subscription for #{subscription.identifier}."
58
+ end
59
+
60
+ @subscriptions << subscription
61
+ end
31
62
 
32
- private
33
-
34
- def handle_subscriptions
63
+ # Internal method to process messages that are inbound on the directory
64
+ # control channel.
65
+ #
66
+ def process_control_messages(now = Time.now)
67
+ # Handle incoming messages on channel
35
68
  while channel.waiting?
36
- subscribe channel.get
69
+ cmd, *rest = channel.get(timeout: 0.1)
70
+ case cmd
71
+ when :subscribe
72
+ subscription, status = *rest
73
+ subscribe subscription, status
74
+ when :ping
75
+ ping_id = rest.first
76
+ subscriptions.
77
+ find { |sub| sub.identifier == ping_id }.
78
+ ping
79
+ else
80
+ warn "Unknown command received: #{cmd.inspect} (#{rest.inspect})"
81
+ end
37
82
  end
83
+
84
+ # Remove all stale subscriptions
85
+ remove_subscriptions { |sub| sub.stale?(now) }
86
+ rescue ArgumentError
87
+ # Probably we could not create a duplicate of a serialized channel.
88
+ # Ignore this round of subscriptions.
38
89
  end
39
-
40
- def subscribe(subscription)
41
- @subscriptions << subscription
90
+
91
+ private
92
+ def remove_subscriptions(&block)
93
+ @subscriptions.delete_if(&block)
42
94
  end
43
95
  end
44
- end
96
+ end
97
+
98
+ require 'cod/directory/countdown'
@@ -0,0 +1,31 @@
1
+ class Cod::Directory
2
+ class Countdown
3
+ attr_reader :run_time
4
+
5
+ def initialize(run_time=30*60, now = Time.now)
6
+ @run_time = run_time
7
+ start(now); stop
8
+ end
9
+
10
+ def elapsed?(now = Time.now)
11
+ if running?
12
+ return (now - @started_at) > @run_time
13
+ else
14
+ return (@stopped_at - @started_at) > @run_time
15
+ end
16
+ end
17
+
18
+ def running?
19
+ @started_at && !@stopped_at
20
+ end
21
+
22
+ def start(now = Time.now)
23
+ @started_at = now
24
+ @stopped_at = nil
25
+ end
26
+
27
+ def stop(now = Time.now)
28
+ @stopped_at = now
29
+ end
30
+ end
31
+ end
@@ -1,21 +1,59 @@
1
- module Cod
2
- # Represents a subscription to a directory.
1
+ class Cod::Directory
2
+ # Represents a subscription to a directory. The subscription is what links
3
+ # the topic to the directory. It carries the topic id as identifier; this is
4
+ # what gives the subscription identity. If two subscriptions in a system
5
+ # have the same identifier, they link to the same topic instance and should
6
+ # not be sent the same message twice.
3
7
  #
4
- class Directory::Subscription
8
+ class Subscription
5
9
  attr_reader :matcher
6
10
  attr_reader :channel
11
+ attr_reader :countdown
7
12
 
8
- def initialize(matcher, channel)
13
+ def initialize(matcher, channel, topic_id)
9
14
  @matcher = matcher
10
15
  @channel = channel
16
+ @countdown = Countdown.new
17
+ @identifier = topic_id
11
18
  end
12
19
 
13
20
  def ===(other)
14
21
  matcher === other
15
22
  end
16
23
 
24
+ def identifier
25
+ @identifier
26
+ end
27
+ def eql?(other)
28
+ hash == other.hash &&
29
+ identifier == other.identifier
30
+ end
31
+ alias == eql?
32
+ def hash
33
+ identifier.hash
34
+ end
35
+
17
36
  def put(msg)
18
- channel.put msg
37
+ countdown.start
38
+
39
+ # Envelope the message to send this subscription id along
40
+ channel.put [identifier, msg]
41
+ end
42
+
43
+ # Is this subscription stale? Staleness is determined by an internal
44
+ # countdown since last #put operation.
45
+ #
46
+ def stale?(now=Time.now)
47
+ countdown.running? && countdown.elapsed?(now)
48
+ end
49
+
50
+ # Tells the subscription that the other end has sent back a ping. This
51
+ # always marks the subscription alive.
52
+ #
53
+ def ping(now= Time.now)
54
+ # Stop the countdown where it is. If it has elapsed?, the subscription
55
+ # will be marked as stale?
56
+ countdown.stop(now)
19
57
  end
20
58
  end
21
59
  end
data/lib/cod/object_io.rb CHANGED
@@ -1,3 +1,6 @@
1
+ module Cod::ObjectIO; end
2
+
3
+ require 'cod/objectio/connection'
1
4
  require 'cod/objectio/reader'
2
5
  require 'cod/objectio/writer'
3
6
  require 'cod/objectio/serializer'
@@ -0,0 +1,106 @@
1
+ module Cod::ObjectIO::Connection
2
+ class Pool
3
+ include Enumerable
4
+
5
+ attr_reader :connect_action
6
+ attr_reader :connections
7
+
8
+ # Example:
9
+ # Connection::Single.new { TCPSocket.new('...') }
10
+ # Connection::Pool.new { socket.accept }
11
+ #
12
+ def initialize(&connect_action)
13
+ raise ArgumentError unless connect_action
14
+
15
+ @connect_action = connect_action
16
+ @connections = []
17
+ end
18
+
19
+ def size
20
+ @connections.size
21
+ end
22
+
23
+ def report_failed(connection)
24
+ @connections.delete_if { |e| e == connection }
25
+ end
26
+
27
+ def accept
28
+ @connections += call_connect
29
+ end
30
+
31
+ def each(&block)
32
+ @connections.each(&block)
33
+ end
34
+
35
+ # Closes all connections in this pool.
36
+ #
37
+ def close
38
+ self.each do |connection|
39
+ begin
40
+ connection.close
41
+ rescue IOError
42
+ # Maybe someone else that shares this connection closed it already?
43
+ # DO NOTHING
44
+ end
45
+ end
46
+ end
47
+ private
48
+ # Calls the connect_action block and normalizes the result to be either
49
+ # an array or nil.
50
+ #
51
+ def call_connect
52
+ result = connect_action[]
53
+ if result
54
+ return [result].flatten
55
+ end
56
+
57
+ return nil
58
+ end
59
+ end
60
+
61
+ # Implements a single connection that is retried on failure for a number
62
+ # of times.
63
+ #
64
+ class Single < Pool
65
+ MAX_FAILURES = 10
66
+
67
+ def initialize(&connect_action)
68
+ super
69
+
70
+ @connected = false
71
+ @failures = 0
72
+ end
73
+
74
+ def report_failed(connection)
75
+ super
76
+
77
+ @failures += 1
78
+ @connected = false
79
+ end
80
+
81
+ def accept
82
+ return if @connected
83
+
84
+ # How many times have we reconnected? Maybe just give up.
85
+ permanent_connection_error if @failures >= MAX_FAILURES
86
+
87
+ # Try and make a new connection: Returns it on success.
88
+ new_connection = call_connect
89
+ if new_connection
90
+ @connected = true
91
+ @connections += new_connection
92
+ return
93
+ end
94
+
95
+ # No new connection could be made. Count this as a failure.
96
+ @failures += 1
97
+ return
98
+ end
99
+
100
+ private
101
+ def permanent_connection_error
102
+ raise Cod::Channel::CommunicationError,
103
+ "Permanent connection failure: Giving up."
104
+ end
105
+ end
106
+ end
@@ -3,55 +3,25 @@ module Cod::ObjectIO
3
3
  #
4
4
  class Reader
5
5
  attr_reader :waiting_messages
6
- attr_reader :registered_ios
6
+ attr_reader :pool
7
7
 
8
8
  # Initializes an object reader that reads from one or several IO objects.
9
- # You can either pass the io object in the constructor (io) or you can
10
- # provide the instance with a block that is called each time a read is
11
- # attempted. The block should return an array of IO objects to also read
12
- # from.
13
9
  #
14
10
  # Example:
15
- # reader = Reader.new { make_connection }
11
+ # connection = Connection::Pool.new { make_new_connection }
12
+ # reader = Reader.new(serializer, connection)
16
13
  #
17
- def initialize(serializer, io=nil, &block)
14
+ def initialize(serializer, conn_pool)
18
15
  @serializer = serializer
19
16
  @waiting_messages = []
20
- @establish_block = block
21
- @registered_ios = Set.new
22
-
23
- register io if io
17
+ @pool = conn_pool
24
18
  end
25
19
 
26
- # Called before each attempt to read from the wire. This should return
27
- # the IO objects that need to be considered when reading.
28
- #
29
- def establish
30
- sockets = @establish_block && @establish_block.call(@io) ||
31
- nil
32
-
33
- [sockets].flatten
34
- end
35
-
36
- def register(ios)
37
- return unless ios
38
- ios.each do |io|
39
- registered_ios << io
40
- end
41
- end
42
-
43
- def unregister(ios)
44
- ios.each do |io|
45
- registered_ios.delete(io)
46
- end
47
- end
48
-
49
20
  def get(opts={})
50
21
  return waiting_messages.shift if queued?
51
22
 
52
23
  start_time = Time.now
53
24
  loop do
54
- # p [:looping, opts]
55
25
  read_from_wire opts
56
26
 
57
27
  # Early return in case we have a message waiting
@@ -69,6 +39,8 @@ module Cod::ObjectIO
69
39
  def waiting?
70
40
  read_from_wire
71
41
  queued?
42
+ rescue Cod::Channel::CommunicationError
43
+ queued?
72
44
  end
73
45
 
74
46
  def queued?
@@ -76,7 +48,7 @@ module Cod::ObjectIO
76
48
  end
77
49
 
78
50
  def close
79
- @registered_ios.each { |io| io.close }
51
+ @pool.close
80
52
  end
81
53
 
82
54
  private
@@ -85,13 +57,10 @@ module Cod::ObjectIO
85
57
  #
86
58
  def read_from_wire(opts={})
87
59
  # Establish new connections and register them
88
- register establish
60
+ @pool.accept
89
61
 
90
- # Wait for sockets to have data
91
- ready_read, _, _ = IO.select(Array(registered_ios), nil, nil, 0.1)
92
-
93
- # Read all ready sockets
94
- process_nonblock(ready_read) if ready_read
62
+ # Process all waiting data
63
+ process_nonblock(@pool.connections)
95
64
  end
96
65
 
97
66
  # Reads all data waiting in each io in the ios array.
@@ -111,11 +80,11 @@ module Cod::ObjectIO
111
80
  while not sio.eof?
112
81
  waiting_messages << deserialize(io, sio)
113
82
  end
83
+ rescue Errno::EAGAIN
84
+ # read failed because there was no data. This is expected.
85
+ return
114
86
  rescue EOFError
115
- # Connection has failed/ been disconnected.
116
- # We will need to reconnect this. If possible.
117
- registered_ios.delete(io)
118
- raise
87
+ @pool.report_failed(io)
119
88
  end
120
89
 
121
90
  # Deserializes a message (in message format, string) into the object that
@@ -2,29 +2,24 @@ module Cod::ObjectIO
2
2
  # Writes objects to an IO stream.
3
3
  #
4
4
  class Writer
5
- def initialize(serializer, io=nil, &block)
5
+ def initialize(serializer, pool)
6
6
  @serializer = serializer
7
- @io = io
8
- @reconnect_block = block
7
+ @pool = pool
9
8
  end
10
9
 
11
10
  def put(message)
12
- attempt_reconnect
11
+ @pool.accept
13
12
 
14
- @io.write(serialize(message)) if @io
13
+ @pool.each do |connection|
14
+ connection.write(serialize(message))
15
+ end
15
16
  end
16
17
 
17
18
  def close
18
- @io.close
19
+ @pool.close
19
20
  end
20
21
 
21
22
  private
22
- def attempt_reconnect
23
- if @reconnect_block
24
- @io = @reconnect_block[]
25
- end
26
- end
27
-
28
23
  def serialize(message)
29
24
  @serializer.serialize(message)
30
25
  end
data/lib/cod/topic.rb CHANGED
@@ -4,9 +4,33 @@ module Cod
4
4
  class Topic
5
5
  attr_reader :answers, :directory
6
6
  attr_reader :match_expr
7
- def initialize(match_expr, directory_channel, answer_channel)
7
+ attr_reader :identifier
8
+ attr_reader :subscription
9
+ attr_reader :renew_countdown
10
+
11
+ # Creates a topic that subscribes to a part of a directory. The match_expr
12
+ # decides which messages get forwarded to this topic, it limits the
13
+ # topic to a subset of the messages in the directory.
14
+ #
15
+ # Parameters:
16
+ # match_expr :: Topic to subscribe
17
+ # directory_channel :: Directory channel
18
+ # answer_channel :: Where the messages for this topic get sent
19
+ # opts :: See below
20
+ #
21
+ # Available options are:
22
+ # :renew :: Renew the subscription every n seconds.
23
+ #
24
+ def initialize(match_expr, directory_channel, answer_channel, opts={})
8
25
  @directory, @answers = directory_channel, answer_channel
9
26
  @match_expr = match_expr
27
+ @identifier = Cod.uuid
28
+ @subscription = Directory::Subscription.new(
29
+ match_expr, answers, @identifier)
30
+
31
+ # Default is to renew subscriptions every 30 minutes
32
+ @renew_countdown = Directory::Countdown.new(opts[:renew] || 30*60)
33
+ renew_countdown.start
10
34
 
11
35
  subscribe
12
36
  end
@@ -14,14 +38,51 @@ module Cod
14
38
  # Subscribes this topic to the directory's messages. This gets called upon
15
39
  # initialization and must not be called again.
16
40
  #
17
- def subscribe
18
- directory.put Directory::Subscription.new(match_expr, answers)
41
+ def subscribe(status=:new)
42
+ directory.put [
43
+ :subscribe, subscription, status]
44
+
45
+ # Start counting down to next subscription renewal
46
+ renew_countdown.start
47
+ end
48
+ def renew_subscription
49
+ subscribe(:refresh)
50
+ end
51
+ def renewal_needed?
52
+ renew_countdown.elapsed?
19
53
  end
20
54
 
21
55
  # Reads the next message from the directory that matches this topic.
22
56
  #
23
- def get
24
- answers.get
57
+ def get(opts={})
58
+ # Read one message from the channel
59
+ subscription_id, message = next_message(opts)
60
+ # Answer back with a ping (so the directory knows we're still there)
61
+ directory.put [:ping, subscription_id]
62
+
63
+ return message
64
+ end
65
+ def next_message(opts)
66
+ if t=opts[:timeout]
67
+ timeout_at = Time.now + t
68
+ timeout = [t, renew_countdown.run_time].min
69
+ else
70
+ timeout_at = nil
71
+ timeout = renew_countdown.run_time
72
+ end
73
+
74
+ loop do
75
+ renew_subscription if renewal_needed?
76
+
77
+ begin
78
+ return answers.get(opts.merge(:timeout => timeout))
79
+ rescue Cod::Channel::TimeoutError
80
+ raise if timeout_at && Time.now > timeout_at
81
+ # DO NOTHING
82
+ end
83
+ end
84
+
85
+ fail "NOT REACHED"
25
86
  end
26
87
 
27
88
  # Closes all resources used by the topic.
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.3.0
4
+ version: 0.3.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,12 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-07-20 00:00:00.000000000 +02:00
13
- default_executable:
12
+ date: 2011-07-20 00:00:00.000000000Z
14
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: uuid
16
+ requirement: &70169583690420 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70169583690420
15
25
  - !ruby/object:Gem::Dependency
16
26
  name: rspec
17
- requirement: &2154614480 !ruby/object:Gem::Requirement
27
+ requirement: &70169583689920 !ruby/object:Gem::Requirement
18
28
  none: false
19
29
  requirements:
20
30
  - - ! '>='
@@ -22,10 +32,10 @@ dependencies:
22
32
  version: '0'
23
33
  type: :development
24
34
  prerelease: false
25
- version_requirements: *2154614480
35
+ version_requirements: *70169583689920
26
36
  - !ruby/object:Gem::Dependency
27
37
  name: flexmock
28
- requirement: &2154476000 !ruby/object:Gem::Requirement
38
+ requirement: &70169583689340 !ruby/object:Gem::Requirement
29
39
  none: false
30
40
  requirements:
31
41
  - - ! '>='
@@ -33,10 +43,10 @@ dependencies:
33
43
  version: '0'
34
44
  type: :development
35
45
  prerelease: false
36
- version_requirements: *2154476000
46
+ version_requirements: *70169583689340
37
47
  - !ruby/object:Gem::Dependency
38
48
  name: sdoc
39
- requirement: &2154475480 !ruby/object:Gem::Requirement
49
+ requirement: &70169583688800 !ruby/object:Gem::Requirement
40
50
  none: false
41
51
  requirements:
42
52
  - - ! '>='
@@ -44,7 +54,7 @@ dependencies:
44
54
  version: '0'
45
55
  type: :development
46
56
  prerelease: false
47
- version_requirements: *2154475480
57
+ version_requirements: *70169583688800
48
58
  description:
49
59
  email: kaspar.schiess@absurd.li
50
60
  executables: []
@@ -57,8 +67,6 @@ files:
57
67
  - LICENSE
58
68
  - Rakefile
59
69
  - README
60
- - lib/at_fork.rb
61
- - lib/cod/channel/abstract.rb
62
70
  - lib/cod/channel/base.rb
63
71
  - lib/cod/channel/beanstalk.rb
64
72
  - lib/cod/channel/pipe.rb
@@ -68,7 +76,7 @@ files:
68
76
  - lib/cod/channel.rb
69
77
  - lib/cod/client.rb
70
78
  - lib/cod/connection/beanstalk.rb
71
- - lib/cod/context.rb
79
+ - lib/cod/directory/countdown.rb
72
80
  - lib/cod/directory/subscription.rb
73
81
  - lib/cod/directory.rb
74
82
  - lib/cod/object_io.rb
@@ -83,10 +91,14 @@ files:
83
91
  - examples/master_child.rb
84
92
  - examples/ping.rb
85
93
  - examples/pong.rb
94
+ - examples/presence_client.rb
95
+ - examples/presence_server.rb
96
+ - examples/pubsub/client.rb
97
+ - examples/pubsub/directory.rb
98
+ - examples/pubsub/README
86
99
  - examples/service.rb
87
100
  - examples/service_directory.rb
88
101
  - examples/tcp.rb
89
- has_rdoc: true
90
102
  homepage: http://kschiess.github.com/cod
91
103
  licenses: []
92
104
  post_install_message:
@@ -109,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
121
  version: '0'
110
122
  requirements: []
111
123
  rubyforge_project:
112
- rubygems_version: 1.6.2
124
+ rubygems_version: 1.8.6
113
125
  signing_key:
114
126
  specification_version: 3
115
127
  summary: Really simple IPC.
data/lib/at_fork.rb DELETED
@@ -1,53 +0,0 @@
1
- # Extends the Kernel module with an at_fork method for installing at_fork
2
- # handlers.
3
- #
4
- # NOTE: at_fork handlers are executed in the thread that does the forking and
5
- # that survives the process fork.
6
- #
7
- # Usage:
8
- #
9
- # at_fork do
10
- # # Do something on fork (in the forking process)
11
- # end
12
- # at_fork(:child) { ... } # do something in the forked process
13
- # at_fork(:parent) { ... } # do something in the forking process
14
- #
15
- module Kernel
16
- # Child at_fork handlers
17
- #
18
- def self.at_fork_child
19
- @at_fork_child ||= []
20
- end
21
-
22
- # Parent at_fork handlers
23
- #
24
- def self.at_fork_parent
25
- @at_fork_parent ||= []
26
- end
27
-
28
- def at_fork(type=:parent,&block)
29
- raise ArgumentError, "Must provide a handler block." unless block
30
-
31
- handler_array = (type == :child) ?
32
- Kernel.at_fork_child : Kernel.at_fork_parent
33
-
34
- handler_array << block
35
- end
36
-
37
- def fork_with_at_fork(&block)
38
- Kernel.at_fork_parent.each(&:call)
39
-
40
- fork_without_at_fork do
41
- # From this point on, operation is single threaded.
42
-
43
- Kernel.at_fork_child.each(&:call)
44
-
45
- Kernel.at_fork_parent.replace([])
46
- Kernel.at_fork_child.replace([])
47
-
48
- block.call
49
- end
50
- end
51
- alias fork_without_at_fork fork
52
- alias fork fork_with_at_fork
53
- end
@@ -1,32 +0,0 @@
1
- module Cod
2
- # This is mostly documentation: Use it as a template for new channels.
3
- class Channel::Abstract < Channel::Base
4
- def initialize(destination)
5
- not_implemented
6
- end
7
-
8
- def initialize_copy(from)
9
- not_implemented
10
- end
11
-
12
- def put(message)
13
- not_implemented
14
- end
15
-
16
- def get(opts={})
17
- not_implemented
18
- end
19
-
20
- def waiting?
21
- not_implemented
22
- end
23
-
24
- def close
25
- not_implemented
26
- end
27
-
28
- def identifier
29
- not_implemented
30
- end
31
- end
32
- end
data/lib/cod/context.rb DELETED
@@ -1,25 +0,0 @@
1
- require 'weakref'
2
-
3
- module Cod
4
- # Context will allow to produce channels retaining some state. Until now,
5
- # this hasn't been neccessary.
6
- #
7
- class Context
8
- def pipe(name=nil)
9
- Cod::Channel::Pipe.new(name)
10
- end
11
-
12
- def beanstalk(url, name=nil)
13
- Cod::Channel::Beanstalk.new(
14
- Connection::Beanstalk.new(url), name)
15
- end
16
-
17
- def tcp(destination)
18
- Cod::Channel::TCPConnection.new(destination)
19
- end
20
-
21
- def tcpserver(bind_to)
22
- Cod::Channel::TCPServer.new(bind_to)
23
- end
24
- end
25
- end