activemessaging 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +50 -0
- data/generators/a13g_test_harness/a13g_test_harness_generator.rb +19 -0
- data/generators/a13g_test_harness/templates/active_messaging_test.rhtml +13 -0
- data/generators/a13g_test_harness/templates/active_messaging_test_controller.rb +29 -0
- data/generators/a13g_test_harness/templates/index.rhtml +17 -0
- data/generators/filter/USAGE +0 -0
- data/generators/filter/filter_generator.rb +19 -0
- data/generators/filter/templates/filter.rb +12 -0
- data/generators/filter/templates/filter_test.rb +28 -0
- data/generators/processor/USAGE +8 -0
- data/generators/processor/processor_generator.rb +31 -0
- data/generators/processor/templates/application.rb +18 -0
- data/generators/processor/templates/broker.yml +79 -0
- data/generators/processor/templates/jruby_poller +117 -0
- data/generators/processor/templates/messaging.rb +12 -0
- data/generators/processor/templates/poller +23 -0
- data/generators/processor/templates/poller.rb +23 -0
- data/generators/processor/templates/processor.rb +8 -0
- data/generators/processor/templates/processor_test.rb +20 -0
- data/generators/tracer/USAGE +8 -0
- data/generators/tracer/templates/controller.rb +14 -0
- data/generators/tracer/templates/helper.rb +2 -0
- data/generators/tracer/templates/index.rhtml +4 -0
- data/generators/tracer/templates/layout.rhtml +16 -0
- data/generators/tracer/templates/trace_processor.rb +100 -0
- data/generators/tracer/tracer_generator.rb +25 -0
- data/lib/activemessaging.rb +133 -0
- data/lib/activemessaging/adapter.rb +21 -0
- data/lib/activemessaging/adapters/asqs.rb +412 -0
- data/lib/activemessaging/adapters/base.rb +82 -0
- data/lib/activemessaging/adapters/jms.rb +237 -0
- data/lib/activemessaging/adapters/reliable_msg.rb +190 -0
- data/lib/activemessaging/adapters/stomp.rb +99 -0
- data/lib/activemessaging/adapters/test.rb +155 -0
- data/lib/activemessaging/adapters/wmq.rb +202 -0
- data/lib/activemessaging/filter.rb +29 -0
- data/lib/activemessaging/gateway.rb +422 -0
- data/lib/activemessaging/message_sender.rb +30 -0
- data/lib/activemessaging/named_base.rb +54 -0
- data/lib/activemessaging/processor.rb +45 -0
- data/lib/activemessaging/support.rb +17 -0
- data/lib/activemessaging/test_helper.rb +194 -0
- data/lib/activemessaging/trace_filter.rb +34 -0
- data/messaging.rb.example +5 -0
- data/tasks/start_consumers.rake +8 -0
- 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,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,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,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
|