Puppet::Util::Queue provides queue client mix-in behaviors that enable easy 
queue client management for consumer classes.  Some relevant behaviors include:
* standard Puppet instance loader behavior for loading queue client modules 
on-demand based on the client module specified by symbolic name
* singleton registry of known queue client types (based on symbol-to-class 
mappings from the instance loading behavior)
* simple interface for working with an actual queue client instance

Puppet::Util::Queue::Stomp wraps the Stomp::Client class to provide an initial 
queue client option supporting the Stomp messaging protocol.  This defines the 
interface for all Puppet queue client plugins going forward.

Signed-off-by: Ethan Rowe <[email protected]>
---
 lib/puppet/util/queue.rb       |  102 ++++++++++++++++++++++++++++++++++++++++
 lib/puppet/util/queue/stomp.rb |   30 ++++++++++++
 spec/unit/util/queue.rb        |   95 +++++++++++++++++++++++++++++++++++++
 spec/unit/util/queue/stomp.rb  |   62 ++++++++++++++++++++++++
 4 files changed, 289 insertions(+), 0 deletions(-)
 create mode 100644 lib/puppet/util/queue.rb
 create mode 100644 lib/puppet/util/queue/stomp.rb
 create mode 100755 spec/unit/util/queue.rb
 create mode 100755 spec/unit/util/queue/stomp.rb

