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.
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