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.
- 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
|