cod 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|