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 +12 -0
- data/HISTORY.txt +11 -0
- data/LICENSE +23 -0
- data/README +33 -0
- data/Rakefile +35 -0
- data/examples/master_child.rb +16 -0
- data/examples/ping.rb +9 -0
- data/examples/pong.rb +9 -0
- data/examples/service.rb +26 -0
- data/examples/service_directory.rb +32 -0
- data/lib/at_fork.rb +29 -0
- data/lib/cod.rb +49 -0
- data/lib/cod/channel.rb +19 -0
- data/lib/cod/channel/base.rb +99 -0
- data/lib/cod/channel/beanstalk.rb +70 -0
- data/lib/cod/channel/pipe.rb +135 -0
- data/lib/cod/client.rb +81 -0
- data/lib/cod/connection/beanstalk.rb +56 -0
- data/lib/cod/context.rb +72 -0
- data/lib/cod/directory.rb +44 -0
- data/lib/cod/directory/subscription.rb +21 -0
- data/lib/cod/service.rb +60 -0
- data/lib/cod/topic.rb +34 -0
- metadata +111 -0
data/Gemfile
ADDED
data/HISTORY.txt
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/examples/ping.rb
ADDED
data/examples/pong.rb
ADDED
data/examples/service.rb
ADDED
@@ -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
|
data/lib/at_fork.rb
ADDED
@@ -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
|
data/lib/cod.rb
ADDED
@@ -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'
|
data/lib/cod/channel.rb
ADDED
@@ -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
|
data/lib/cod/client.rb
ADDED
@@ -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
|
data/lib/cod/context.rb
ADDED
@@ -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
|
data/lib/cod/service.rb
ADDED
@@ -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
|
data/lib/cod/topic.rb
ADDED
@@ -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
|
+
|