cod 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Gemfile +1 -1
  2. data/HISTORY.txt +5 -1
  3. data/README +12 -13
  4. data/Rakefile +7 -1
  5. data/examples/{ping.rb → ping_pong/ping.rb} +1 -1
  6. data/examples/{pong.rb → ping_pong/pong.rb} +1 -1
  7. data/examples/{presence_client.rb → presence/client.rb} +0 -0
  8. data/examples/{presence_server.rb → presence/server.rb} +0 -0
  9. data/examples/queue/README +9 -0
  10. data/examples/queue/client.rb +31 -0
  11. data/examples/queue/queue.rb +51 -0
  12. data/examples/queue/send.rb +9 -0
  13. data/examples/service.rb +2 -2
  14. data/examples/tcp.rb +1 -1
  15. data/lib/cod.rb +47 -82
  16. data/lib/cod/beanstalk.rb +7 -0
  17. data/lib/cod/beanstalk/channel.rb +170 -0
  18. data/lib/cod/beanstalk/serializer.rb +80 -0
  19. data/lib/cod/beanstalk/service.rb +53 -0
  20. data/lib/cod/channel.rb +54 -12
  21. data/lib/cod/pipe.rb +188 -0
  22. data/lib/cod/select.rb +47 -0
  23. data/lib/cod/select_group.rb +87 -0
  24. data/lib/cod/service.rb +55 -42
  25. data/lib/cod/simple_serializer.rb +19 -0
  26. data/lib/cod/tcp_client.rb +202 -0
  27. data/lib/cod/tcp_server.rb +124 -0
  28. data/lib/cod/work_queue.rb +129 -0
  29. metadata +31 -45
  30. data/examples/pubsub/README +0 -12
  31. data/examples/pubsub/client.rb +0 -13
  32. data/examples/pubsub/directory.rb +0 -13
  33. data/examples/service_directory.rb +0 -32
  34. data/lib/cod/channel/base.rb +0 -185
  35. data/lib/cod/channel/beanstalk.rb +0 -69
  36. data/lib/cod/channel/pipe.rb +0 -137
  37. data/lib/cod/channel/tcp.rb +0 -16
  38. data/lib/cod/channel/tcpconnection.rb +0 -67
  39. data/lib/cod/channel/tcpserver.rb +0 -84
  40. data/lib/cod/client.rb +0 -81
  41. data/lib/cod/connection/beanstalk.rb +0 -77
  42. data/lib/cod/directory.rb +0 -98
  43. data/lib/cod/directory/countdown.rb +0 -31
  44. data/lib/cod/directory/subscription.rb +0 -59
  45. data/lib/cod/object_io.rb +0 -6
  46. data/lib/cod/objectio/connection.rb +0 -106
  47. data/lib/cod/objectio/reader.rb +0 -98
  48. data/lib/cod/objectio/serializer.rb +0 -26
  49. data/lib/cod/objectio/writer.rb +0 -27
  50. 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
- module Channel
3
- # This is raised when you try to read from a channel you've already
4
- # written to or write to a channel that you've already read from.
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
- class DirectionError < StandardError; end
25
+ def get
26
+ abstract_method_error
27
+ end
7
28
 
8
- # This is raised when a fatal communication error has occurred that
9
- # Cod cannot recover from.
29
+ # Puts one message into a channel.
10
30
  #
11
- class CommunicationError < StandardError; end
12
-
13
- # When calling #get on a channel with a timeout (see :timeout option),
14
- # this may be raised. It means that the channel isn't ready for delivering
15
- # a message in timeout seconds.
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
- class TimeoutError < StandardError; end
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