cod 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/HISTORY.txt +5 -1
- data/README +12 -13
- data/Rakefile +7 -1
- data/examples/{ping.rb → ping_pong/ping.rb} +1 -1
- data/examples/{pong.rb → ping_pong/pong.rb} +1 -1
- data/examples/{presence_client.rb → presence/client.rb} +0 -0
- data/examples/{presence_server.rb → presence/server.rb} +0 -0
- data/examples/queue/README +9 -0
- data/examples/queue/client.rb +31 -0
- data/examples/queue/queue.rb +51 -0
- data/examples/queue/send.rb +9 -0
- data/examples/service.rb +2 -2
- data/examples/tcp.rb +1 -1
- data/lib/cod.rb +47 -82
- data/lib/cod/beanstalk.rb +7 -0
- data/lib/cod/beanstalk/channel.rb +170 -0
- data/lib/cod/beanstalk/serializer.rb +80 -0
- data/lib/cod/beanstalk/service.rb +53 -0
- data/lib/cod/channel.rb +54 -12
- data/lib/cod/pipe.rb +188 -0
- data/lib/cod/select.rb +47 -0
- data/lib/cod/select_group.rb +87 -0
- data/lib/cod/service.rb +55 -42
- data/lib/cod/simple_serializer.rb +19 -0
- data/lib/cod/tcp_client.rb +202 -0
- data/lib/cod/tcp_server.rb +124 -0
- data/lib/cod/work_queue.rb +129 -0
- metadata +31 -45
- data/examples/pubsub/README +0 -12
- data/examples/pubsub/client.rb +0 -13
- data/examples/pubsub/directory.rb +0 -13
- data/examples/service_directory.rb +0 -32
- data/lib/cod/channel/base.rb +0 -185
- data/lib/cod/channel/beanstalk.rb +0 -69
- data/lib/cod/channel/pipe.rb +0 -137
- data/lib/cod/channel/tcp.rb +0 -16
- data/lib/cod/channel/tcpconnection.rb +0 -67
- data/lib/cod/channel/tcpserver.rb +0 -84
- data/lib/cod/client.rb +0 -81
- data/lib/cod/connection/beanstalk.rb +0 -77
- data/lib/cod/directory.rb +0 -98
- data/lib/cod/directory/countdown.rb +0 -31
- data/lib/cod/directory/subscription.rb +0 -59
- data/lib/cod/object_io.rb +0 -6
- data/lib/cod/objectio/connection.rb +0 -106
- data/lib/cod/objectio/reader.rb +0 -98
- data/lib/cod/objectio/serializer.rb +0 -26
- data/lib/cod/objectio/writer.rb +0 -27
- data/lib/cod/topic.rb +0 -95
@@ -1,77 +0,0 @@
|
|
1
|
-
module Cod
|
2
|
-
# Wraps the lower level beanstalk connection and exposes only methods that
|
3
|
-
# we need; also makes tube handling a bit more predictable.
|
4
|
-
#
|
5
|
-
# This class is NOT thread safe.
|
6
|
-
#
|
7
|
-
class Connection::Beanstalk
|
8
|
-
# The url that was used to connect to the beanstalk server.
|
9
|
-
attr_reader :url
|
10
|
-
|
11
|
-
# Connection to the beanstalk server.
|
12
|
-
attr_reader :connection
|
13
|
-
|
14
|
-
def initialize(url)
|
15
|
-
@url = url
|
16
|
-
connect
|
17
|
-
end
|
18
|
-
|
19
|
-
def initialize_copy(from)
|
20
|
-
@url = from.url
|
21
|
-
connect
|
22
|
-
end
|
23
|
-
|
24
|
-
# Writes a raw message as a job to the tube given by name.
|
25
|
-
#
|
26
|
-
def put(name, message)
|
27
|
-
connection.use name
|
28
|
-
# TODO throws EOFError when the beanstalkd server goes away
|
29
|
-
connection.put message
|
30
|
-
end
|
31
|
-
|
32
|
-
# Returns true if there are jobs waiting in the tube given by 'name'
|
33
|
-
def waiting?(name)
|
34
|
-
connection.stats_tube(name)['current-jobs-ready'] > 0
|
35
|
-
rescue Beanstalk::NotFoundError
|
36
|
-
# Tube could not be found. No one has written to it! Nothing is waiting.
|
37
|
-
false
|
38
|
-
end
|
39
|
-
|
40
|
-
# Removes and returns the next message waiting in the tube given by name.
|
41
|
-
#
|
42
|
-
def get(name, opts={})
|
43
|
-
watch(name) do
|
44
|
-
# TODO throws EOFError when the beanstalkd queue goes away.
|
45
|
-
job = connection.reserve(opts[:timeout])
|
46
|
-
job.delete
|
47
|
-
|
48
|
-
job.body
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# Closes the connection
|
53
|
-
#
|
54
|
-
def close
|
55
|
-
connection.close if connection
|
56
|
-
@connection = nil
|
57
|
-
end
|
58
|
-
|
59
|
-
# Creates a connection
|
60
|
-
#
|
61
|
-
def connect
|
62
|
-
# TODO throws Errno::ECONNREFUSED if the beanstalkd doesn't answer
|
63
|
-
@connection = Beanstalk::Connection.new(url)
|
64
|
-
@watching = nil
|
65
|
-
end
|
66
|
-
private
|
67
|
-
def watch(name)
|
68
|
-
unless @watching == name
|
69
|
-
connection.ignore(@watching)
|
70
|
-
connection.watch(name)
|
71
|
-
@watching = name
|
72
|
-
end
|
73
|
-
|
74
|
-
yield
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
data/lib/cod/directory.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
module Cod
|
2
|
-
# A directory where one can publish messages given a topic string.
|
3
|
-
#
|
4
|
-
# The channel given will be where people should subscribe.
|
5
|
-
#
|
6
|
-
class Directory
|
7
|
-
# The channel the directory receives subscription messages on.
|
8
|
-
#
|
9
|
-
attr_reader :channel
|
10
|
-
|
11
|
-
# Subscriptions this directory handles.
|
12
|
-
#
|
13
|
-
attr_reader :subscriptions
|
14
|
-
|
15
|
-
def initialize(channel)
|
16
|
-
@channel = channel
|
17
|
-
@subscriptions = Set.new
|
18
|
-
end
|
19
|
-
|
20
|
-
# Sends the message to all subscribers that listen to this topic. Returns
|
21
|
-
# the number of subscribers this message has been sent to.
|
22
|
-
#
|
23
|
-
def publish(topic, message)
|
24
|
-
process_control_messages
|
25
|
-
|
26
|
-
n = 0
|
27
|
-
failed_subscriptions = []
|
28
|
-
for subscription in @subscriptions
|
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
|
39
|
-
end
|
40
|
-
|
41
|
-
process_control_messages
|
42
|
-
|
43
|
-
remove_subscriptions { |sub| failed_subscriptions.include?(sub) }
|
44
|
-
return n
|
45
|
-
end
|
46
|
-
|
47
|
-
# Closes all resources used by the directory.
|
48
|
-
#
|
49
|
-
def close
|
50
|
-
channel.close
|
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
|
62
|
-
|
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
|
68
|
-
while channel.waiting?
|
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
|
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.
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
def remove_subscriptions(&block)
|
93
|
-
@subscriptions.delete_if(&block)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
require 'cod/directory/countdown'
|
@@ -1,31 +0,0 @@
|
|
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,59 +0,0 @@
|
|
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.
|
7
|
-
#
|
8
|
-
class Subscription
|
9
|
-
attr_reader :matcher
|
10
|
-
attr_reader :channel
|
11
|
-
attr_reader :countdown
|
12
|
-
|
13
|
-
def initialize(matcher, channel, topic_id)
|
14
|
-
@matcher = matcher
|
15
|
-
@channel = channel
|
16
|
-
@countdown = Countdown.new
|
17
|
-
@identifier = topic_id
|
18
|
-
end
|
19
|
-
|
20
|
-
def ===(other)
|
21
|
-
matcher === other
|
22
|
-
end
|
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
|
-
|
36
|
-
def 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)
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
data/lib/cod/object_io.rb
DELETED
@@ -1,106 +0,0 @@
|
|
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
|
data/lib/cod/objectio/reader.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
module Cod::ObjectIO
|
2
|
-
# Reads objects from one or more IO streams.
|
3
|
-
#
|
4
|
-
class Reader
|
5
|
-
attr_reader :waiting_messages
|
6
|
-
attr_reader :pool
|
7
|
-
|
8
|
-
# Initializes an object reader that reads from one or several IO objects.
|
9
|
-
#
|
10
|
-
# Example:
|
11
|
-
# connection = Connection::Pool.new { make_new_connection }
|
12
|
-
# reader = Reader.new(serializer, connection)
|
13
|
-
#
|
14
|
-
def initialize(serializer, conn_pool)
|
15
|
-
@serializer = serializer
|
16
|
-
@waiting_messages = []
|
17
|
-
@pool = conn_pool
|
18
|
-
end
|
19
|
-
|
20
|
-
def get(opts={})
|
21
|
-
return waiting_messages.shift if queued?
|
22
|
-
|
23
|
-
start_time = Time.now
|
24
|
-
loop do
|
25
|
-
read_from_wire opts
|
26
|
-
|
27
|
-
# Early return in case we have a message waiting
|
28
|
-
return waiting_messages.shift if queued?
|
29
|
-
|
30
|
-
if opts[:timeout] && (Time.now-start_time) > opts[:timeout]
|
31
|
-
raise Cod::Channel::TimeoutError,
|
32
|
-
"No messages waiting in pipe."
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
fail "NOTREACHED"
|
37
|
-
end
|
38
|
-
|
39
|
-
def waiting?
|
40
|
-
read_from_wire
|
41
|
-
queued?
|
42
|
-
rescue Cod::Channel::CommunicationError
|
43
|
-
queued?
|
44
|
-
end
|
45
|
-
|
46
|
-
def queued?
|
47
|
-
! waiting_messages.empty?
|
48
|
-
end
|
49
|
-
|
50
|
-
def close
|
51
|
-
@pool.close
|
52
|
-
end
|
53
|
-
|
54
|
-
private
|
55
|
-
|
56
|
-
# Checks if data is waiting and processes messages.
|
57
|
-
#
|
58
|
-
def read_from_wire(opts={})
|
59
|
-
# Establish new connections and register them
|
60
|
-
@pool.accept
|
61
|
-
|
62
|
-
# Process all waiting data
|
63
|
-
process_nonblock(@pool.connections)
|
64
|
-
end
|
65
|
-
|
66
|
-
# Reads all data waiting in each io in the ios array.
|
67
|
-
#
|
68
|
-
def process_nonblock(ios)
|
69
|
-
ios.each do |io|
|
70
|
-
process_nonblock_single(io)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
# Reads all data waiting in a single io.
|
75
|
-
#
|
76
|
-
def process_nonblock_single(io)
|
77
|
-
buffer = io.read_nonblock(1024*1024*1024)
|
78
|
-
|
79
|
-
sio = StringIO.new(buffer)
|
80
|
-
while not sio.eof?
|
81
|
-
waiting_messages << deserialize(io, sio)
|
82
|
-
end
|
83
|
-
rescue Errno::EAGAIN
|
84
|
-
# read failed because there was no data. This is expected.
|
85
|
-
return
|
86
|
-
rescue EOFError
|
87
|
-
@pool.report_failed(io)
|
88
|
-
end
|
89
|
-
|
90
|
-
# Deserializes a message (in message format, string) into the object that
|
91
|
-
# was transmitted. Overwrite this message if you want to control the
|
92
|
-
# message format.
|
93
|
-
#
|
94
|
-
def deserialize(*args)
|
95
|
-
@serializer.deserialize(*args)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|