cod 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
+