activemessaging 0.6.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.
Files changed (46) hide show
  1. data/Rakefile +50 -0
  2. data/generators/a13g_test_harness/a13g_test_harness_generator.rb +19 -0
  3. data/generators/a13g_test_harness/templates/active_messaging_test.rhtml +13 -0
  4. data/generators/a13g_test_harness/templates/active_messaging_test_controller.rb +29 -0
  5. data/generators/a13g_test_harness/templates/index.rhtml +17 -0
  6. data/generators/filter/USAGE +0 -0
  7. data/generators/filter/filter_generator.rb +19 -0
  8. data/generators/filter/templates/filter.rb +12 -0
  9. data/generators/filter/templates/filter_test.rb +28 -0
  10. data/generators/processor/USAGE +8 -0
  11. data/generators/processor/processor_generator.rb +31 -0
  12. data/generators/processor/templates/application.rb +18 -0
  13. data/generators/processor/templates/broker.yml +79 -0
  14. data/generators/processor/templates/jruby_poller +117 -0
  15. data/generators/processor/templates/messaging.rb +12 -0
  16. data/generators/processor/templates/poller +23 -0
  17. data/generators/processor/templates/poller.rb +23 -0
  18. data/generators/processor/templates/processor.rb +8 -0
  19. data/generators/processor/templates/processor_test.rb +20 -0
  20. data/generators/tracer/USAGE +8 -0
  21. data/generators/tracer/templates/controller.rb +14 -0
  22. data/generators/tracer/templates/helper.rb +2 -0
  23. data/generators/tracer/templates/index.rhtml +4 -0
  24. data/generators/tracer/templates/layout.rhtml +16 -0
  25. data/generators/tracer/templates/trace_processor.rb +100 -0
  26. data/generators/tracer/tracer_generator.rb +25 -0
  27. data/lib/activemessaging.rb +133 -0
  28. data/lib/activemessaging/adapter.rb +21 -0
  29. data/lib/activemessaging/adapters/asqs.rb +412 -0
  30. data/lib/activemessaging/adapters/base.rb +82 -0
  31. data/lib/activemessaging/adapters/jms.rb +237 -0
  32. data/lib/activemessaging/adapters/reliable_msg.rb +190 -0
  33. data/lib/activemessaging/adapters/stomp.rb +99 -0
  34. data/lib/activemessaging/adapters/test.rb +155 -0
  35. data/lib/activemessaging/adapters/wmq.rb +202 -0
  36. data/lib/activemessaging/filter.rb +29 -0
  37. data/lib/activemessaging/gateway.rb +422 -0
  38. data/lib/activemessaging/message_sender.rb +30 -0
  39. data/lib/activemessaging/named_base.rb +54 -0
  40. data/lib/activemessaging/processor.rb +45 -0
  41. data/lib/activemessaging/support.rb +17 -0
  42. data/lib/activemessaging/test_helper.rb +194 -0
  43. data/lib/activemessaging/trace_filter.rb +34 -0
  44. data/messaging.rb.example +5 -0
  45. data/tasks/start_consumers.rake +8 -0
  46. metadata +123 -0
