activemessaging 0.6.0

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