cod 0.2.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 ADDED
@@ -0,0 +1,12 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'beanstalk-client'
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ gem 'flexmock'
8
+
9
+ gem 'guard'
10
+ gem 'growl'
11
+ gem 'rb-fsevent'
12
+ end
@@ -0,0 +1,11 @@
1
+
2
+ == 0.2 / 2011-04-27
3
+
4
+ * Cod::Client now allows both synchronous (with answer) and asynchronous
5
+ calls (#call vs. #notify).
6
+
7
+ == 0.1 / 2011-04-20
8
+
9
+ * Basic communication via Beanstalk and IO.pipe.
10
+ * Client-Service communication
11
+ * Publish-Subscribe communication
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ Copyright (c) 2011 Kaspar Schiess
3
+
4
+ Permission is hereby granted, free of charge, to any person
5
+ obtaining a copy of this software and associated documentation
6
+ files (the "Software"), to deal in the Software without
7
+ restriction, including without limitation the rights to use,
8
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following
11
+ conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
18
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
20
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
23
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,33 @@
1
+
2
+ cod is a simple ipc abstraction layer. It allows you to focus on interaction
3
+ between processes instead of having to think about interaction with the OS.
4
+
5
+ SYNOPSIS
6
+
7
+ # Cod's basic elements are channels, unidirectional communication links.
8
+ pipe = Cod.pipe
9
+ beanstalk = Cod.beanstalk('localhost:11300')
10
+
11
+ # You can use those either directly:
12
+ pipe.put :some_ruby_object # Process A
13
+ pipe.get # => :some_ruby_object # Process B
14
+
15
+ # Or use them as bricks for more:
16
+ service = Cod::Service.new(beanstalk)
17
+ client = Cod::Client.new(beanstalk, pipe)
18
+
19
+ service.one { |msg| :response } # Process A
20
+ client.call :ruby_object # => :response # Process B
21
+
22
+ # And more: Publish/Subscribe, easy construction of more advanced distributed
23
+ # communication.
24
+
25
+ STATUS
26
+
27
+ Becoming more useful by the day. Most things will work nicely already,
28
+ although error handling is not production quality. Toy around with it now
29
+ and give me feedback!
30
+
31
+ At version 0.1 (unreleased)
32
+
33
+ (c) 2011 Kaspar Schiess
@@ -0,0 +1,35 @@
1
+ require "rubygems"
2
+ require "rake/rdoctask"
3
+ require 'rspec/core/rake_task'
4
+ require "rake/gempackagetask"
5
+
6
+ desc "Run all tests: Exhaustive."
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default => :spec
10
+
11
+ require 'sdoc'
12
+
13
+ # Generate documentation
14
+ Rake::RDocTask.new do |rdoc|
15
+ rdoc.title = "parslet - construction of parsers made easy"
16
+ rdoc.options << '--line-numbers'
17
+ rdoc.options << '--fmt' << 'shtml' # explictly set shtml generator
18
+ rdoc.template = 'direct' # lighter template used on railsapi.com
19
+ rdoc.main = "README"
20
+ rdoc.rdoc_files.include("README", "lib/**/*.rb")
21
+ rdoc.rdoc_dir = "rdoc"
22
+ end
23
+
24
+ desc 'Clear out RDoc'
25
+ task :clean => [:clobber_rdoc, :clobber_package]
26
+
27
+ # This task actually builds the gem.
28
+ task :gem => :spec
29
+ spec = eval(File.read('cod.gemspec'))
30
+
31
+ desc "Generate the gem package."
32
+ Rake::GemPackageTask.new(spec) do |pkg|
33
+ pkg.gem_spec = spec
34
+ end
35
+
@@ -0,0 +1,16 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'cod'
3
+
4
+ pipe = Cod.pipe
5
+
6
+ child_pid = fork do
7
+ pipe.put 'test'
8
+ pipe.put Process.pid
9
+ end
10
+
11
+ begin
12
+ p pipe.get
13
+ p pipe.get
14
+ ensure
15
+ Process.wait(child_pid)
16
+ end
@@ -0,0 +1,9 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'cod'
3
+
4
+ pipe = Cod::Channel::Beanstalk.new('localhost:11300', 'pingpong')
5
+
6
+ loop do
7
+ pipe.put Time.now
8
+ sleep 1
9
+ end
@@ -0,0 +1,9 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'cod'
3
+
4
+ pipe = Cod::Channel::Beanstalk.new('localhost:11300', "pingpong")
5
+
6
+ loop do
7
+ puts "Received: "+pipe.get.inspect
8
+ end
9
+
@@ -0,0 +1,26 @@
1
+ # This example spawns a small worker process that will provide a simple
2
+ # service to its parent process. This is just one way of structuring this; the
3
+ # important part here is the client/service code.
4
+
5
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
6
+ require 'cod'
7
+
8
+ service_channel = Cod.pipe
9
+ answer_channel = Cod.pipe
10
+
11
+ child_pid = fork do
12
+ service = Cod::Service.new(service_channel)
13
+ service.one { |call|
14
+ puts "Service got called with #{call.inspect}"
15
+ time = Time.now
16
+ puts "Answering with current time: #{time}"
17
+ time }
18
+ end
19
+
20
+ client = Cod::Client.new(service_channel, answer_channel)
21
+ puts "Calling service..."
22
+ answer = client.call('42')
23
+
24
+ puts "Service answered with #{answer}."
25
+
26
+ Process.wait(child_pid)
@@ -0,0 +1,32 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__) + "/../lib")
2
+ require 'cod'
3
+
4
+ announce = Cod.pipe
5
+ directory = Cod::Directory.new(announce)
6
+
7
+ pipes = []
8
+ pids = ['foo.bar', /^foo\..+/].map { |match_expr|
9
+ # Creates a communication channel that both the parent and the child know
10
+ # about. After the fork, they will own unique ends to that channel.
11
+ pipes << pipe = Cod.pipe # store in pipes as well to prevent GC
12
+
13
+ # Create a child that will receive messages that match match_expr.
14
+ fork do
15
+ puts "Spawned child: #{Process.pid}"
16
+ topic = Cod::Topic.new(match_expr, announce, pipe)
17
+
18
+ sleep 0.1
19
+ loop do
20
+ message = topic.get
21
+ puts "#{Process.pid}: received #{message.inspect}."
22
+
23
+ break if message == :shutdown
24
+ end
25
+ end }
26
+
27
+ directory.publish 'foo.bar', 'Hi everyone!' # to both childs
28
+ directory.publish 'foo.baz', 'Hi you!' # only second child matches this
29
+ directory.publish 'no.one', 'echo?' # no one matches this
30
+
31
+ directory.publish 'foo.bar', :shutdown # shutdown children in orderly fashion
32
+ Process.waitall
@@ -0,0 +1,29 @@
1
+ # Extends the Kernel module with an at_fork method for installing at_fork
2
+ # handlers.
3
+
4
+ module Kernel
5
+ class << self
6
+ def at_fork_handler
7
+ @at_fork_handler ||= proc {}
8
+ end
9
+ def at_fork_handler=(handler)
10
+ @at_fork_handler = handler
11
+ end
12
+ end
13
+
14
+ def at_fork(&block)
15
+ old_handler = Kernel.at_fork_handler
16
+ Kernel.at_fork_handler = lambda { block.call(old_handler) }
17
+ end
18
+
19
+ def fork_with_at_fork(&block)
20
+ Kernel.at_fork_handler.call()
21
+
22
+ fork_without_at_fork do
23
+ Kernel.at_fork_handler = nil
24
+ block.call
25
+ end
26
+ end
27
+ alias fork_without_at_fork fork
28
+ alias fork fork_with_at_fork
29
+ end
@@ -0,0 +1,49 @@
1
+ require 'at_fork'
2
+
3
+ module Cod
4
+ # This gets raised in #create_reference when the identifier passed in is
5
+ # either invalid (has never existed) or when it cannot be turned into an
6
+ # object instance. (Because it might have been garbage collected or other
7
+ # such reasons)
8
+ #
9
+ class InvalidIdentifier < StandardError; end
10
+
11
+ def beanstalk(url, name=nil)
12
+ context.beanstalk(url, name)
13
+ end
14
+ module_function :beanstalk
15
+
16
+ def pipe(name=nil)
17
+ context.pipe(name)
18
+ end
19
+ module_function :pipe
20
+
21
+ def context
22
+ @convenience_context ||= Context.new
23
+ end
24
+ module_function :context
25
+
26
+ # For testing mainly
27
+ #
28
+ def reset
29
+ @convenience_context = nil
30
+ end
31
+ module_function :reset
32
+ end
33
+
34
+ module Cod::Connection; end
35
+ require 'cod/connection/beanstalk'
36
+
37
+ require 'cod/channel'
38
+ require 'cod/channel/base'
39
+ require 'cod/channel/pipe'
40
+ require 'cod/channel/beanstalk'
41
+
42
+ require 'cod/context'
43
+ require 'cod/client'
44
+
45
+ require 'cod/service'
46
+
47
+ require 'cod/directory'
48
+ require 'cod/directory/subscription'
49
+ require 'cod/topic'
@@ -0,0 +1,19 @@
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.
5
+ #
6
+ class DirectionError < StandardError; end
7
+
8
+ # This is raised when a fatal communication error has occurred that
9
+ # Cod cannot recover from.
10
+ #
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.
16
+ #
17
+ class TimeoutError < StandardError; end
18
+ end
19
+ end
@@ -0,0 +1,99 @@
1
+
2
+ module Cod
3
+ # TODO document write/read semantics
4
+ # TODO document dup behaviour
5
+ # TODO document object serialisation
6
+ class Channel::Base
7
+ # Writes a Ruby object (the 'message') to the channel. This object will
8
+ # be queued in the channel and become available for #get in a FIFO manner.
9
+ #
10
+ # Issuing a #put also closes the channel instance for subsequent #get's.
11
+ #
12
+ # Example:
13
+ # chan.put 'test'
14
+ # chan.put true
15
+ # chan.put :symbol
16
+ #
17
+ def put(message)
18
+ not_implemented
19
+ end
20
+
21
+ # Reads a Ruby object (a message) from the channel. Some channels may not
22
+ # allow reading after you've written to it once. Options that work:
23
+ #
24
+ # :timeout :: Time to wait before throwing a Cod::Channel::TimeoutError.
25
+ #
26
+ def get(opts={})
27
+ not_implemented
28
+ end
29
+
30
+ # Returns true if there are messages waiting in the channel.
31
+ #
32
+ def waiting?
33
+ not_implemented
34
+ end
35
+
36
+ def close
37
+ not_implemented
38
+ end
39
+
40
+ def identifier
41
+ not_implemented
42
+ end
43
+
44
+ # Returns the Identifier class below the current channel class. This is
45
+ # a helper function that should only be used by subclasses.
46
+ #
47
+ def identifier_class
48
+ self.class.const_get(:Identifier)
49
+ end
50
+
51
+ def marshal_dump
52
+ identifier
53
+ end
54
+
55
+ def marshal_load(identifier)
56
+ temp = identifier.resolve
57
+ initialize_copy(temp)
58
+ end
59
+
60
+ private
61
+ def serialize(message)
62
+ Marshal.dump(message)
63
+ end
64
+
65
+ def deserialize(buffer)
66
+ Marshal.load(buffer)
67
+ end
68
+
69
+ # Turns the object into a buffer (simple transport layer that prefixes a
70
+ # size)
71
+ #
72
+ def transport_pack(message)
73
+ serialized = serialize(message)
74
+ buffer = [serialized.size].pack('l') + serialized
75
+ end
76
+
77
+ # Slices one message from the front of buffer
78
+ #
79
+ def transport_unpack(buffer)
80
+ size = buffer.slice!(0...4).unpack('l').first
81
+ serialized = buffer.slice!(0...size)
82
+ deserialize(serialized)
83
+ end
84
+
85
+ def direction_error(msg)
86
+ raise Cod::Channel::DirectionError, msg
87
+ end
88
+
89
+ def communication_error(msg)
90
+ raise Cod::Channel::CommunicationError, msg
91
+ end
92
+
93
+ def not_implemented
94
+ raise NotImplementedError,
95
+ "You called a method in Cod::Channel::Base. Missing implementation in "+
96
+ "the subclass!"
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,70 @@
1
+
2
+ begin
3
+ require 'beanstalk-client'
4
+ rescue LoadError
5
+ fail "You should install the gem 'beanstalk-client' to use Cod::Channel::Beanstalk."
6
+ end
7
+
8
+ module Cod
9
+ class Channel::Beanstalk < Channel::Base
10
+ NONBLOCK_TIMEOUT = 0.01
11
+
12
+ # Connection instance that is in use for this channel.
13
+ attr_reader :connection
14
+
15
+ # Name of the queue on the beanstalk server
16
+ attr_reader :tube_name
17
+
18
+ def initialize(connection, name=nil)
19
+ @connection = connection
20
+ @tube_name = (name || gen_anonymous_name('beanstalk')).freeze
21
+ end
22
+
23
+ def initialize_copy(from)
24
+ @connection = from.connection
25
+ @tube_name = from.tube_name
26
+ end
27
+
28
+ def put(message)
29
+ buffer = serialize(message)
30
+ connection.put(tube_name, buffer)
31
+ end
32
+
33
+ def waiting?
34
+ connection.waiting?(tube_name)
35
+ end
36
+
37
+ def get(opts={})
38
+ message = connection.get(tube_name,
39
+ :timeout => opts[:timeout])
40
+ return deserialize(message)
41
+ rescue Beanstalk::TimedOut
42
+ raise Channel::TimeoutError, "No messages waiting in #{tube_name}."
43
+ end
44
+
45
+ def close
46
+ @connection = @reference = nil
47
+ end
48
+
49
+ def identifier
50
+ identifier_class.new(connection.url, tube_name)
51
+ end
52
+ private
53
+ def gen_anonymous_name(base)
54
+ base + ".anonymous"
55
+ end
56
+ end
57
+
58
+ class Channel::Beanstalk::Identifier
59
+ def initialize(url, tube_name)
60
+ @url, @tube_name = url, tube_name
61
+ end
62
+
63
+ def resolve(ctxt=nil)
64
+ raise NotImplementedError, "Explicit context not yet implemented." \
65
+ if ctxt
66
+
67
+ Cod.beanstalk(@url, @tube_name)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,135 @@
1
+ module Cod
2
+ # A channel that uses IO.pipe as its transport mechanism. This means that
3
+ # you can only communicate within a single process hierarchy using this,
4
+ # since the file descriptors are not visible to the outside world.
5
+ #
6
+ class Channel::Pipe < Channel::Base
7
+ # A tuple storing the read and the write end of a IO.pipe.
8
+ #
9
+ Fds = Struct.new(:r, :w)
10
+
11
+ attr_reader :fds
12
+
13
+ def initialize(name=nil)
14
+ @fds = Fds.new(*IO.pipe)
15
+ @waiting_messages = []
16
+ end
17
+
18
+ def initialize_copy(old)
19
+ old_fds = old.fds
20
+
21
+ raise ArgumentError,
22
+ "Dupping a pipe channel only makes sense if it is still unused." \
23
+ unless old_fds.r && old_fds.w
24
+
25
+ @fds = Fds.new(
26
+ old_fds.r.dup,
27
+ old_fds.w.dup)
28
+
29
+ @waiting_messages = []
30
+ end
31
+
32
+ def put(message)
33
+ close_read
34
+
35
+ unless fds.w
36
+ direction_error 'Cannot put data to pipe. Already closed that end?'
37
+ end
38
+
39
+ buffer = transport_pack(message)
40
+ fds.w.write(buffer)
41
+ rescue Errno::EPIPE
42
+ direction_error "You should #dup before writing; Looks like no other copy exists currently."
43
+ end
44
+
45
+ def waiting?
46
+ process_inbound_nonblock
47
+ queued?
48
+ rescue Cod::Channel::CommunicationError
49
+ # Gets raised whenever communication fails permanently. This means that
50
+ # we probably wont be able to return any more messages. The only messages
51
+ # remaining in such a situation would be those queued.
52
+ return queued?
53
+ end
54
+
55
+ def get(opts={})
56
+ close_write
57
+
58
+ return @waiting_messages.shift if queued?
59
+
60
+ start_time = Time.now
61
+ loop do
62
+ IO.select([fds.r], nil, [fds.r], 0.1)
63
+ process_inbound_nonblock
64
+ return @waiting_messages.shift if queued?
65
+
66
+ if opts[:timeout] && (Time.now-start_time) > opts[:timeout]
67
+ raise Cod::Channel::TimeoutError,
68
+ "No messages waiting in pipe."
69
+ end
70
+ end
71
+ # NEVER REACHED
72
+
73
+ rescue Errno::EPIPE
74
+ direction_error 'Cannot get data from pipe. Already closed that end?'
75
+ end
76
+
77
+ def close
78
+ close_write
79
+ close_read
80
+ end
81
+
82
+ def identifier
83
+ identifier_class.new(self)
84
+ end
85
+ private
86
+ def queued?
87
+ not @waiting_messages.empty?
88
+ end
89
+
90
+ def close_write
91
+ return unless fds.w
92
+ fds.w.close
93
+ fds.w = nil
94
+ end
95
+
96
+ def close_read
97
+ return unless fds.r
98
+ fds.r.close
99
+ fds.r = nil
100
+ end
101
+
102
+ # Tries hard to empty the pipe and to store incoming messages in
103
+ # @waiting_messages.
104
+ #
105
+ def process_inbound_nonblock
106
+ buffer = fds.r.read_nonblock(1024*1024*1024)
107
+
108
+ while buffer.size > 0
109
+ @waiting_messages << transport_unpack(buffer)
110
+ end
111
+ rescue EOFError
112
+ # We've just hit end of file in the pipe. That means that all write
113
+ # ends have been closed.
114
+ communication_error "All write ends for this pipe have been closed. "+
115
+ "Further #get's would block forever." \
116
+ unless queued?
117
+ rescue Errno::EAGAIN
118
+ # Catch and ignore this: fds.r is not ready and read would block.
119
+ end
120
+ end
121
+
122
+ class Channel::Pipe::Identifier
123
+ def initialize(channel)
124
+ @objid = channel.object_id
125
+ end
126
+
127
+ def resolve
128
+ ObjectSpace._id2ref(@objid)
129
+ rescue RangeError
130
+ raise Cod::InvalidIdentifier,
131
+ "Could not reference channel. Either it was garbage collected "+
132
+ "or it never existed in this process."
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,81 @@
1
+
2
+ module Cod
3
+ # A client that consumes a service (Cod::Service).
4
+ #
5
+ class Client
6
+ attr_reader :incoming
7
+ attr_reader :outgoing
8
+
9
+ # Create a new client and tie it to an answer channel. The answer channel
10
+ # will often be anonymous - no one except the service needs to write
11
+ # there.
12
+ #
13
+ def initialize(requests, answers, timeout=1)
14
+ @timeout = timeout
15
+ @incoming = answers
16
+ @outgoing = requests
17
+ @request_id = 0
18
+ end
19
+
20
+ # Calls the service in a synchronous fashion. Returns the message the
21
+ # server sends back.
22
+ #
23
+ def call(message=nil)
24
+ expected_id = next_request_id
25
+ outgoing.put envelope(expected_id, message, incoming, true)
26
+
27
+ start_time = Time.now
28
+ loop do
29
+ received_id, answer = incoming.get(:timeout => @timeout)
30
+ return answer if received_id == expected_id
31
+
32
+ # We're receiving answers with request_ids that are outside the
33
+ # window that we would expect. Something is seriously amiss.
34
+ raise Cod::Channel::CommunicationError,
35
+ "Missed request." unless earlier?(expected_id, received_id)
36
+
37
+ # We've been waiting (and consuming answers) for too long - overall
38
+ # timeout has elapsed.
39
+ raise Cod::Channel::TimeoutError,
40
+ "Timed out while waiting for service request answer." \
41
+ if (Time.now-start_time) > @timeout
42
+ end
43
+ end
44
+
45
+ # This sends the server a message without waiting for an answer. The
46
+ # server will throw away the answer produced.
47
+ #
48
+ def notify(message=nil)
49
+ outgoing.put envelope(next_request_id, message, incoming, false)
50
+ nil
51
+ end
52
+
53
+ # Closes all resources that are held in the client.
54
+ #
55
+ def close
56
+ incoming.close
57
+ outgoing.close
58
+ end
59
+
60
+ private
61
+
62
+ # Creates a message to send to the service.
63
+ #
64
+ def envelope(id, message, incoming_channel, needs_answer)
65
+ [id, message, incoming_channel, needs_answer]
66
+ end
67
+
68
+ # Returns a sequence of request ids.
69
+ #
70
+ def next_request_id
71
+ @request_id += 1
72
+ end
73
+
74
+ # True if the received request id answers a request that has been
75
+ # earlier than the expected request id.
76
+ #
77
+ def earlier?(expected, received)
78
+ expected > received
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,56 @@
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
+ class Connection::Beanstalk
6
+ # The url that was used to connect to the beanstalk server.
7
+ attr_reader :url
8
+
9
+ # Connection to the beanstalk server.
10
+ attr_reader :connection
11
+
12
+ def initialize(url)
13
+ @url = url
14
+ @connection = Beanstalk::Connection.new(url)
15
+ end
16
+
17
+ # Writes a raw message as a job to the tube given by name.
18
+ #
19
+ def put(name, message)
20
+ connection.use name
21
+ connection.put message
22
+ end
23
+
24
+ # Returns true if there are jobs waiting in the tube given by 'name'
25
+ def waiting?(name)
26
+ watch(name) do
27
+ !! connection.peek_ready
28
+ end
29
+ end
30
+
31
+ # Removes and returns the next message waiting in the tube given by name.
32
+ #
33
+ def get(name, opts={})
34
+ watch(name) do
35
+ job = connection.reserve(opts[:timeout])
36
+ job.delete
37
+
38
+ job.body
39
+ end
40
+ end
41
+
42
+ # Closes the connection
43
+ #
44
+ def close
45
+ @connection = nil
46
+ end
47
+
48
+ private
49
+ def watch(name)
50
+ connection.watch(name)
51
+ yield
52
+ ensure
53
+ connection.ignore(name)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,72 @@
1
+ require 'weakref'
2
+
3
+ module Cod
4
+ # Channels inside a context know each other and can be looked up by their
5
+ # identifier. Context is also responsible for holding connections and for
6
+ # doing background work. For most purposes, you will only need one context;
7
+ # by using methods on the Cod module directly, you implicitly hold a context
8
+ # and call methods there.
9
+ #
10
+ class Context
11
+
12
+ def self.install_at_fork(ref)
13
+ at_fork do |old_handler|
14
+ old_handler.call rescue nil
15
+
16
+ begin
17
+ ref.reset
18
+ rescue WeakRef::RefError
19
+ # IGNORED EXCEPTION
20
+ end
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ @connections = {}
26
+
27
+ self.class.install_at_fork(WeakRef.new(self))
28
+ end
29
+
30
+ def pipe(name=nil)
31
+ Cod::Channel::Pipe.new(name)
32
+ end
33
+
34
+ def beanstalk(url, name=nil)
35
+ Cod::Channel::Beanstalk.new(
36
+ connection(:beanstalk, url), name)
37
+ end
38
+
39
+ def reset
40
+ @connections = {}
41
+ end
42
+
43
+ private
44
+ # Returns a connection to a system identified by type and url. Currently,
45
+ # connections are never released or closed. This is only a minor drawback
46
+ # since there will be few of them. (considering we only use this for
47
+ # beanstalk)
48
+ #
49
+ def connection(type, url)
50
+ key = connection_key(type, url)
51
+
52
+ connection = @connections[key]
53
+ return connection if connection
54
+
55
+ produce_connection(type, url).tap { |connection|
56
+ @connections.store(key, connection) }
57
+ end
58
+
59
+ def connection_key(type, url)
60
+ [type, url]
61
+ end
62
+
63
+ def produce_connection(type, url)
64
+ case type
65
+ when :beanstalk
66
+ return Connection::Beanstalk.new(url)
67
+ end
68
+
69
+ fail "Tried to produce a connection of unknown type #{type.inspect}."
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,44 @@
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
+ def initialize(channel)
12
+ @channel = channel
13
+ @subscriptions = []
14
+ end
15
+
16
+ # Sends the message to all subscribers that listen to this topic.
17
+ #
18
+ def publish(topic, message)
19
+ handle_subscriptions
20
+
21
+ for subscription in @subscriptions
22
+ subscription.put message if subscription === topic
23
+ end
24
+ end
25
+
26
+ # Closes all resources used by the directory.
27
+ #
28
+ def close
29
+ channel.close
30
+ end
31
+
32
+ private
33
+
34
+ def handle_subscriptions
35
+ while channel.waiting?
36
+ subscribe channel.get
37
+ end
38
+ end
39
+
40
+ def subscribe(subscription)
41
+ @subscriptions << subscription
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ module Cod
2
+ # Represents a subscription to a directory.
3
+ #
4
+ class Directory::Subscription
5
+ attr_reader :matcher
6
+ attr_reader :channel
7
+
8
+ def initialize(matcher, channel)
9
+ @matcher = matcher
10
+ @channel = channel
11
+ end
12
+
13
+ def ===(other)
14
+ matcher === other
15
+ end
16
+
17
+ def put(msg)
18
+ channel.put msg
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+
2
+ module Cod
3
+ # A service that receives requests and answers. A service has always at
4
+ # least one provider that creates an instance of this class and waits in
5
+ # #one or #each. Clients then instantiate Cod::Client and #call the service.
6
+ #
7
+ # Example:
8
+ # # service side
9
+ # service = Cod::Service.new(incoming_channel)
10
+ # service.one { |msg| 'answer' }
11
+ #
12
+ # # client side
13
+ # client = Cod::Client.new(incoming_channel, service_channel)
14
+ # client.call('call message') # => 'answer'
15
+ #
16
+ # == Topology
17
+ #
18
+ # A service always has (potentially) multiple clients and depending on the
19
+ # transport layer used, one or more workers handling the clients request.
20
+ # They will always receive the messages in a round robin fashion; the
21
+ # service corresponds in this case to the channel address; clients need not
22
+ # know the workers involved.
23
+ #
24
+ class Service
25
+ # Incoming channel for requests.
26
+ attr_reader :incoming
27
+
28
+ def initialize(channel)
29
+ @incoming = channel
30
+ end
31
+
32
+ # Calls the given block with the next request and returns the block answer
33
+ # to the service client.
34
+ #
35
+ def one
36
+ request_id, message, answer_channel, needs_answer = incoming.get
37
+
38
+ answer = yield(message)
39
+
40
+ if needs_answer
41
+ answer_channel.put [request_id, answer]
42
+ end
43
+ end
44
+
45
+ # Loops forever, yielding requests to the block given and returning the
46
+ # answers to the client.
47
+ #
48
+ def each(&block)
49
+ loop do
50
+ one(&block)
51
+ end
52
+ end
53
+
54
+ # Releases all resources held by the service.
55
+ #
56
+ def close
57
+ incoming.close
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,34 @@
1
+ module Cod
2
+ # A topic in a directory.
3
+ #
4
+ class Topic
5
+ attr_reader :answers, :directory
6
+ attr_reader :match_expr
7
+ def initialize(match_expr, directory_channel, answer_channel)
8
+ @directory, @answers = directory_channel, answer_channel
9
+ @match_expr = match_expr
10
+
11
+ subscribe
12
+ end
13
+
14
+ # Subscribes this topic to the directory's messages. This gets called upon
15
+ # initialization and must not be called again.
16
+ #
17
+ def subscribe
18
+ directory.put Directory::Subscription.new(match_expr, answers)
19
+ end
20
+
21
+ # Reads the next message from the directory that matches this topic.
22
+ #
23
+ def get
24
+ answers.get
25
+ end
26
+
27
+ # Closes all resources used by the topic.
28
+ #
29
+ def close
30
+ directory.close
31
+ answers.close
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cod
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0
6
+ platform: ruby
7
+ authors:
8
+ - Kaspar Schiess
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-04-27 00:00:00 +02:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: flexmock
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: sdoc
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id003
49
+ description:
50
+ email: kaspar.schiess@absurd.li
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README
57
+ files:
58
+ - Gemfile
59
+ - HISTORY.txt
60
+ - LICENSE
61
+ - Rakefile
62
+ - README
63
+ - lib/at_fork.rb
64
+ - lib/cod/channel/base.rb
65
+ - lib/cod/channel/beanstalk.rb
66
+ - lib/cod/channel/pipe.rb
67
+ - lib/cod/channel.rb
68
+ - lib/cod/client.rb
69
+ - lib/cod/connection/beanstalk.rb
70
+ - lib/cod/context.rb
71
+ - lib/cod/directory/subscription.rb
72
+ - lib/cod/directory.rb
73
+ - lib/cod/service.rb
74
+ - lib/cod/topic.rb
75
+ - lib/cod.rb
76
+ - examples/master_child.rb
77
+ - examples/ping.rb
78
+ - examples/pong.rb
79
+ - examples/service.rb
80
+ - examples/service_directory.rb
81
+ has_rdoc: true
82
+ homepage: http://kschiess.github.com/cod
83
+ licenses: []
84
+
85
+ post_install_message:
86
+ rdoc_options:
87
+ - --main
88
+ - README
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ requirements: []
104
+
105
+ rubyforge_project:
106
+ rubygems_version: 1.5.2
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Really simple IPC.
110
+ test_files: []
111
+