@@ -0,0 +1,12 @@
1
+ #
2
+ # Add your destination definitions here
3
+ # can also be used to configure filters, and processor groups
4
+ #
5
+ ActiveMessaging::Gateway.define do |s|
6
+ #s.destination :orders, '/queue/Orders'
7
+ #s.filter :some_filter, :only=>:orders
8
+ #s.processor_group :group1, :order_processor
9
+
10
+ s.destination :<%= singular_name %>, '/queue/<%= class_name %>'
11
+
12
+ end
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'daemons'
4
+ require 'activesupport'
5
+ require 'activemessaging'
6
+
7
+ APP_ROOT = File.expand_path(File.dirname(__FILE__) + '/..')
8
+ script_file = File.join(File.dirname(__FILE__)+'/../lib/poller.rb')
9
+ tmp_dir = File.join(File.expand_path(APP_ROOT), 'tmp')
10
+
11
+ options = {
12
+ :app_name => "poller",
13
+ :dir_mode => :normal,
14
+ :dir => tmp_dir,
15
+ :multiple => true,
16
+ :ontop => false,
17
+ :mode => :load,
18
+ :backtrace => true,
19
+ :monitor => true,
20
+ :log_output => true
21
+ }
22
+
23
+ Daemons.run(script_file,options)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # Make sure stdout and stderr write out without delay for using with daemon like scripts
3
+ STDOUT.sync = true; STDOUT.flush
4
+ STDERR.sync = true; STDERR.flush
5
+
6
+ #Try to Load Merb
7
+ begin
8
+ require File.expand_path(File.dirname(__FILE__)+'/../config/boot')
9
+ #need this because of the CWD
10
+ Merb.root = MERB_ROOT
11
+ require File.expand_path(File.dirname(__FILE__)+'/../config/merb_init')
12
+ rescue LoadError
13
+ # Load Rails
14
+ RAILS_ROOT=File.expand_path(File.join(File.dirname(__FILE__), '..'))
15
+ require File.join(RAILS_ROOT, 'config', 'boot')
16
+ require File.join(RAILS_ROOT, 'config', 'environment')
17
+ end
18
+
19
+ # Load ActiveMessaging processors
20
+ #ActiveMessaging::load_processors
21
+
22
+ # Start it up!
23
+ ActiveMessaging::start
@@ -0,0 +1,8 @@
1
+ class <%= class_name %>Processor < ApplicationProcessor
2
+
3
+ subscribes_to :<%= singular_name %>
4
+
5
+ def on_message(message)
6
+ logger.debug "<%= class_name %>Processor received: " + message
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+ require File.dirname(__FILE__) + '/../../vendor/plugins/activemessaging/lib/activemessaging/test_helper'
3
+ require File.dirname(__FILE__) + '/../../app/processors/application'
4
+
5
+ class <%= class_name %>ProcessorTest < Test::Unit::TestCase
6
+ include ActiveMessaging::TestHelper
7
+
8
+ def setup
9
+ load File.dirname(__FILE__) + "/../../app/processors/<%= file_name %>_processor.rb"
10
+ @processor = <%= class_name %>Processor.new
11
+ end
12
+
13
+ def teardown
14
+ @processor = nil
15
+ end
16
+
17
+ def test_<%= file_name %>_processor
18
+ @processor.on_message('Your test message here!')
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a stub ActiveMessaging Tracing Controller.
3
+
4
+ Examples:
5
+ ./script/generate tracer Tracer
6
+ will create:
7
+ /app/processors/tracer_processor.rb
8
+ /app/views/tracer/index.rhtml
@@ -0,0 +1,14 @@
1
+ class <%= class_name %>Controller < ApplicationController
2
+ include ActiveMessaging::MessageSender
3
+
4
+ publishes_to :trace
5
+
6
+ def index
7
+ end
8
+
9
+ def clear
10
+ publish :trace, "<trace-control>clear</trace-control>"
11
+ redirect_to :action=>'index'
12
+ end
13
+
14
+ end
@@ -0,0 +1,2 @@
1
+ module <%= class_name %>Helper
2
+ end
@@ -0,0 +1,4 @@
1
+ <%=button_to 'Clear', :action=>'clear'%>
2
+
3
+ <%=image_tag '../trace.png', :id => 'graph' %>
4
+
@@ -0,0 +1,16 @@
1
+ <html>
2
+ <head>
3
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
4
+ <title>Tracer</title>
5
+ <%=javascript_include_tag :defaults %>
6
+ </head>
7
+ <body>
8
+ <h2>Tracer</h2>
9
+
10
+ <% if @flash[:note] -%>
11
+ <div id="flash"><%= @flash[:note] %></div>
12
+ <% end -%>
13
+
14
+ <%= @content_for_layout %>
15
+ </body>
16
+ </html>
@@ -0,0 +1,100 @@
1
+ require 'mpath'
2
+ require 'active_support'
3
+
4
+ class Dot
5
+
6
+ attr_accessor :name, :nodes, :edges, :clean_names
7
+
8
+ def initialize name
9
+ @name = name
10
+ @nodes = {}
11
+ @clean_names = {}
12
+ @edges = []
13
+ yield self
14
+ end
15
+
16
+ def node name, params = {}
17
+ @nodes[clean_name(name)] = params.stringify_keys.reverse_merge "label"=>name
18
+ end
19
+
20
+ def clean_name name
21
+ @clean_names[name] = "node#{@clean_names.length+1}" if @clean_names[name].nil?
22
+ @clean_names[name]
23
+ end
24
+
25
+ def edge from, to
26
+ edge = [clean_name(from), clean_name(to)]
27
+ @edges << edge unless @edges.member? edge
28
+ end
29
+
30
+ def to_s
31
+ dot = "digraph #{@name} {\n"
32
+ @nodes.each do |node_name, options|
33
+ dot += "\t#{node_name.to_s}"
34
+ optionstrings = []
35
+ options.keys.sort.each do |key|
36
+ optionstrings << "#{key}=\"#{options[key]}\""
37
+ end
38
+ dot += " [#{optionstrings.join(', ')}]" if optionstrings.length>0
39
+ dot += ";\n"
40
+ end
41
+ @edges.each {|e| dot += "\t#{e[0].to_s}->#{e[1].to_s};\n"}
42
+ dot += "}\n"
43
+ end
44
+
45
+ def == other
46
+ (other.name == name) && (other.nodes == nodes) && (other.edges == edges) && (other.clean_names == clean_names)
47
+ end
48
+ end
49
+
50
+ class TraceProcessor < ActiveMessaging::Processor
51
+ subscribes_to :trace
52
+
53
+ @@dot = Dot.new("Trace") {}
54
+
55
+ class << self
56
+
57
+ end
58
+
59
+ def dot
60
+ @@dot
61
+ end
62
+
63
+ def on_message(message)
64
+ xml = Mpath.parse(message)
65
+ if (xml.sent?) then
66
+ from = xml.sent.from.to_s
67
+ queue = xml.sent.queue.to_s
68
+
69
+ @@dot.node from
70
+ @@dot.node queue, "shape" => 'box'
71
+ @@dot.edge from, queue #hah - could do from => to
72
+ elsif (xml.received?) then
73
+ by = xml.received.by.to_s
74
+ queue = xml.received.queue.to_s
75
+
76
+ @@dot.node queue, "shape" => 'box'
77
+ @@dot.node by
78
+ @@dot.edge queue, by
79
+ elsif (xml.trace_control) then
80
+ command = xml.trace_control.to_s
81
+ begin
82
+ send command
83
+ rescue
84
+ puts "TraceProcessor: I don't understand the command #{command}"
85
+ end
86
+ end
87
+ create_image
88
+ end
89
+
90
+ def create_image
91
+ File.open(DOT_FILE, "w") {|f| f.puts @@dot.to_s }
92
+ output_file = APP_ROOT + "/public/trace.png"
93
+ `dot -Tpng -o #{output_file} #{DOT_FILE}`
94
+ end
95
+
96
+ def clear
97
+ @@dot = Dot.new("Trace") {}
98
+ end
99
+
100
+ end
@@ -0,0 +1,25 @@
1
+ class TracerGenerator < RubiGen::Base
2
+ def manifest
3
+ record do |m|
4
+ path = 'app/controllers'
5
+ m.directory path
6
+ m.template 'controller.rb', File.join(path, "#{file_name}_controller.rb")
7
+
8
+ path = 'app/processors'
9
+ m.directory path
10
+ m.template 'trace_processor.rb', File.join(path, "#{file_name}_processor.rb")
11
+
12
+ path = 'app/helpers'
13
+ m.directory path
14
+ m.template 'helper.rb', File.join(path, "#{file_name}_helper.rb")
15
+
16
+ path = 'app/views/layouts'
17
+ m.directory path
18
+ m.file 'layout.rhtml', File.join(path, "#{file_name}.rhtml")
19
+
20
+ path = "app/views/#{file_name}"
21
+ m.directory path
22
+ m.file 'index.rhtml', File.join(path, "index.rhtml")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,133 @@
1
+ module ActiveMessaging
2
+ VERSION = "0.5" #maybe this should be higher, but I'll let others judge :)
3
+ APP_ROOT = ENV['APP_ROOT'] || ENV['RAILS_ROOT'] || ((defined? RAILS_ROOT) && RAILS_ROOT) || File.dirname($0)
4
+ APP_ENV = ENV['APP_ENV'] || ENV['RAILS_ENV'] || 'development'
5
+
6
+ # Used to indicate that the processing for a thread shoud complete
7
+ class StopProcessingException < Interrupt #:nodoc:
8
+ end
9
+
10
+ # Used to indicate that the processing on a message should cease,
11
+ # and the message should be returned back to the broker as best it can be
12
+ class AbortMessageException < Exception #:nodoc:
13
+ end
14
+
15
+ # Used to indicate that the processing on a message should cease,
16
+ # but no further action is required
17
+ class StopFilterException < Exception #:nodoc:
18
+ end
19
+
20
+ def ActiveMessaging.logger
21
+ @@logger = RAILS_DEFAULT_LOGGER if !defined?(@@logger) && (defined?(RAILS_DEFAULT_LOGGER) && !RAILS_DEFAULT_LOGGER.nil?)
22
+ @@logger = ActiveRecord::Base.logger unless defined?(@@logger)
23
+ @@logger = Logger.new(STDOUT) unless defined?(@@logger)
24
+ @@logger
25
+ end
26
+
27
+ # DEPRECATED, so I understand, but I'm using it nicely below.
28
+ def self.load_extensions
29
+ require 'logger'
30
+ require 'activemessaging/support'
31
+ require 'activemessaging/gateway'
32
+ require 'activemessaging/adapter'
33
+ require 'activemessaging/message_sender'
34
+ require 'activemessaging/processor'
35
+ require 'activemessaging/filter'
36
+ require 'activemessaging/trace_filter'
37
+
38
+ # load all under the adapters dir
39
+ Dir[APP_ROOT + '/vendor/plugins/activemessaging/lib/activemessaging/adapters/*.rb'].each{|a|
40
+ begin
41
+ adapter_name = File.basename(a, ".rb")
42
+ require 'activemessaging/adapters/' + adapter_name
43
+ rescue RuntimeError, LoadError => e
44
+ logger.debug "ActiveMessaging: adapter #{adapter_name} not loaded: #{ e.message }"
45
+ end
46
+ }
47
+ end
48
+
49
+ def self.load_config
50
+ path = File.expand_path("#{APP_ROOT}/config/messaging.rb")
51
+ begin
52
+ load path
53
+ rescue MissingSourceFile
54
+ logger.debug "ActiveMessaging: no '#{path}' file to load"
55
+ rescue
56
+ raise $!, " ActiveMessaging: problems trying to load '#{path}': \n\t#{$!.message}"
57
+ end
58
+ end
59
+
60
+ def self.load_processors(first=true)
61
+ #Load the parent processor.rb, then all child processor classes
62
+ load APP_ROOT + '/vendor/plugins/activemessaging/lib/activemessaging/message_sender.rb' unless defined?(ActiveMessaging::MessageSender)
63
+ load APP_ROOT + '/vendor/plugins/activemessaging/lib/activemessaging/processor.rb' unless defined?(ActiveMessaging::Processor)
64
+ load APP_ROOT + '/vendor/plugins/activemessaging/lib/activemessaging/filter.rb' unless defined?(ActiveMessaging::Filter)
65
+ logger.debug "ActiveMessaging: Loading #{APP_ROOT + '/app/processors/application.rb'}" if first
66
+ load APP_ROOT + '/app/processors/application.rb' if File.exist?("#{APP_ROOT}/app/processors/application.rb")
67
+ Dir[APP_ROOT + '/app/processors/*.rb'].each do |f|
68
+ unless f.match(/\/application.rb/)
69
+ logger.debug "ActiveMessaging: Loading #{f}" if first
70
+ load f
71
+ end
72
+ end
73
+ end
74
+
75
+ def self.reload_activemessaging
76
+ # this is resetting the messaging.rb
77
+ ActiveMessaging::Gateway.filters = []
78
+ ActiveMessaging::Gateway.named_destinations = {}
79
+ ActiveMessaging::Gateway.processor_groups = {}
80
+
81
+ # now load the config
82
+ load_config
83
+ load_processors(false)
84
+ end
85
+
86
+ def self.load_activemessaging
87
+ load_extensions
88
+ load_config
89
+ load_processors
90
+ end
91
+
92
+ def self.start
93
+ if ActiveMessaging::Gateway.subscriptions.empty?
94
+ err_msg = <<EOM
95
+
96
+ ActiveMessaging Error: No subscriptions.
97
+ If you have no processor classes in app/processors, add them using the command:
98
+ script/generate processor DoSomething"
99
+
100
+ If you have processor classes, make sure they include in the class a call to 'subscribes_to':
101
+ class DoSomethingProcessor < ActiveMessaging::Processor
102
+ subscribes_to :do_something
103
+
104
+ EOM
105
+ puts err_msg
106
+ logger.error err_msg
107
+ exit
108
+ end
109
+
110
+ Gateway.start
111
+ end
112
+
113
+ end
114
+
115
+ #load these once to start with
116
+ ActiveMessaging.load_activemessaging
117
+
118
+
119
+
120
+ # reload these on each request - leveraging Dispatcher semantics for consistency
121
+ begin
122
+ require 'dispatcher' unless defined?(::Dispatcher)
123
+
124
+ # add processors and config to on_prepare if supported (rails 1.2+)
125
+ if ::Dispatcher.respond_to? :to_prepare
126
+ ::Dispatcher.to_prepare :activemessaging do
127
+ ActiveMessaging.reload_activemessaging
128
+ end
129
+ end
130
+ rescue MissingSourceFile => e
131
+ logger.info e.message
132
+ logger.info "Rails not available."
133
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveMessaging
2
+
3
+ # include this module to make a new adapter - will register the adapter w/gateway so an be used in connection config
4
+ module Adapter
5
+
6
+ def self.included(included_by)
7
+ class << included_by
8
+ def register adapter_name
9
+ Gateway.register_adapter adapter_name, self
10
+ end
11
+ end
12
+ end
13
+
14
+ def logger()
15
+ @@logger = ActiveMessaging.logger unless defined?(@@logger)
16
+ @@logger
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,412 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'cgi'
6
+ require 'time'
7
+ require 'activemessaging/adapter'
8
+
9
+ module ActiveMessaging
10
+ module Adapters
11
+ module AmazonSQS
12
+
13
+ class Connection
14
+ include ActiveMessaging::Adapter
15
+
16
+ register :asqs
17
+
18
+ QUEUE_NAME_LENGTH = 1..80
19
+ # MESSAGE_SIZE = 1..(256 * 1024)
20
+ MESSAGE_SIZE = 1..(8 * 1024)
21
+ VISIBILITY_TIMEOUT = 0..(24 * 60 * 60)
22
+ NUMBER_OF_MESSAGES = 1..255
23
+ GET_QUEUE_ATTRIBUTES = ['All', 'ApproximateNumberOfMessages', 'VisibilityTimeout']
24
+ SET_QUEUE_ATTRIBUTES = ['VisibilityTimeout']
25
+
26
+ #configurable params
27
+ attr_accessor :reliable, :reconnectDelay, :access_key_id, :secret_access_key, :aws_version, :content_type, :host, :port, :poll_interval, :cache_queue_list
28
+
29
+ #generic init method needed by a13g
30
+ def initialize cfg
31
+ raise "Must specify a access_key_id" if (cfg[:access_key_id].nil? || cfg[:access_key_id].empty?)
32
+ raise "Must specify a secret_access_key" if (cfg[:secret_access_key].nil? || cfg[:secret_access_key].empty?)
33
+
34
+ @access_key_id=cfg[:access_key_id]
35
+ @secret_access_key=cfg[:secret_access_key]
36
+ @request_expires = cfg[:requestExpires] || 10
37
+ @request_retry_count = cfg[:requestRetryCount] || 5
38
+ @aws_version = cfg[:aws_version] || '2008-01-01'
39
+ @content_type = cfg[:content_type] || 'text/plain'
40
+ @host = cfg[:host] || 'queue.amazonaws.com'
41
+ @port = cfg[:port] || 80
42
+ @protocol = cfg[:protocol] || 'http'
43
+ @poll_interval = cfg[:poll_interval] || 1
44
+ @reconnect_delay = cfg[:reconnectDelay] || 5
45
+ @aws_url="#{@protocol}://#{@host}"
46
+
47
+ @cache_queue_list = cfg[:cache_queue_list].nil? ? true : cfg[:cache_queue_list]
48
+ @reliable = cfg[:reliable].nil? ? true : cfg[:reliable]
49
+
50
+ #initialize the subscriptions and queues
51
+ @subscriptions = {}
52
+ @current_subscription = 0
53
+ queues
54
+ end
55
+
56
+ def disconnect
57
+ #it's an http request - there is no disconnect - ha!
58
+ end
59
+
60
+ # queue_name string, headers hash
61
+ # for sqs, make sure queue exists, if not create, then add to list of polled queues
62
+ def subscribe queue_name, message_headers={}
63
+ # look at the existing queues, create any that are missing
64
+ queue = get_or_create_queue queue_name
65
+ if @subscriptions.has_key? queue.name
66
+ @subscriptions[queue.name].add
67
+ else
68
+ @subscriptions[queue.name] = Subscription.new(queue.name, message_headers)
69
+ end
70
+ end
71
+
72
+ # queue_name string, headers hash
73
+ # for sqs, attempt delete the queues, won't work if not empty, that's ok
74
+ def unsubscribe queue_name, message_headers={}
75
+ if @subscriptions[queue_name]
76
+ @subscriptions[queue_name].remove
77
+ @subscriptions.delete(queue_name) if @subscriptions[queue_name].count <= 0
78
+ end
79
+ end
80
+
81
+ # queue_name string, body string, headers hash
82
+ # send a single message to a queue
83
+ def send queue_name, message_body, message_headers={}
84
+ queue = get_or_create_queue queue_name
85
+ send_messsage queue, message_body
86
+ end
87
+
88
+ # receive a single message from any of the subscribed queues
89
+ # check each queue once, then sleep for poll_interval
90
+ def receive
91
+ raise "No subscriptions to receive messages from." if (@subscriptions.nil? || @subscriptions.empty?)
92
+ start = @current_subscription
93
+ while true
94
+ # puts "calling receive..."
95
+ @current_subscription = ((@current_subscription < @subscriptions.length-1) ? @current_subscription + 1 : 0)
96
+ sleep poll_interval if (@current_subscription == start)
97
+ queue_name = @subscriptions.keys.sort[@current_subscription]
98
+ queue = queues[queue_name]
99
+ subscription = @subscriptions[queue_name]
100
+ unless queue.nil?
101
+ messages = retrieve_messsages queue, 1, subscription.headers[:visibility_timeout]
102
+ return messages[0] unless (messages.nil? or messages.empty? or messages[0].nil?)
103
+ end
104
+ end
105
+ end
106
+
107
+ def received message, headers={}
108
+ begin
109
+ delete_message message
110
+ rescue Object=>exception
111
+ logger.error "Exception in ActiveMessaging::Adapters::AmazonSQS::Connection.received() logged and ignored: "
112
+ logger.error exception
113
+ end
114
+ end
115
+
116
+ def unreceive message, headers={}
117
+ # do nothing; by not deleting the message will eventually become visible again
118
+ return true
119
+ end
120
+
121
+ protected
122
+
123
+ def create_queue(name)
124
+ validate_new_queue name
125
+ response = make_request('CreateQueue', nil, {'QueueName'=>name})
126
+ add_queue response.get_text("//QueueUrl") unless response.nil?
127
+ end
128
+
129
+ def delete_queue queue
130
+ validate_queue queue
131
+ response = make_request('DeleteQueue', "#{queue.queue_url}")
132
+ end
133
+
134
+ def list_queues(queue_name_prefix=nil)
135
+ validate_queue_name queue_name_prefix unless queue_name_prefix.nil?
136
+ params = queue_name_prefix.nil? ? {} : {"QueueNamePrefix"=>queue_name_prefix}
137
+ response = make_request('ListQueues', nil, params)
138
+ response.nil? ? [] : response.nodes("//QueueUrl").collect{ |n| add_queue(n.text) }
139
+ end
140
+
141
+ def get_queue_attributes(queue, attribute='All')
142
+ validate_get_queue_attribute(attribute)
143
+ params = {'AttributeName'=>attribute}
144
+ response = make_request('GetQueueAttributes', "#{queue.queue_url}")
145
+ attributes = {}
146
+ response.each_node('/GetQueueAttributesResponse/GetQueueAttributesResult/Attribute') { |n|
147
+ n = n.elements['Name'].text
148
+ v = n.elements['Value'].text
149
+ attributes[n] = v
150
+ }
151
+ if attribute != 'All'
152
+ attributes[attribute]
153
+ else
154
+ attributes
155
+ end
156
+ end
157
+
158
+ def set_queue_attribute(queue, attribute, value)
159
+ validate_set_queue_attribute(attribute)
160
+ params = {'Attribute.Name'=>attribute, 'Attribute.Value'=>value.to_s}
161
+ response = make_request('SetQueueAttributes', "#{queue.queue_url}", params)
162
+ end
163
+
164
+ def delete_queue queue
165
+ validate_queue queue
166
+ response = make_request('DeleteQueue', "#{queue.queue_url}")
167
+ end
168
+
169
+ # in progress
170
+ def send_messsage queue, message
171
+ validate_queue queue
172
+ validate_message message
173
+ response = make_request('SendMessage', queue.queue_url, {'MessageBody'=>message})
174
+ response.get_text("//MessageId") unless response.nil?
175
+ end
176
+
177
+ def retrieve_messsages queue, num_messages=1, timeout=nil
178
+ validate_queue queue
179
+ validate_number_of_messages num_messages
180
+ validate_timeout timeout if timeout
181
+
182
+ params = {'MaxNumberOfMessages'=>num_messages.to_s}
183
+ params['VisibilityTimeout'] = timeout.to_s if timeout
184
+
185
+ response = make_request('ReceiveMessage', "#{queue.queue_url}", params)
186
+ response.nodes("//Message").collect{ |n| Message.from_element n, response, queue } unless response.nil?
187
+ end
188
+
189
+ def delete_message message
190
+ response = make_request('DeleteMessage', "#{message.queue.queue_url}", {'ReceiptHandle'=>message.receipt_handle})
191
+ end
192
+
193
+ def make_request(action, url=nil, params = {})
194
+ # puts "make_request a=#{action} u=#{url} p=#{params}"
195
+ url ||= @aws_url
196
+
197
+ # Add Actions
198
+ params['Action'] = action
199
+ params['Version'] = @aws_version
200
+ params['AWSAccessKeyId'] = @access_key_id
201
+ params['Expires']= (Time.now + @request_expires).gmtime.iso8601
202
+ params['SignatureVersion'] = '1'
203
+
204
+ # Sign the string
205
+ sorted_params = params.sort_by { |key,value| key.downcase }
206
+ joined_params = sorted_params.collect { |key, value| key.to_s + value.to_s }
207
+ string_to_sign = joined_params.to_s
208
+ digest = OpenSSL::Digest::Digest.new('sha1')
209
+ hmac = OpenSSL::HMAC.digest(digest, @secret_access_key, string_to_sign)
210
+ params['Signature'] = Base64.encode64(hmac).chomp
211
+
212
+ # Construct request
213
+ query_params = params.collect { |key, value| key + "=" + CGI.escape(value.to_s) }.join("&")
214
+
215
+ # Put these together to get the request query string
216
+ request_url = "#{url}?#{query_params}"
217
+ # puts "request_url = #{request_url}"
218
+ request = Net::HTTP::Get.new(request_url)
219
+
220
+ retry_count = 0
221
+ while retry_count < @request_retry_count.to_i
222
+ retry_count = retry_count + 1
223
+ # puts "make_request try retry_count=#{retry_count}"
224
+ begin
225
+ response = SQSResponse.new(http_request(host,port,request))
226
+ check_errors(response)
227
+ return response
228
+ rescue Object=>ex
229
+ # puts "make_request caught #{ex}"
230
+ raise ex unless reliable
231
+ sleep(@reconnect_delay)
232
+ end
233
+ end
234
+ end
235
+
236
+ # I wrap this so I can move to a different client, or easily mock for testing
237
+ def http_request h, p, r
238
+ return Net::HTTP.start(h, p){ |http| http.request(r) }
239
+ end
240
+
241
+ def check_errors(response)
242
+ raise "http response was nil" if (response.nil?)
243
+ raise response.errors if (response && response.errors?)
244
+ response
245
+ end
246
+
247
+ private
248
+
249
+ # internal data structure methods
250
+ def add_queue(url)
251
+ q = Queue.from_url url
252
+ queues[q.name] = q if self.cache_queue_list
253
+ return q
254
+ end
255
+
256
+ def get_or_create_queue queue_name
257
+ qs = queues
258
+ qs.has_key?(queue_name) ? qs[queue_name] : create_queue(queue_name)
259
+ end
260
+
261
+ def queues
262
+ return @queues if (@queues && cache_queue_list)
263
+ @queues = {}
264
+ list_queues.each{|q| @queues[q.name]=q }
265
+ return @queues
266
+ end
267
+
268
+ # validation methods
269
+ def validate_queue_name qn
270
+ raise "Queue name, '#{qn}', must be between #{QUEUE_NAME_LENGTH.min} and #{QUEUE_NAME_LENGTH.max} characters." unless QUEUE_NAME_LENGTH.include?(qn.length)
271
+ raise "Queue name, '#{qn}', must be alphanumeric only." if (qn =~ /[^\w\-\_]/ )
272
+ end
273
+
274
+ def validate_new_queue qn
275
+ validate_queue_name qn
276
+ raise "Queue already exists: #{qn}" if queues.has_key? qn
277
+ end
278
+
279
+ def validate_queue q
280
+ raise "Never heard of queue, can't use it: #{q.name}" unless queues.has_key? q.name
281
+ end
282
+
283
+ def validate_message m
284
+ raise "Message cannot be nil." if m.nil?
285
+ raise "Message length, #{m.length}, must be between #{MESSAGE_SIZE.min} and #{MESSAGE_SIZE.max}." unless MESSAGE_SIZE.include?(m.length)
286
+ end
287
+
288
+ def validate_timeout to
289
+ raise "Timeout, #{to}, must be between #{VISIBILITY_TIMEOUT.min} and #{VISIBILITY_TIMEOUT.max}." unless VISIBILITY_TIMEOUT.include?(to)
290
+ end
291
+
292
+ def validate_get_queue_attribute qa
293
+ raise "Queue Attribute name, #{qa}, not in list of valid attributes to get: #{GET_QUEUE_ATTRIBUTES.to_sentence}." unless GET_QUEUE_ATTRIBUTES.include?(qa)
294
+ end
295
+
296
+ def validate_set_queue_attribute qa
297
+ raise "Queue Attribute name, #{qa}, not in list of valid attributes to set: #{SET_QUEUE_ATTRIBUTES.to_sentence}." unless SET_QUEUE_ATTRIBUTES.include?(qa)
298
+ end
299
+
300
+ def validate_number_of_messages nom
301
+ raise "Number of messages, #{nom}, must be between #{NUMBER_OF_MESSAGES.min} and #{NUMBER_OF_MESSAGES.max}." unless NUMBER_OF_MESSAGES.include?(nom)
302
+ end
303
+
304
+ end
305
+
306
+ class SQSResponse
307
+ attr_accessor :headers, :doc, :http_response
308
+
309
+ def initialize response
310
+ # puts "response.body = #{response.body}"
311
+ @http_response = response
312
+ @headers = response.to_hash()
313
+ @doc = REXML::Document.new(response.body)
314
+ end
315
+
316
+ def message_type
317
+ return doc ? doc.root.name : ''
318
+ end
319
+
320
+ def errors?
321
+ (not http_response.kind_of?(Net::HTTPSuccess)) or (message_type == "ErrorResponse")
322
+ end
323
+
324
+ def errors
325
+ return "HTTP Error: #{http_response.code} : #{http_response.message}" unless http_response.kind_of?(Net::HTTPSuccess)
326
+
327
+ msg = nil
328
+ each_node('//Error') { |n|
329
+ msg ||= ""
330
+ c = n.elements['Code'].text
331
+ m = n.elements['Message'].text
332
+ msg << ", " if msg != ""
333
+ msg << "#{c} : #{m}"
334
+ }
335
+
336
+ return msg
337
+ end
338
+
339
+ def get_text(xpath,default='')
340
+ e = REXML::XPath.first( doc, xpath)
341
+ e.nil? ? default : e.text
342
+ end
343
+
344
+ def each_node(xp)
345
+ REXML::XPath.each(doc.root, xp) {|n| yield n}
346
+ end
347
+
348
+ def nodes(xp)
349
+ doc.elements.to_a(xp)
350
+ end
351
+ end
352
+
353
+ class Subscription
354
+ attr_accessor :name, :headers, :count
355
+
356
+ def initialize(destination, headers={}, count=1)
357
+ @destination, @headers, @count = destination, headers, count
358
+ end
359
+
360
+ def add
361
+ @count += 1
362
+ end
363
+
364
+ def remove
365
+ @count -= 1
366
+ end
367
+
368
+ end
369
+
370
+ class Queue
371
+ attr_accessor :name, :pathinfo, :domain, :visibility_timeout
372
+
373
+ def self.from_url url
374
+ return Queue.new($2,$1) if (url =~ /^http:\/\/(.+)\/([-a-zA-Z0-9_]+)$/)
375
+ raise "Bad Queue URL: #{url}"
376
+ end
377
+
378
+ def queue_url
379
+ "#{pathinfo}/#{name}"
380
+ end
381
+
382
+ def initialize name, domain, vt=nil
383
+ @name, @pathinfo, @domain, @visibility_timeout = name, pathinfo, domain, vt
384
+ end
385
+
386
+ def to_s
387
+ "<AmazonSQS::Queue name='#{name}' url='#{queue_url}' domain='#{domain}'>"
388
+ end
389
+ end
390
+
391
+ # based on stomp message, has pointer to the SQSResponseObject
392
+ class Message
393
+ attr_accessor :headers, :id, :body, :command, :response, :queue, :md5_of_body, :receipt_handle
394
+
395
+ def self.from_element e, response, queue
396
+ Message.new(response.headers, e.elements['MessageId'].text, e.elements['Body'].text, e.elements['MD5OfBody'].text, e.elements['ReceiptHandle'].text, response, queue)
397
+ end
398
+
399
+ def initialize headers, id, body, md5_of_body, receipt_handle, response, queue, command='MESSAGE'
400
+ @headers, @id, @body, @md5_of_body, @receipt_handle, @response, @queue, @command = headers, id, body, md5_of_body, receipt_handle, response, queue, command
401
+ headers['destination'] = queue.name
402
+ end
403
+
404
+
405
+ def to_s
406
+ "<AmazonSQS::Message id='#{id}' body='#{body}' headers='#{headers.inspect}' command='#{command}' response='#{response}'>"
407
+ end
408
+ end
409
+
410
+ end
411
+ end
412
+ end