diff --git a/lib/puppet/util/queue.rb b/lib/puppet/util/queue.rb
new file mode 100644
index 0000000..3724bab
--- /dev/null
+++ b/lib/puppet/util/queue.rb
@@ -0,0 +1,102 @@
+
+require 'puppet/indirector'
+require 'puppet/util/instance_loader'
+
+# Implements a message queue client type plugin registry for use by the 
indirector facility.
+# Client modules for speaking a particular protocol (e.g. Stomp::Client for 
Stomp message
+# brokers, Memcached for Starling and Sparrow, etc.) register themselves with 
this module.
+# 
+# Client classes are expected to live under the Puppet::Util::Queue namespace 
and corresponding
+# directory; the attempted use of a client via its typename (see below) will 
cause Puppet::Util::Queue
+# to attempt to load the corresponding plugin if it is not yet loaded.  The 
client class registers itself
+# with Puppet::Util::Queue and should use the same type name as the autloader 
expects for the plugin file.
+#   class Puppet::Util::Queue::SpecialMagicalClient < Messaging::SpecialMagic
+#       ...
+#       Puppet::Util::Queue.register_queue_type_class(self)
+#   end
+#
+# This module reduces the rightmost segment of the class name into a pretty 
symbol that will
+# serve as the queuing client's name.  Which means that the 
"SpecialMagicalClient" above will
+# be named <em>:special_magical_client</em> within the registry.
+#
+# Another class/module may mix-in this module, and may then make use of the 
registered clients.
+#   class Queue::Fue
+#       # mix it in at the class object level rather than instance level
+#       extend ::Puppet::Util::Queue
+#
+#       # specify the default client type to use.
+#       self.queue_type_default = :special_magical_type
+#   end
+#
+# Queue::Fue instances can get a message queue client through the registry 
through the mixed-in method
+# +client+, which will return a class-wide singleton client instance, 
determined by +client_class+.
+#
+# The client plugins are expected to implement an interface similar to that of 
Stomp::Client:
+# * <tt>new()</tt> should return a connected, ready-to-go client instance.  
Note that no arguments are passed in.
+# * <tt>send_message(queue, message)</tt> should send the _message_ to the 
specified _queue_.
+# * <tt>subscribe(queue)</tt> _block_ subscribes to _queue_ and executes 
_block_ upon receiving a message.
+# * _queue_ names are simple names independent of the message broker or client 
library.  No "/queue/" prefixes like in Stomp::Client.
+module Puppet::Util::Queue
+    extend Puppet::Util::InstanceLoader
+    attr_accessor :queue_type_default
+    instance_load :queue_clients, 'puppet/util/queue'
+
+    # Adds a new class/queue-type pair to the registry.  The _type_ argument 
is optional; if not provided,
+    # _type_ defaults to a lowercased, underscored symbol programmatically 
derived from the rightmost
+    # namespace of <em>klass.name</em>.
+    #
+    #   # register with default name +:you+
+    #   register_queue_type(Foo::You)
+    #   
+    #   # register with explicit queue type name +:myself+
+    #   register_queue_type(Foo::Me, :myself)
+    #
+    # If the type is already registered, an exception is thrown.  No checking 
is performed of _klass_,
+    # however; a given class could be registered any number of times, as long 
as the _type_ differs with
+    # each registration.
+    def self.register_queue_type(klass, type = nil)
+        type ||= queue_type_from_class(klass)
+        raise Puppet::Error, "Queue type %s is already registered" % type.to_s 
if instance_hash(:queue_clients).include?(type)
+        instance_hash(:queue_clients)[type] = klass
+    end
+
+    # Given a queue type symbol, returns the associated +Class+ object.  If 
the queue type is unknown
+    # (meaning it hasn't been registered with this module), an exception is 
thrown.
+    def self.queue_type_to_class(type)
+        c = loaded_instance :queue_clients, type
+        raise Puppet::Error, "Queue type %s is unknown." % type unless c
+        c
+    end
+
+    # Given a class object _klass_, returns the programmatic default queue 
type name symbol for _klass_.
+    # The algorithm is as shown in earlier examples; the last namespace 
segment of _klass.name_ is taken
+    # and converted from mixed case to underscore-separated lowercase, and 
interned.
+    #   queue_type_from_class(Foo) -> :foo
+    #   queue_type_from_class(Foo::Too) -> :too
+    #   queue_type_from_class(Foo::ForYouTwo) -> :for_you_too
+    #
+    # The implicit assumption here, consistent with Puppet's approach to 
plugins in general,
+    # is that all your client modules live in the same namespace, such that 
reduction to
+    # a flat namespace of symbols is reasonably safe.
+    def self.queue_type_from_class(klass)
+        # convert last segment of classname from studly caps to lower case 
with underscores, and symbolize
+        klass.name.split('::').pop.sub(/^[A-Z]/) {|c| 
c.downcase}.gsub(/[A-Z]/) {|c| '_' + c.downcase }.intern
+    end
+
+    # The class object for the client to be used, determined by queue 
configuration
+    # settings and known queue client types.
+    # Looked to the <tt>:queue_client</tt> configuration entry in the running 
application for
+    # the default queue type to use, and fails over to +queue_type_default+ if 
the configuration
+    # information is not present.
+    def client_class
+         Puppet::Util::Queue.queue_type_to_class(Puppet[:queue_client] || 
queue_type_default)
+        # Puppet::Util::Queue.queue_type_to_class(Puppet[:queue_client] || 
:stomp)
+    end
+
+    # Returns (instantiating as necessary) the singleton queue client 
instance, according to the
+    # client_class.  No arguments go to the client class constructor, meaning 
its up to the client class
+    # to know how to determine its queue message source (presumably through 
Puppet configuration data).
+    def client
+        @client ||= client_class.new
+    end
+end
diff --git a/lib/puppet/util/queue/stomp.rb b/lib/puppet/util/queue/stomp.rb
new file mode 100644
index 0000000..6f845c3
--- /dev/null
+++ b/lib/puppet/util/queue/stomp.rb
@@ -0,0 +1,30 @@
+require 'puppet/util/queue'
+require 'stomp'
+
+# Implements the Ruby Stomp client as a queue type within the 
Puppet::Indirector::Queue::Client
+# registry, for use with the <tt>:queue</tt> indirection terminus type.
+#
+# Looks to <tt>Puppet[:queue_source]</tt> for the sole argument to the 
underlying Stomp::Client constructor;
+# consequently, for this client to work, <tt>Puppet[:queue_source]</tt> must 
use the Stomp::Client URL-like
+# syntax for identifying the Stomp message broker: 
<em>login:[email protected]</em>
+class Puppet::Util::Queue::Stomp
+    attr_accessor :stomp_client
+
+    def initialize
+        self.stomp_client = Stomp::Client.new( Puppet[:queue_source] )
+    end
+
+    def send_message(target, msg)
+        stomp_client.send(stompify_target(target), msg)
+    end
+
+    def subscribe(target)
+        stomp_client.subscribe(stompify_target(target)) {|stomp_message| 
yield(stomp_message.body)}
+    end
+
+    def stompify_target(target)
+        '/queue/' + target.to_s
+    end
+
+    Puppet::Util::Queue.register_queue_type(self, :stomp)
+end
diff --git a/spec/unit/util/queue.rb b/spec/unit/util/queue.rb
new file mode 100755
index 0000000..525e623
--- /dev/null
+++ b/spec/unit/util/queue.rb
@@ -0,0 +1,95 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'puppet/util/queue'
+require 'spec/mocks'
+
+def make_test_client_class(n)
+    c = Class.new do
+        class <<self
+            attr_accessor :name
+            def to_s
+                name
+            end
+        end
+    end
+    c.name = n
+    c
+end
+
+mod = Puppet::Util::Queue
+client_classes = { :default => make_test_client_class('Bogus::Default'), 
:setup => make_test_client_class('Bogus::Setup') }
+mod.register_queue_type(client_classes[:default], :default)
+mod.register_queue_type(client_classes[:setup], :setup)
+
+describe Puppet::Util::Queue do
+    before :each do
+        @class = Class.new do
+            extend mod
+            self.queue_type_default = :default
+        end
+    end
+
+    context 'when determining a type name from a class' do
+        it 'should handle a simple one-word class name' do
+            mod.queue_type_from_class(make_test_client_class('Foo')).should == 
:foo
+        end
+
+        it 'should handle a simple two-word class name' do
+            mod.queue_type_from_class(make_test_client_class('FooBar')).should 
== :foo_bar
+        end
+
+        it 'should handle a two-part class name with one terminating word' do
+            
mod.queue_type_from_class(make_test_client_class('Foo::Bar')).should == :bar
+        end
+
+        it 'should handle a two-part class name with two terminating words' do
+            
mod.queue_type_from_class(make_test_client_class('Foo::BarBah')).should == 
:bar_bah
+        end
+    end
+
+    context 'when registering a queue client class' do
+        c = make_test_client_class('Foo::Bogus')
+        it 'uses the proper default name logic when type is unspecified' do
+            mod.register_queue_type(c)
+            mod.queue_type_to_class(:bogus).should == c
+        end
+
+        it 'uses an explicit type name when provided' do
+            mod.register_queue_type(c, :aardvark)
+            mod.queue_type_to_class(:aardvark).should == c
+        end
+
+        it 'throws an exception when type names conflict' do
+            mod.register_queue_type( make_test_client_class('Conflict') )
+            lambda { mod.register_queue_type( c, :conflict) }.should 
raise_error
+        end
+
+        it 'handle multiple, non-conflicting registrations' do
+            a = make_test_client_class('TestA')
+            b = make_test_client_class('TestB')
+            mod.register_queue_type(a)
+            mod.register_queue_type(b)
+            mod.queue_type_to_class(:test_a).should == a
+            mod.queue_type_to_class(:test_b).should == b
+        end
+
+        it 'throws an exception when type name is unknown' do
+            lambda { mod.queue_type_to_class(:nope) }.should raise_error
+        end
+    end
+
+    context 'when determining client type' do
+        it 'returns client class based on queue_type_default' do
+            Puppet.settings.stubs(:value).returns(nil)
+            @class.client_class.should == client_classes[:default]
+            @class.client.class.should == client_classes[:default]
+        end
+
+        it 'prefers settings variable for client class when specified' do
+            Puppet.settings.stubs(:value).with(:queue_client).returns(:setup)
+            @class.client_class.should == client_classes[:setup]
+            @class.client.class.should == client_classes[:setup]
+        end
+    end
+end
diff --git a/spec/unit/util/queue/stomp.rb b/spec/unit/util/queue/stomp.rb
new file mode 100755
index 0000000..c4d8b76
--- /dev/null
+++ b/spec/unit/util/queue/stomp.rb
@@ -0,0 +1,62 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../../../spec_helper'
+require 'puppet/util/queue'
+
+describe Puppet::Util::Queue do
+    it 'should load :stomp client appropriately' do
+        Puppet.settings.stubs(:value).returns 'faux_queue_source'
+        Puppet::Util::Queue.queue_type_to_class(:stomp).name.should == 
'Puppet::Util::Queue::Stomp'
+    end
+end
+
+describe 'Puppet::Util::Queue::Stomp' do
+    before :all do
+        class Stomp::Client
+            include Mocha::Standalone
+            attr_accessor :queue_source
+
+            def send(q, m)
+                'To %s: %s' % [q, m]
+            end
+
+            def subscribe(q)
+                yield(stub(:body => 'subscribe: %s' % q))
+            end
+
+            def initialize(s)
+                self.queue_source = s
+            end
+        end
+    end
+
+    before :each do
+        Puppet.settings.stubs(:value).returns 'faux_queue_source'
+    end
+
+    it 'should make send function like core Ruby instead of stomp client send 
method' do
+        o = Puppet::Util::Queue::Stomp.new
+        o.expects(:pants).with('foo').once
+        o.send(:pants, 'foo')
+    end
+
+    it 'should be registered with Puppet::Util::Queue as :stomp type' do
+        Puppet::Util::Queue.queue_type_to_class(:stomp).should == 
Puppet::Util::Queue::Stomp
+    end
+
+    it 'should initialize using Puppet[:queue_source] for configuration' do
+        o = Puppet::Util::Queue::Stomp.new
+        o.stomp_client.queue_source.should == 'faux_queue_source'
+    end
+
+    it 'should transform the simple queue name to "/queue/<queue_name>"' do
+        Puppet::Util::Queue::Stomp.new.stompify_target('blah').should == 
'/queue/blah'
+    end
+
+    it 'should transform the queue name properly and pass along to superclass 
for send and subscribe' do
+        o = Puppet::Util::Queue::Stomp.new
+        o.send_message('fooqueue', 'Smite!').should == 'To /queue/fooqueue: 
Smite!'
+        o.subscribe('moodew') {|obj| obj}.should == 'subscribe: /queue/moodew'
+    end
+end
+
-- 
1.5.5.1


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to 
[email protected]
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to