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
@@ -0,0 +1,80 @@
|
|
1
|
+
module Cod::Beanstalk
|
2
|
+
# This is a kind of beanstalk message middleware: It generates and parses
|
3
|
+
# beanstalk messages from a ruby format into raw bytes. The raw bytes go
|
4
|
+
# directly into the tcp channel that underlies the beanstalk channel.
|
5
|
+
#
|
6
|
+
# Messages are represented as simple Ruby arrays, specifying first the
|
7
|
+
# beanstalkd command, then arguments. Examples:
|
8
|
+
# [:use, 'a_tube']
|
9
|
+
# [:delete, 123]
|
10
|
+
#
|
11
|
+
# One exception: The commands that have a body attached will be described
|
12
|
+
# like so in protocol.txt:
|
13
|
+
# put <pri> <delay> <ttr> <bytes>\r\n
|
14
|
+
# <data>\r\n
|
15
|
+
#
|
16
|
+
# To generate this message, just put the data where the bytes would be and
|
17
|
+
# the serializer will do the right thing.
|
18
|
+
# [:put, pri, delay, ttr, "my_small_data"]
|
19
|
+
#
|
20
|
+
# Results come back in the same way, except that the answers take the place
|
21
|
+
# of the commands. Answers are always in upper case.
|
22
|
+
#
|
23
|
+
# Also see https://raw.github.com/kr/beanstalkd/master/doc/protocol.txt.
|
24
|
+
#
|
25
|
+
class Serializer
|
26
|
+
def en(msg)
|
27
|
+
cmd = msg.first
|
28
|
+
|
29
|
+
if cmd == :put
|
30
|
+
body = msg.last
|
31
|
+
format(*msg[0..-2], body.bytesize) << format(body)
|
32
|
+
else
|
33
|
+
format(*msg)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def de(io)
|
38
|
+
str = io.gets("\r\n")
|
39
|
+
raw = str.split
|
40
|
+
|
41
|
+
cmd = convert_cmd(raw.first)
|
42
|
+
msg = [cmd, *convert_args(raw[1..-1])]
|
43
|
+
|
44
|
+
if [:ok, :reserved].include?(cmd)
|
45
|
+
# More data to read:
|
46
|
+
size = msg.last
|
47
|
+
data = io.read(size+2)
|
48
|
+
|
49
|
+
fail "No crlf at end of data?" unless data[-2..-1] == "\r\n"
|
50
|
+
msg[-1] = data[0..-3]
|
51
|
+
end
|
52
|
+
|
53
|
+
msg
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
# Joins the arguments with a space and appends a \r\n
|
58
|
+
#
|
59
|
+
def format(*args)
|
60
|
+
args.join(' ') << "\r\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
# Converts a beanstalkd answer like INSERTED to :inserted
|
64
|
+
#
|
65
|
+
def convert_cmd(cmd)
|
66
|
+
cmd.downcase.to_sym
|
67
|
+
end
|
68
|
+
|
69
|
+
# Converts an argument to either a number or a string, depending on
|
70
|
+
# what it looks like.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
# convert_args(['1', 'a string']) # => [1, 'a string']
|
74
|
+
#
|
75
|
+
def convert_args(args)
|
76
|
+
args.map { |e|
|
77
|
+
/^\d+$/.match(e) ? Integer(e) : e }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Cod::Beanstalk
|
2
|
+
class Service < Cod::Service
|
3
|
+
def one(&block)
|
4
|
+
@channel.try_get { |(rq, answer_chan), control|
|
5
|
+
result = if block.arity == 2
|
6
|
+
block.call(rq, Control.new(control))
|
7
|
+
else
|
8
|
+
block.call(rq)
|
9
|
+
end
|
10
|
+
|
11
|
+
unless control.command_given?
|
12
|
+
# The only way to respond to the caller is by exiting the block
|
13
|
+
# without giving metacommands.
|
14
|
+
answer_chan.put result if answer_chan
|
15
|
+
end
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
class Control
|
20
|
+
def initialize(channel_control)
|
21
|
+
@channel_control = channel_control
|
22
|
+
end
|
23
|
+
|
24
|
+
def retry_in(seconds)
|
25
|
+
fail ArgumentError,
|
26
|
+
"#retry_in accepts only an integer number of seconds." \
|
27
|
+
unless seconds.floor == seconds
|
28
|
+
|
29
|
+
@channel_control.release_with_delay(seconds)
|
30
|
+
end
|
31
|
+
def retry
|
32
|
+
@channel_control.release
|
33
|
+
end
|
34
|
+
def bury
|
35
|
+
@channel_control.bury
|
36
|
+
end
|
37
|
+
def delete
|
38
|
+
@channel_control.delete
|
39
|
+
end
|
40
|
+
|
41
|
+
def command_issued?
|
42
|
+
@channel_control.command_given?
|
43
|
+
end
|
44
|
+
|
45
|
+
def msg_id
|
46
|
+
@channel_control.msg_id
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Client < Cod::Service::Client
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/cod/channel.rb
CHANGED
@@ -1,19 +1,61 @@
|
|
1
1
|
module Cod
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
# Channels transport ruby objects from one end to the other. The
|
3
|
+
# communication setup varies a bit depending on the transport used for the
|
4
|
+
# channel, but the interface you interact with doesn't vary. You can #put
|
5
|
+
# messages into a channel and you then #get them out of it.
|
6
|
+
#
|
7
|
+
# Synopsis:
|
8
|
+
# channel.put [:a, :ruby, :object]
|
9
|
+
# channel.get # => [:a, :ruby, :object]
|
10
|
+
#
|
11
|
+
# By default, channels will serialize the messages you give them using
|
12
|
+
# 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
|
+
# description of the interface such a serializer needs to implement.
|
15
|
+
#
|
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.
|
19
|
+
#
|
20
|
+
class Channel
|
21
|
+
# Obtains one message from the channel. If the channel is empty, but
|
22
|
+
# theoretically able to receive more messages, blocks forever. But if the
|
23
|
+
# channel is somehow broken, an exception is raised.
|
5
24
|
#
|
6
|
-
|
25
|
+
def get
|
26
|
+
abstract_method_error
|
27
|
+
end
|
7
28
|
|
8
|
-
#
|
9
|
-
# Cod cannot recover from.
|
29
|
+
# Puts one message into a channel.
|
10
30
|
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# a
|
31
|
+
def put(msg)
|
32
|
+
abstract_method_error
|
33
|
+
end
|
34
|
+
|
35
|
+
# Interact with a channel by first writing msg to it, then reading back
|
36
|
+
# the other ends answer.
|
37
|
+
#
|
38
|
+
def interact(msg)
|
39
|
+
put msg
|
40
|
+
get
|
41
|
+
end
|
42
|
+
|
43
|
+
# Produces a service that has this channel as communication point.
|
16
44
|
#
|
17
|
-
|
45
|
+
def service
|
46
|
+
abstract_method_error
|
47
|
+
end
|
48
|
+
|
49
|
+
# Produces a service client that connects to this channel and receives
|
50
|
+
# service answers to the channel indicated by answers_to.
|
51
|
+
#
|
52
|
+
def client(answers_to)
|
53
|
+
abstract_method_error
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
def abstract_method_error
|
58
|
+
fail "Abstract method called"
|
59
|
+
end
|
18
60
|
end
|
19
61
|
end
|
data/lib/cod/pipe.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
|
2
|
+
module Cod
|
3
|
+
# A cod channel based on IO.pipe.
|
4
|
+
#
|
5
|
+
# NOTE: If you embed Cod::Pipe channels into your messages, Cod will insert
|
6
|
+
# the object id of that channel into the byte stream that is transmitted. On
|
7
|
+
# receiving such an object id (a machine pointer), Cod will try to
|
8
|
+
# reconstruct the channel that was at the origin of the id. This can
|
9
|
+
# obviously only work if you have such an object in your address space.
|
10
|
+
# There are multiple ways to construct such a situation. Say you want to
|
11
|
+
# send a pipe channel to one of your (forked) childs: This will work if you
|
12
|
+
# create the channel before forking the child, since master and child will
|
13
|
+
# share all objects that were available before the fork.
|
14
|
+
#
|
15
|
+
class Pipe
|
16
|
+
attr_reader :pipe
|
17
|
+
attr_reader :serializer
|
18
|
+
|
19
|
+
IOPair = Struct.new(:r, :w) do
|
20
|
+
# Performs a deep copy of the structure.
|
21
|
+
def initialize_copy(other)
|
22
|
+
super
|
23
|
+
self.r = other.r.dup if other.r
|
24
|
+
self.w = other.w.dup if other.w
|
25
|
+
end
|
26
|
+
def write(buf)
|
27
|
+
close_r
|
28
|
+
raise Cod::ReadOnlyChannel unless w
|
29
|
+
w.write(buf)
|
30
|
+
end
|
31
|
+
def close
|
32
|
+
close_r
|
33
|
+
close_w
|
34
|
+
end
|
35
|
+
def close_r
|
36
|
+
r.close if r
|
37
|
+
self.r = nil
|
38
|
+
end
|
39
|
+
def close_w
|
40
|
+
w.close if w
|
41
|
+
self.w = nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# A few methods that a pipe split must answer to. The split itself is
|
46
|
+
# basically an array instance; these methods add some calling safety and
|
47
|
+
# convenience.
|
48
|
+
#
|
49
|
+
module SplitMethods # :nodoc:
|
50
|
+
def read; first; end
|
51
|
+
def write; last; end
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(serializer=nil)
|
55
|
+
super
|
56
|
+
@serializer = serializer || SimpleSerializer.new
|
57
|
+
@pipe = IOPair.new(*IO.pipe)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a copy of this pipe channel. This performs a shallow #dup except
|
61
|
+
# for the file descriptors stored in the pipe, so that a #close affects
|
62
|
+
# only one copy.
|
63
|
+
#
|
64
|
+
def initialize_copy(other)
|
65
|
+
super
|
66
|
+
@serializer = other.serializer
|
67
|
+
@pipe = other.pipe.dup
|
68
|
+
end
|
69
|
+
|
70
|
+
# Makes this pipe readonly. Calls to #put will error out. This closes the
|
71
|
+
# write end permanently and provokes end of file on the read end once all
|
72
|
+
# processes that posses a link to the write end do so.
|
73
|
+
#
|
74
|
+
# Returns self so that you can write for example:
|
75
|
+
# read_end = pipe.dup.readonly
|
76
|
+
#
|
77
|
+
def readonly
|
78
|
+
pipe.close_w
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Makes this pipe writeonly. Calls to #get will error out. See #readonly.
|
83
|
+
#
|
84
|
+
# Returns self so that you can write for example:
|
85
|
+
# write_end = pipe.dup.writeonly
|
86
|
+
#
|
87
|
+
def writeonly
|
88
|
+
pipe.close_r
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Actively splits this pipe into two ends, a read end and a write end. The
|
93
|
+
# original pipe is closed, leaving only the two ends to work with. The
|
94
|
+
# read end can only be read from (#get) and the write end can only be
|
95
|
+
# written to (#put).
|
96
|
+
#
|
97
|
+
def split
|
98
|
+
[self.dup.readonly, self.dup.writeonly].tap { |split|
|
99
|
+
self.close
|
100
|
+
|
101
|
+
split.extend(SplitMethods)
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Using #put on a pipe instance will close the other pipe end. Subsequent
|
106
|
+
# #get will raise a Cod::InvalidOperation.
|
107
|
+
#
|
108
|
+
# Example:
|
109
|
+
# pipe.put [:a, :message]
|
110
|
+
#
|
111
|
+
def put(obj)
|
112
|
+
raise Cod::ReadOnlyChannel unless can_write?
|
113
|
+
|
114
|
+
pipe.write(
|
115
|
+
serializer.en(obj))
|
116
|
+
end
|
117
|
+
|
118
|
+
# Using #get on a pipe instance will close the other pipe end. Subsequent
|
119
|
+
# #put will receive a Cod::InvalidOperation.
|
120
|
+
#
|
121
|
+
# Example:
|
122
|
+
# pipe.get # => obj
|
123
|
+
#
|
124
|
+
def get(opts={})
|
125
|
+
raise Cod::WriteOnlyChannel unless can_read?
|
126
|
+
pipe.close_w
|
127
|
+
|
128
|
+
loop do
|
129
|
+
ready = Cod.select(nil, self)
|
130
|
+
return deserialize_one if ready
|
131
|
+
end
|
132
|
+
rescue EOFError
|
133
|
+
fail "All pipe ends seem to be closed. Reading from this pipe will not "+
|
134
|
+
"return any data."
|
135
|
+
end
|
136
|
+
|
137
|
+
# Closes the pipe completely. All active ends are closed. Note that you
|
138
|
+
# can call this function on a closed pipe without getting an error raised.
|
139
|
+
#
|
140
|
+
def close
|
141
|
+
pipe.close
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns if this pipe is ready for reading.
|
145
|
+
#
|
146
|
+
def select(timeout=nil)
|
147
|
+
result = Cod.select(timeout, self)
|
148
|
+
not result.nil?
|
149
|
+
end
|
150
|
+
def to_read_fds
|
151
|
+
pipe.r
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns true if you can read from this pipe.
|
155
|
+
#
|
156
|
+
def can_read?
|
157
|
+
not pipe.r.nil?
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns true if you can write to this pipe.
|
161
|
+
#
|
162
|
+
def can_write?
|
163
|
+
not pipe.w.nil?
|
164
|
+
end
|
165
|
+
|
166
|
+
# --------------------------------------------------------- service/client
|
167
|
+
|
168
|
+
def service
|
169
|
+
Service.new(self)
|
170
|
+
end
|
171
|
+
def client(answers_to)
|
172
|
+
Service::Client.new(self, answers_to)
|
173
|
+
end
|
174
|
+
|
175
|
+
# ---------------------------------------------------------- serialization
|
176
|
+
def _dump(depth) # :nodoc:
|
177
|
+
object_id.to_s
|
178
|
+
end
|
179
|
+
def self._load(string) # :nodoc:
|
180
|
+
ObjectSpace._id2ref(Integer(string))
|
181
|
+
end
|
182
|
+
private
|
183
|
+
def deserialize_one
|
184
|
+
# Now deserialize one message from the buffer in io
|
185
|
+
serializer.de(pipe.r)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
data/lib/cod/select.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
module Cod
|
2
|
+
def select(timeout, groups)
|
3
|
+
Select.new(timeout, groups).do
|
4
|
+
end
|
5
|
+
module_function :select
|
6
|
+
|
7
|
+
# Performs an IO.select on a list of file descriptors and Cod channels.
|
8
|
+
# Construct this like so:
|
9
|
+
# Select.new(
|
10
|
+
# 0.1, # timeout
|
11
|
+
# foo: single_fd, # a single named FD
|
12
|
+
# bar: [one, two, three], # a group of FDs.
|
13
|
+
# )
|
14
|
+
#
|
15
|
+
class Select
|
16
|
+
attr_reader :timeout
|
17
|
+
attr_reader :groups
|
18
|
+
|
19
|
+
def initialize(timeout, groups)
|
20
|
+
@timeout = timeout
|
21
|
+
@groups = SelectGroup.new(groups)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Performs the IO.select and returns a thinned out version of that initial
|
25
|
+
# groups, containing only FDs and channels that are ready for reading.
|
26
|
+
#
|
27
|
+
def do
|
28
|
+
fds = groups.values { |e| to_read_fd(e) }
|
29
|
+
|
30
|
+
# Perform select
|
31
|
+
r,w,e = IO.select(fds, nil, nil, timeout)
|
32
|
+
|
33
|
+
# Nothing is ready if r is nil
|
34
|
+
return groups.empty unless r
|
35
|
+
|
36
|
+
# Prepare a return value: The original hash, where the fds are ready.
|
37
|
+
groups.
|
38
|
+
keep_if { |e| r.include?(to_read_fd(e)) }.
|
39
|
+
unpack
|
40
|
+
end
|
41
|
+
private
|
42
|
+
def to_read_fd(single)
|
43
|
+
return single.to_read_fds if single.respond_to?(:to_read_fds)
|
44
|
+
return single
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Cod
|
2
|
+
# A select group is a special kind of hash, basically. It contains group
|
3
|
+
# names as keys (probably symbols) and has either array values or single
|
4
|
+
# object instances.
|
5
|
+
#
|
6
|
+
# A number of operations is defined to make it easier to filter such
|
7
|
+
# hashes during IO.select. The API user only ever gets to see the resulting
|
8
|
+
# hash.
|
9
|
+
#
|
10
|
+
class SelectGroup # :nodoc:
|
11
|
+
def initialize(hash_or_value)
|
12
|
+
if hash_or_value.respond_to?(:each)
|
13
|
+
@h = hash_or_value
|
14
|
+
@unpack = false
|
15
|
+
else
|
16
|
+
@h = {box: hash_or_value}
|
17
|
+
@unpack = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns all values as a single flat array. NOT like Hash#values.
|
22
|
+
#
|
23
|
+
def values(&block)
|
24
|
+
values = []
|
25
|
+
block ||= lambda { |e| e } # identity
|
26
|
+
|
27
|
+
@h.each do |_,v|
|
28
|
+
if v.respond_to?(:to_ary)
|
29
|
+
values << v.map(&block)
|
30
|
+
else
|
31
|
+
values << block.call(v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
values.flatten
|
35
|
+
end
|
36
|
+
|
37
|
+
# Keeps values around with their respective keys if block returns true
|
38
|
+
# for the values. Deletes everything else. NOT like Hash#keep_if.
|
39
|
+
#
|
40
|
+
def keep_if(&block)
|
41
|
+
old_hash = @h
|
42
|
+
@h = Hash.new
|
43
|
+
old_hash.each do |key, values|
|
44
|
+
# Now values is either an Array like structure that we iterate
|
45
|
+
# on or it is a single value.
|
46
|
+
if values.respond_to?(:to_ary)
|
47
|
+
ary = values.select { |e| block.call(e) }
|
48
|
+
@h[key] = ary unless ary.empty?
|
49
|
+
else
|
50
|
+
value = values
|
51
|
+
@h[key] = value if block.call(value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# EXACTLY like Hash#keys.
|
59
|
+
def keys
|
60
|
+
@h.keys
|
61
|
+
end
|
62
|
+
|
63
|
+
# Converts this to a result value. If this instance was constructed with a
|
64
|
+
# simple ruby object, return the object. Otherwise return the resulting
|
65
|
+
# hash.
|
66
|
+
#
|
67
|
+
def unpack
|
68
|
+
if @unpack
|
69
|
+
@h[:box]
|
70
|
+
else
|
71
|
+
@h
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Returns something that will represent the empty result to our client.
|
76
|
+
# If this class was constructed with just a single object, the empty
|
77
|
+
# result is nil. Otherwise the empty result is an empty hash.
|
78
|
+
#
|
79
|
+
def empty
|
80
|
+
if @unpack
|
81
|
+
nil
|
82
|
+
else
|
83
|
+
{}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|