pauldowman-sqs_accelerator 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,80 @@
1
+ SQS Accelerator
2
+ ===============
3
+
4
+ [http://github.com/pauldowman/sqs_accelerator](http://github.com/pauldowman/sqs_accelerator)
5
+
6
+ _WARNING: this is totally experimental right now! It's just an idea I'm playing with and I'm trying to see if it works. (But please try it out and tell me what you think!)_
7
+
8
+ This is a simple and scalable event-driven server (using [Event Machine](http://eventmachine.rubyforge.org)) that proxies requests to Amazon's [Simple Queue Service](http://aws.amazon.com/sqs/) hosted queue. It's purpose is to queue messages very quickly, because otherwise SQS is too slow to use from within a web app (to be precise, the time it takes to queue a message is often too long).
9
+
10
+ It provides a simple RESTful interface for interacting with SQS that's also convenient to use from a browser.
11
+
12
+ One instance of the SQS Accelerator can send messages to any number of SQS queues, the queue name is given in the URL and the AWS credentials are given in HTTP headers (as HTTP Basic Authentication headers actually so it's convenient to use from a browser)
13
+
14
+
15
+ How it works
16
+ ------------
17
+
18
+ The SQS Accelerator accepts messages from your web app and returns a response instantly without waiting for a response from SQS. Your web app then happily continues and returns it's response to the user. The SQS Accelerator finishes queueing the message to SQS in the background.
19
+
20
+ It runs as a daemon, if it's accepting queued messages from a web app one instance should be run on each app server. It listens on port 9292.
21
+
22
+
23
+ FAQ
24
+ ------
25
+
26
+ __Q: Won't lots of messages build up inside SQS Accelerator since it receives them faster than it sends them to SQS?__
27
+
28
+ A: No, but you might get errors if you hit the maximum number of open connections (not sure what this limit is yet, but it should be very high with EventMachine). SQS has high latency, but it can accept a virtually unlimited number of incoming connections, so if you can hold a large number of open connections to it then your throughput can be very high. Because an evented client can hold a large number of open connections it can maintain a very high throughput.
29
+
30
+ __Q: Isn't this like queueing messages locally before queueing them in SQS?__
31
+
32
+ A: No, it's more like a proxy than a queue, the messages don't build up inside SQS Accelerator, they are all being sent to SQS concurrently.
33
+
34
+ __Q: Why not just use a local queue?__
35
+
36
+ A: You could do that instead of using SQS. But SQS is simple and scalable, and it's also simple and scalable to run an instance of SQS Accelerator on each app server.
37
+
38
+ __Q: Are you sure about all this?__
39
+
40
+ A: Not yet, it's just a theory so far, my next priority is to benchmark this and make sure it really works as it should in theory.
41
+
42
+
43
+ Usage instructions
44
+ ------------------
45
+
46
+ * Install this gem
47
+ * Run sqs_accelerator.ru
48
+ * Hit [http://localhost:9292/](http://localhost:9292/) in a browser
49
+ * Make an HTTP GET request to /queues to list all queues
50
+ * Make an HTTP POST request to /queues with queue_name=newqueue to create a queue named newqueue
51
+ * Make an HTTP GET request to /queues/queuename to show info about a queue named "queuename" and to give a form to send a message
52
+ * Make an HTTP POST request to /queues/queuename to send a message
53
+ * Use your SQS credentials for HTTP auth
54
+ * TODO improve these instructions
55
+
56
+
57
+ Thanks to the following great projects
58
+ --------------------------------------
59
+
60
+ * [Event Machine](http://eventmachine.rubyforge.org), the thing that makes this even possible.
61
+ * [async_sinatra](http://github.com/raggi/async_sinatra), allows writing evented HTTP servers using the nice Sinatra framework/DSL.
62
+ * [em-http-request](http://github.com/igrigorik/em-http-request), an asynchronous HTTP Client.
63
+ * [RightAWS](http://rightscale.rubyforge.org/right_aws_gem_doc) (I stole a few lines of the SQS code).
64
+ * [Amazon SQS](http://aws.amazon.com/sqs/) for having a decent [Query API](http://docs.amazonwebservices.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/), even if it's not actually RESTful. :-)
65
+
66
+
67
+ Still to do
68
+ -----
69
+
70
+ * Find all the TODO comments in the code
71
+ * Benchmarking
72
+ * A command to start and stop the daemon, maybe a [god](http://god.rubyforge.org/) config file
73
+ * Use SSL when connecting to SQS to protect message content (AWS credentials are never sent, they're just used to sign the message)
74
+ * Unit tests (I'm just trying to figure out if this idea even works first)
75
+ * Create a Ruby client library
76
+ * Switch all actions to use evented HTTP client instead of EM.defer. Right now some actions are using EM.defer to use the RightAWS client in a thread. These actions will be less scalable. Sending messages, the most important action, _is_ using the evented client. This means making direct HTTP requests to the [SQS Query API](http://docs.amazonwebservices.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/)
77
+ * Fix bugs and make it nicer.
78
+ * Refactor all the SQS stuff out of the actions
79
+ * Some configuration options
80
+
data/lib/server.rb ADDED
@@ -0,0 +1,166 @@
1
+ require "em-http"
2
+ require "haml"
3
+ require "logger"
4
+ require "pp"
5
+ require "sinatra/async"
6
+ require "xml"
7
+
8
+ require "#{File.dirname(__FILE__)}/sqs_accelerator"
9
+ require "#{File.dirname(__FILE__)}/sqs_helper"
10
+ require "#{File.dirname(__FILE__)}/sqs_proxy"
11
+
12
+ class SqsAccelerator::Server < Sinatra::Base
13
+ register Sinatra::Async
14
+
15
+ configure do
16
+ # TODO fix logging.
17
+ LOGGER = Logger.new(STDOUT)
18
+ LOGGER.level = Logger::DEBUG
19
+ use Rack::CommonLogger, LOGGER
20
+ end
21
+
22
+ # use http basic auth to get aws_access_key_id and aws_secret_access_key
23
+ helpers do
24
+ include SqsAccelerator::SqsHelper
25
+
26
+ def deny
27
+ response['WWW-Authenticate'] = %(Basic realm="SQS Accelerator - use SQS credentials")
28
+ response.status = 401 # "Unauthorized": http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
29
+ body "Please provide the SQS access key id and secret access key as the userid and password.\n"
30
+ end
31
+
32
+ def authorized?
33
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
34
+ @auth.provided? && @auth.basic? && @auth.credentials
35
+ # We don't care what the credentials are, we'll just pass them on to SQS
36
+ end
37
+
38
+ def aws_access_key_id
39
+ @auth.credentials[0]
40
+ end
41
+
42
+ def aws_secret_access_key
43
+ @auth.credentials[1]
44
+ end
45
+
46
+ def logger
47
+ LOGGER
48
+ end
49
+ end
50
+
51
+ aget '/' do
52
+ body haml :home
53
+ end
54
+
55
+ aget '/queues' do
56
+ deny and return unless authorized?
57
+
58
+ request_hash = generate_request_hash('ListQueues')
59
+ http = EventMachine::HttpRequest.new("http://queue.amazonaws.com/").get :query => request_hash, :timeout => 120
60
+
61
+ http.callback {
62
+ queue_urls = []
63
+ doc = parse_response(http.response)
64
+ doc.find('//sqs:QueueUrl').each do |u|
65
+ queue_urls << u.content.strip
66
+ end
67
+
68
+ body haml :all_queues, :locals => { :queue_urls => queue_urls }
69
+ }
70
+
71
+ http.errback {
72
+ # TODO a decent log message and error page here
73
+ logger.debug "fail"
74
+ body "fail"
75
+ }
76
+
77
+ end
78
+
79
+ # Create a queue
80
+ apost '/queues' do
81
+ deny and return unless authorized?
82
+
83
+ queue_name = params[:queue_name]
84
+ visibility_timeout = params[:visibility_timeout]
85
+
86
+ # TODO switch to using evented client
87
+ operation = proc do
88
+ sqs = SqsAccelerator::SqsProxy.new(aws_access_key_id, aws_secret_access_key, :logger => logger)
89
+ sqs.create_queue(queue_name, visibility_timeout)
90
+ # TODO check the result and return an error unless success
91
+ # TODO set Location header field with URI
92
+ response.status = 201 # "Created": http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
93
+ body haml :queue_created, :locals => { :queue_name => queue_name }
94
+ end
95
+ EM.defer(operation)
96
+ end
97
+
98
+ # Get info on a queue. Doesn't create the queue if it doesn't exist
99
+ aget '/queues/:queue_name' do
100
+ deny and return unless authorized?
101
+
102
+ queue_name = params[:queue_name]
103
+
104
+ # TODO switch to using evented client
105
+ operation = proc do
106
+ sqs = SqsAccelerator::SqsProxy.new(aws_access_key_id, aws_secret_access_key, :logger => logger)
107
+ queue_info = sqs.get_queue_info(queue_name)
108
+ body haml :queue_info, :locals => { :queue_name => queue_name, :queue_info => queue_info }
109
+ end
110
+ EM.defer(operation)
111
+ end
112
+
113
+ # Send a new message on a queue. Returns immediately and sends message asynchronously
114
+ apost '/queues/:queue_name' do
115
+ deny and return unless authorized?
116
+
117
+ queue_name = params[:queue_name]
118
+ message_body = params[:message_body]
119
+
120
+ logger.info "Received message for queue #{queue_name}"
121
+ logger.debug "message_body: #{message_body}"
122
+
123
+ # TODO check that chars are allowed by SQS: #x9 | #xA | #xD | [#x20 to #xD7FF] | [#xE000 to #xFFFD] | [#x10000 to #x10FFFF]
124
+ # TODO deal with unicode, where chars != bytes
125
+ if message_body.size > SqsAccelerator::SqsHelper::MAX_MESSAGE_SIZE
126
+ logger.error "Message is too large, rejecting."
127
+ response.status = 413 # "Request Entity Too Large": http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
128
+ body "Message must be less than SqsAccelerator::SqsHelper::MAX_MESSAGE_SIZE bytes"
129
+ return
130
+ end
131
+
132
+ # Messaage seems to be OK, return a response immediately then send message to SQS
133
+ response.status = 202 # "Accepted": http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
134
+ body "Message will be queued, check log for errors.\n"
135
+ # TODO generate and return a message id then log that with the SQS message id?
136
+
137
+ request_hash = generate_request_hash("SendMessage", :message => message_body)
138
+ # build the body string ourselves for now until Ilya's gem gets updated because of the content-length bug
139
+ request_body = request_hash.to_a.collect{|key,val| CGI.escape(key.to_s) + "=" + CGI.escape(val.to_s) }.join("&")
140
+ http = EventMachine::HttpRequest.new("http://queue.amazonaws.com/#{queue_name}").post({:body => request_body, :head => {"Content-Type" => "application/x-www-form-urlencoded"}})
141
+
142
+ http.callback {
143
+ doc = parse_response(http.response)
144
+ id_el = doc.find_first('//sqs:MessageId')
145
+ md5_el = doc.find_first('//sqs:MD5OfMessageBody')
146
+ if id_el && md5_el
147
+ message_id = id_el.content.strip
148
+ checksum = md5_el.content.strip
149
+ # TODO check md5
150
+ # TODO a decent log message here
151
+ logger.info "Queued message, SQS message id is: #{message_id}"
152
+ else
153
+ logger.error "SQS returned an error response"
154
+ # TODO parse the response and print something useful
155
+ # TODO retry a few times with exponentially increasing delay
156
+ end
157
+ }
158
+
159
+ http.errback {
160
+ # TODO a decent log message here
161
+ logger.error "fail"
162
+ # TODO dump the message to a temp file and write a utility to re-send dumped messages
163
+ }
164
+
165
+ end
166
+ end
@@ -0,0 +1,2 @@
1
+ module SqsAccelerator
2
+ end
data/lib/sqs_helper.rb ADDED
@@ -0,0 +1,63 @@
1
+ require "cgi"
2
+ require "base64"
3
+ require "rexml/document"
4
+ require "openssl"
5
+ require "digest/sha1"
6
+ require 'md5'
7
+
8
+ module SqsAccelerator::SqsHelper
9
+ # Thanks to RightScale for the great RightAWS library.
10
+ # Parts of this code have been inspired by/stolen from RightAws.
11
+
12
+ SIGNATURE_VERSION = "1"
13
+ API_VERSION = "2008-01-01"
14
+ DEFAULT_HOST = "queue.amazonaws.com"
15
+ DEFAULT_PORT = 80
16
+ DEFAULT_PROTOCOL = 'http'
17
+ REQUEST_TTL = 30
18
+ DEFAULT_VISIBILITY_TIMEOUT = 30
19
+ MAX_MESSAGE_SIZE = (8 * 1024)
20
+
21
+ @@digest = OpenSSL::Digest::Digest.new("sha1")
22
+
23
+ def sign(aws_secret_access_key, auth_string)
24
+ Base64.encode64(OpenSSL::HMAC.digest(@@digest, aws_secret_access_key, auth_string)).strip
25
+ end
26
+
27
+ # From Amazon's SQS Dev Guide, a brief description of how to escape:
28
+ # "URL encode the computed signature and other query parameters as specified in
29
+ # RFC1738, section 2.2. In addition, because the + character is interpreted as a blank space
30
+ # by Sun Java classes that perform URL decoding, make sure to encode the + character
31
+ # although it is not required by RFC1738."
32
+ # Avoid using CGI::escape to escape URIs.
33
+ # CGI::escape will escape characters in the protocol, host, and port
34
+ # sections of the URI. Only target chars in the query
35
+ # string should be escaped.
36
+ def URLencode(raw)
37
+ e = URI.escape(raw)
38
+ e.gsub(/\+/, "%2b")
39
+ end
40
+
41
+ def generate_request_hash(action, params={})
42
+ message = params[:message]
43
+ params.each{ |key, value| params.delete(key) if (value.nil? || key.is_a?(Symbol)) }
44
+ request_hash = { "Action" => action,
45
+ "Expires" => (Time.now + REQUEST_TTL).utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
46
+ "AWSAccessKeyId" => aws_access_key_id,
47
+ "Version" => API_VERSION,
48
+ "SignatureVersion" => SIGNATURE_VERSION }
49
+ request_hash["MessageBody"] = message if message
50
+ request_hash.merge(params)
51
+ request_data = request_hash.sort{|a,b| (a[0].to_s.downcase)<=>(b[0].to_s.downcase)}.to_s
52
+ request_hash['Signature'] = sign(aws_secret_access_key, request_data)
53
+ logger.debug "request_hash:\n#{request_hash.pretty_inspect}"
54
+ return request_hash
55
+ end
56
+
57
+ def parse_response(string)
58
+ parser = XML::Parser.string(string)
59
+ doc = parser.parse
60
+ doc.root.namespaces.default_prefix = "sqs"
61
+ return doc
62
+ end
63
+ end
data/lib/sqs_proxy.rb ADDED
@@ -0,0 +1,27 @@
1
+ require "right_aws"
2
+
3
+ # This class will be removed. It's just for the operations that haven't been
4
+ # switched to use the evented http client yet.
5
+
6
+ class SqsAccelerator::SqsProxy
7
+
8
+ def initialize(aws_access_key_id, aws_secret_access_key, options = {})
9
+ options = {:multi_thread => true}.merge(options)
10
+ @logger = options[:logger]
11
+ @sqs = RightAws::SqsGen2.new(aws_access_key_id, aws_secret_access_key, options)
12
+ end
13
+
14
+ def create_queue(queue_name, visibility_timeout)
15
+ RightAws::SqsGen2::Queue.create(@sqs, queue_name, true, visibility_timeout)
16
+ end
17
+
18
+ def get_queue_info(queue_name)
19
+ # TODO do something smart if the queue doesn't exist
20
+ queue = @sqs.queue(queue_name, false)
21
+ queue_info = {
22
+ :num_messages => queue.size,
23
+ :visibility_timeout => queue.visibility
24
+ }
25
+ return queue_info
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ spec = Gem::Specification.new do |s|
2
+ s.name = 'sqs_accelerator'
3
+ s.version = '0.0.1'
4
+ s.date = '2009-06-22'
5
+ s.summary = 'SQS Accelerator'
6
+ s.description = "A simple and scalable event-drive server that proxies requests to Amazon's Simple Queue Service to queue messages very quickly."
7
+ s.email = 'paul@pauldowman.com'
8
+ s.homepage = "http://github.com/pauldowman/sqs-accelerator"
9
+ s.has_rdoc = false
10
+ s.authors = ["Paul Dowman"]
11
+ s.add_dependency('eventmachine', '>= 0.12.2')
12
+ s.add_dependency('igrigorik-em-http-request', '>= 0.1.6')
13
+ s.add_dependency('sinatra', '>= 0.9.2')
14
+ s.add_dependency('async_sinatra', '>= 0.1.4')
15
+ s.add_dependency('libxml-ruby', '>= 1.1.3')
16
+ s.rubyforge_project = "sqs-accelerator"
17
+
18
+ # ruby -rpp -e' pp `git ls-files`.split("\n") '
19
+ s.files = ["README.markdown",
20
+ "lib/server.rb",
21
+ "lib/sqs_accelerator.rb",
22
+ "lib/sqs_helper.rb",
23
+ "lib/sqs_proxy.rb",
24
+ "sqs_accelerator.gemspec",
25
+ "sqs_accelerator.ru",
26
+ "views/all_queues.haml",
27
+ "views/home.haml",
28
+ "views/queue_info.haml"]
29
+
30
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rackup -Ilib:../lib -s thin
2
+
3
+ require 'lib/server'
4
+
5
+ run SqsAccelerator::Server.new
@@ -0,0 +1,13 @@
1
+ !!! XML
2
+ !!!
3
+ %html{:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => "en", :lang => "en"}
4
+ %head
5
+ %title
6
+ SQS Queues
7
+ %body
8
+ %h3
9
+ SQS Queues:
10
+
11
+ - queue_urls.each do |url|
12
+ = url
13
+ %br/
data/views/home.haml ADDED
@@ -0,0 +1,26 @@
1
+ !!! XML
2
+ !!!
3
+ %html{:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => "en", :lang => "en"}
4
+ %head
5
+ %title
6
+ SQS Accelerator
7
+ %body
8
+ %h3
9
+ SQS Accelerator
10
+
11
+ %p
12
+ TODO more detail and links here
13
+ %ul
14
+ %li
15
+ Make an HTTP GET request to
16
+ %a{:href => "/queues"}
17
+ \/queues
18
+ to list all queues
19
+ %li
20
+ Make an HTTP POST request to /queues with queue_name=newqueue to create a queue named newqueue
21
+ %li
22
+ Make an HTTP GET request to /queues/queuename to show info about a queue named "queuename" and to give a form to send a message
23
+ %li
24
+ Make an HTTP POST request to /queues/queuename to send a message
25
+ %li
26
+ Use your SQS credentials for HTTP auth
@@ -0,0 +1,31 @@
1
+ !!! XML
2
+ !!!
3
+ %html{:xmlns => "http://www.w3.org/1999/xhtml", 'xml:lang' => "en", :lang => "en"}
4
+ %head
5
+ %title
6
+ SQS Queue name:
7
+ = queue_name
8
+ %body
9
+ %h3
10
+ SQS Queue name:
11
+ = queue_name
12
+
13
+ %p
14
+ Approximate number of messages in queue:
15
+ = queue_info[:num_messages]
16
+
17
+ %p
18
+ Visibility timeout:
19
+ = queue_info[:visibility_timeout]
20
+
21
+ %p
22
+ Queue a new message:
23
+ %form{:method => "POST"}
24
+ %p
25
+ Send message:
26
+ %br/
27
+ %textarea{:name => "message_body", :cols => "80", :rows => "20"}
28
+
29
+ %p
30
+ %input{:type => "submit"}
31
+
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pauldowman-sqs_accelerator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Paul Dowman
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-06-22 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.12.2
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: igrigorik-em-http-request
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.1.6
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: sinatra
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 0.9.2
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: async_sinatra
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.1.4
54
+ version:
55
+ - !ruby/object:Gem::Dependency
56
+ name: libxml-ruby
57
+ type: :runtime
58
+ version_requirement:
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 1.1.3
64
+ version:
65
+ description: A simple and scalable event-drive server that proxies requests to Amazon's Simple Queue Service to queue messages very quickly.
66
+ email: paul@pauldowman.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - README.markdown
75
+ - lib/server.rb
76
+ - lib/sqs_accelerator.rb
77
+ - lib/sqs_helper.rb
78
+ - lib/sqs_proxy.rb
79
+ - sqs_accelerator.gemspec
80
+ - sqs_accelerator.ru
81
+ - views/all_queues.haml
82
+ - views/home.haml
83
+ - views/queue_info.haml
84
+ has_rdoc: false
85
+ homepage: http://github.com/pauldowman/sqs-accelerator
86
+ post_install_message:
87
+ rdoc_options: []
88
+
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: "0"
96
+ version:
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ version:
103
+ requirements: []
104
+
105
+ rubyforge_project: sqs-accelerator
106
+ rubygems_version: 1.2.0
107
+ signing_key:
108
+ specification_version: 2
109
+ summary: SQS Accelerator
110
+ test_files: []
111
+