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