q3 0.0.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.6.4"
12
+ #gem "rcov", ">= 0"
13
+ end
14
+
15
+ gem 'sinatra'
16
+ gem 'builder'
17
+ gem 'redis'
18
+ gem 'redis-namespace'
data/Gemfile.lock ADDED
@@ -0,0 +1,42 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ builder (3.2.2)
5
+ diff-lcs (1.1.3)
6
+ git (1.2.7)
7
+ jeweler (1.6.4)
8
+ bundler (~> 1.0)
9
+ git (>= 1.2.5)
10
+ rake
11
+ rack (1.5.2)
12
+ rack-protection (1.5.3)
13
+ rack
14
+ rake (10.3.2)
15
+ redis (3.1.0)
16
+ redis-namespace (1.5.0)
17
+ redis (~> 3.0, >= 3.0.4)
18
+ rspec (2.3.0)
19
+ rspec-core (~> 2.3.0)
20
+ rspec-expectations (~> 2.3.0)
21
+ rspec-mocks (~> 2.3.0)
22
+ rspec-core (2.3.1)
23
+ rspec-expectations (2.3.0)
24
+ diff-lcs (~> 1.1.2)
25
+ rspec-mocks (2.3.0)
26
+ sinatra (1.4.5)
27
+ rack (~> 1.4)
28
+ rack-protection (~> 1.4)
29
+ tilt (~> 1.3, >= 1.3.4)
30
+ tilt (1.4.1)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ builder
37
+ bundler (~> 1.0.0)
38
+ jeweler (~> 1.6.4)
39
+ redis
40
+ redis-namespace
41
+ rspec (~> 2.3.0)
42
+ sinatra
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 tily
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Q3
2
+
3
+ ## Summary
4
+
5
+ Simple Amazon SQS compatible API implementation with sinatra and redis.
6
+
7
+ ## Concept
8
+
9
+ * simplicity
10
+ * no authentication, open like wiki
11
+ * no validation, just works as proxy to redis functions
12
+
13
+ ## Usage
14
+
15
+ ### global service
16
+
17
+ You can try q3 easily with global endpoint <http://q3-global.herokuapp.com/>.
18
+
19
+ $ curl "http://q3-global.herokuapp.com/?Action=CreateQueue&QueueName=MyQueue001"
20
+ <?xml version="1.0" encoding="UTF-8"?>
21
+ <CreateQueueResponse>
22
+ <CreateQueueResult>
23
+ <QueueUrl>http://q3-global.herokuapp.com/*/MyQueue001</QueueUrl>
24
+ </CreateQueueResult>
25
+ <ResponseMetadata>
26
+ <RequestId>9bcf7831-59a3-456c-b5fe-e0674f322b79</RequestId>
27
+ </ResponseMetadata>
28
+ </CreateQueueResponse>
29
+
30
+ You can use any Amazon SQS client.
31
+
32
+ require 'aws-sdk'
33
+ q3 = AWS::SQS.new(
34
+ :sqs_endpoint => 'q3-global.herokuapp.com',
35
+ :access_key_id => 'dummy',
36
+ :secret_access_key => 'dummy',
37
+ :use_ssl => false
38
+ )
39
+
40
+ queue = q3.queues.create('MyQueue001')
41
+ queue.send_message('hello!')
42
+ p queue.receive_message.body # => "hello!"
43
+
44
+ ### as rack app
45
+
46
+ Install gem and you can invoke `q3` command to run api.
47
+
48
+ $ gem install q3
49
+ $ ruby bin/q3
50
+ [2014-07-06 19:54:47] INFO WEBrick 1.3.1
51
+ [2014-07-06 19:54:47] INFO ruby 2.0.0 (2014-02-24) [universal.x86_64-darwin13]
52
+ == Sinatra/1.4.5 has taken the stage on 4567 for development with backup from WEBrick
53
+ [2014-07-06 19:54:47] INFO WEBrick::HTTPServer#start: pid=29495 port=4567
54
+
55
+ Or you can integrate q3 into your config.ru like this:
56
+
57
+ require 'q3.rb'
58
+ run Q3
59
+
60
+ ## Actions
61
+
62
+ See for complete action list: [Welcome - Amazon Simple Queue Service](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/Welcome.html).
63
+
64
+ ### implemented
65
+
66
+ * Queue Operation
67
+ * CreateQueue
68
+ * ListQueues
69
+ * GetQueueAttributes
70
+ * GetQueueUrl
71
+ * SetQueueAttributes
72
+ * DeleteQueue
73
+ * Message Operation
74
+ * SendMessage
75
+ * ReceiveMessage
76
+ * ChangeMessageVisibility
77
+ * DeleteMessage
78
+
79
+ ### will be implemented
80
+
81
+ * SendMessageBatch
82
+ * ChangeMessageVisibilityBatch
83
+ * DeleteMessageBatch
84
+ * ListDeadLetterSourceQueues
85
+
86
+ ### will not be implemented
87
+
88
+ * AddPermission
89
+ * RemovePermission
90
+
91
+ ## Data Schema
92
+
93
+ | Key Name | Type | Content | Delete Timing |
94
+ | ------------------------------------------------------- | ---------- | ------------------------------ | --------------------------------------------- |
95
+ | Queues | List | Queue Names | - |
96
+ | Queues:${QueueName} | Hash | Queue Attributes | DeleteQueue action |
97
+ | Queues:${QueueName}:Messages | List | Message Ids | DeleteQueue action |
98
+ | Queues:${QueueName}:Messages:${MessageId} | Hash | Message Attributes | expires due to MessageRetentionPeriod |
99
+ | Queues:${QueueName}:Messages:${MessageId}:Delayed | String | Message Id | expires due to DelaySeconds |
100
+ | Queues:${QueueName}:Messages:${MessageId}:ReceiptHandle | String | ReceiptHandle | expires due to VisibilityTimeout |
101
+ | Queues:${QueueName}:ReceiptHandles:${ReceiptHandle} | String | Message Id | expires due to VisibilityTimeout |
102
+
103
+ ## TODO
104
+
105
+ * https
106
+ * mount on subdirectory
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "q3"
18
+ gem.homepage = "http://github.com/tily/q3"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Simple Amazon SQS compatible API implementation with sinatra and redis}
21
+ gem.description = %Q{Simple Amazon SQS compatible API implementation with sinatra and redis}
22
+ gem.email = "tily05@gmail.com"
23
+ gem.authors = ["tily"]
24
+ # dependencies defined in Gemfile
25
+ gem.executables = ['q3']
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
36
+ spec.pattern = 'spec/**/*_spec.rb'
37
+ spec.rcov = true
38
+ end
39
+
40
+ task :default => :spec
data/bin/q3 ADDED
@@ -0,0 +1,2 @@
1
+ $:.unshift File.dirname(__FILE__) + '/../lib' unless $:.include? File.dirname(__FILE__) + '/../lib'
2
+ (require 'q3') && (Q3.run!)
data/config.ru ADDED
@@ -0,0 +1 @@
1
+ (require './lib/q3.rb') && (run Q3)
data/lib/q3.rb ADDED
@@ -0,0 +1,257 @@
1
+ %w(digest/md5 sinatra/base builder redis redis-namespace).each {|x| require x }
2
+
3
+ class Q3 < Sinatra::Base
4
+ configure do
5
+ enable :logging
6
+ end
7
+
8
+ DEFAULTS = {
9
+ 'VisibilityTimeout' => 30,
10
+ 'MaximumMessageSize' => 262144,
11
+ 'MessageRetentionPeriod' => 345600,
12
+ 'DelaySeconds' => 0,
13
+ 'ReceiveMessageWaitTimeSeconds' => 0,
14
+ }
15
+ CREATE_QUEUE = %w(
16
+ VisibilityTimeout MessageRetentionPeriod MaximumMessageSize DelaySeconds ReceiveMessageWaitTimeSeconds
17
+ )
18
+ SET_QUEUE_ATTRIBUTES = %w(
19
+ DelaySeconds MaximumMessageSize MessageRetentionPeriod Policy ReceiveMessageWaitTimeSeconds
20
+ VisibilityTimeout RedrivePolicy
21
+ )
22
+
23
+ def self.dispatch!
24
+ @paths.each do |path, opts|
25
+ [:get, :post].each do |x|
26
+ send(x, path) do
27
+ if opt = opts.find {|opt| opt[:action] == params['Action'] }
28
+ instance_eval(&opt[:block])
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ def self.action(action, path='/', &block)
36
+ @paths ||= Hash.new {|h, k| h[k] = [] }
37
+ @paths[path] << {action: action, block: block}
38
+ end
39
+
40
+ before do
41
+ logger.info "#{request_id}: request start with path = #{request.path_info}, params = #{params}"
42
+ content_type 'application/xml'
43
+ end
44
+
45
+ after do
46
+ logger.info "#{request_id}: request end with #{response.body}"
47
+ end
48
+
49
+ action('CreateQueue') do
50
+ halt 400, return_error_xml('Sender', 'MissingParameter', '') if params['QueueName'].nil?
51
+ timestamp = now
52
+ hash = CREATE_QUEUE.inject({'CreateTimestamp' => timestamp, 'LastModifiedTimestamp' => timestamp}) do |hash, attribute|
53
+ hash[attribute] = attributes[attribute] || DEFAULTS[attribute]
54
+ hash
55
+ end
56
+ redis.rpush("Queues", params[:QueueName])
57
+ redis.hmset("Queues:#{params[:QueueName]}", *hash.to_a)
58
+ return_xml {|xml| xml.QueueUrl queue_url("#{params[:QueueName]}") }
59
+ end
60
+
61
+ action('ListQueues') do
62
+ return_xml do |xml|
63
+ redis.lrange('Queues', 0, -1).each {|queue_name| xml.QueueUrl queue_url(queue_name) }
64
+ end
65
+ end
66
+
67
+ action('GetQueueUrl') do
68
+ validate_queue_existence
69
+ return_xml {|xml| xml.QueueUrl queue_url(params[:QueueName]) }
70
+ end
71
+
72
+ action('GetQueueAttributes', '/*/:QueueName') do
73
+ validate_queue_existence
74
+ delayed = redis.keys("Queues:#{params[:QueueName]}:Messages:*:Delayed").size
75
+ not_visible = redis.keys("Queues:#{params[:QueueName]}:Messages:*:ReceiptHandle").size
76
+ messages = redis.llen("Queues:#{params[:QueueName]}:Messages") - delayed - not_visible
77
+ return_xml do |xml|
78
+ queue.each do |name, value|
79
+ xml.Attribute { xml.Name name; xml.Value value }
80
+ end
81
+ xml.Attribute { xml.Name 'ApproximateNumberOfMessages' ; xml.Value messages }
82
+ xml.Attribute { xml.Name 'ApproximateNumberOfMessagesNotVisible' ; xml.Value not_visible }
83
+ xml.Attribute { xml.Name 'ApproximateNumberOfMessagesDelayed' ; xml.Value delayed }
84
+ end
85
+ end
86
+
87
+ action('SetQueueAttributes', '/*/:QueueName') do
88
+ validate_queue_existence
89
+ hash = SET_QUEUE_ATTRIBUTES.inject({'LastModifiedTimestamp' => now}) do |hash, attribute|
90
+ hash[attribute] = attributes[attribute] if attributes[attribute]
91
+ hash
92
+ end
93
+ redis.hmset("Queues:#{params[:QueueName]}", hash.to_a.flatten)
94
+ return_xml {}
95
+ end
96
+
97
+ action('DeleteQueue', '/*/:QueueName') do
98
+ validate_queue_existence
99
+ redis.keys("Queues:#{params[:QueueName]}*").each {|key| redis.del(key) }
100
+ redis.lrem("Queues", 0, params[:QueueName])
101
+ return_xml {}
102
+ end
103
+
104
+ action('SendMessage', '/*/:QueueName') do
105
+ validate_queue_existence
106
+ delay_seconds = params['DelaySeconds'] || queue['DelaySeconds']
107
+ message_id = SecureRandom.uuid
108
+ redis.rpush("Queues:#{params[:QueueName]}:Messages", message_id)
109
+ redis.hmset("Queues:#{params[:QueueName]}:Messages:#{message_id}",
110
+ 'MessageId', message_id,
111
+ 'MessageBody', params[:MessageBody],
112
+ 'SenderId', '*',
113
+ 'SentTimestamp', now,
114
+ 'ApproximateReceiveCount', 0
115
+ )
116
+ redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}", queue['MessageRetentionPeriod'])
117
+ if delay_seconds.to_i > 0
118
+ redis.set("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed", message_id)
119
+ redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed", delay_seconds)
120
+ end
121
+ return_xml do |xml|
122
+ xml.MD5OfMessageBody Digest::MD5.hexdigest(params[:MessageBody])
123
+ xml.MessageId message_id
124
+ end
125
+ end
126
+
127
+ action('ReceiveMessage', '/*/:QueueName') do
128
+ validate_queue_existence
129
+ wait_time_seconds = params['WaitTimeSeconds'] || queue['ReceiveMessageWaitTimeSeconds']
130
+ max_number_of_messages = params['MaxNumberOfMessages'] ? params['MaxNumberOfMessages'].to_i : 1
131
+ visibility_timeout = params['VisibilityTimeout'] || queue['VisibilityTimeout']
132
+ visible_messages = []
133
+ message_ids = redis.lrange("Queues:#{params[:QueueName]}:Messages", 0, -1)
134
+ message_ids.each do |message_id|
135
+ next if redis.exists("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle")
136
+ next if redis.exists("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed")
137
+ message = redis.hgetall("Queues:#{params[:QueueName]}:Messages:#{message_id}")
138
+ message['ApproximateFirstReceiveTimestamp'] ||= now
139
+ message['ApproximateReceiveCount'] = (message['ApproximateReceiveCount'].to_i + 1).to_s
140
+ redis.hmset("Queues:#{params[:QueueName]}:Messages:#{message_id}", message.to_a.flatten)
141
+ receipt_handle = SecureRandom.uuid
142
+ redis.set("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", receipt_handle)
143
+ redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", visibility_timeout)
144
+ redis.set("Queues:#{params[:QueueName]}:ReceiptHandles:#{receipt_handle}", message_id)
145
+ redis.expire("Queues:#{params[:QueueName]}:ReceiptHandles:#{receipt_handle}", visibility_timeout)
146
+ visible_messages << {:MessageId => message_id, :MessageBody => message['MessageBody'], :ReceiptHandle => receipt_handle, :Attributes => message}
147
+ break if visible_messages.size >= max_number_of_messages
148
+ end
149
+ return_xml do |xml|
150
+ visible_messages.each do |message|
151
+ xml.Message do
152
+ xml.MessageId message[:MessageId]
153
+ xml.ReceiptHandle message[:ReceiptHandle]
154
+ xml.MD5OfBody Digest::MD5.hexdigest(message[:MessageBody])
155
+ xml.Body message[:MessageBody]
156
+ %w(SenderId SentTimestamp ApproximateFirstReceiveTimestamp ApproximateReceiveCount).each do |name|
157
+ xml.Attribute { xml.Name name; xml.Value message[:Attributes][name] }
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ action('ChangeMessageVisibility', '/*/:QueueName') do
165
+ validate_queue_existence
166
+ message_id = redis.get("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
167
+ redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", params['VisibilityTimeout'])
168
+ redis.expire("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}", params['VisibilityTimeout'])
169
+ return_xml {}
170
+ end
171
+
172
+ action('DeleteMessage', '/*/:QueueName') do
173
+ validate_queue_existence
174
+ message_id = redis.get("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
175
+ redis.lrem("Queues:#{params[:QueueName]}:Messages", 0, message_id)
176
+ redis.del("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
177
+ return_xml {}
178
+ end
179
+
180
+ action('SendMessageBatch', '/*/:QueueName') do
181
+ validate_queue_existence
182
+ # not implemented yet
183
+ end
184
+
185
+ action('ChangeMessageVisibilityBatch', '/*/:QueueName') do
186
+ validate_queue_existence
187
+ # not implemented yet
188
+ end
189
+
190
+ action('DeleteMessageBatch', '/*/:QueueName') do
191
+ validate_queue_existence
192
+ # not implemented yet
193
+ end
194
+
195
+ helpers do
196
+ def redis
197
+ @redis ||= Redis::Namespace.new(:Q3, redis: Redis.new(url: ENV['REDISTOGO_URL'] || 'redis://localhost:6379/15'))
198
+ end
199
+
200
+ def request_id
201
+ @request_id ||= SecureRandom.uuid
202
+ end
203
+
204
+ def queue
205
+ @queue ||= redis.hgetall("Queues:#{params[:QueueName]}")
206
+ end
207
+
208
+ def queue_url(queue_name)
209
+ "http://#{request.host}/*/#{queue_name}"
210
+ end
211
+
212
+ def attributes
213
+ @attributes ||= (1..10).to_a.inject({}) do |attributes, i|
214
+ if (name = params["Attribute.#{i.to_s}.Name"]) && (value = params["Attribute.#{i.to_s}.Value"])
215
+ attributes[name] = value
216
+ end
217
+ attributes
218
+ end
219
+ end
220
+
221
+ def return_xml(&block)
222
+ builder do |xml|
223
+ xml.instruct!
224
+ xml.tag!("#{params['Action']}Response") do
225
+ xml.tag!("#{params['Action']}Result") do
226
+ block.call(xml)
227
+ end
228
+ xml.ResponseMetadata { xml.RequestId request_id }
229
+ end
230
+ end
231
+ end
232
+
233
+ def return_error_xml(type, code, message)
234
+ builder do |xml|
235
+ xml.instruct!
236
+ xml.ErrorResponse do
237
+ xml.Error do
238
+ xml.Type type
239
+ xml.Code code
240
+ xml.Message message
241
+ end
242
+ xml.ResponseMetadata { xml.RequestId request_id }
243
+ end
244
+ end
245
+ end
246
+
247
+ def validate_queue_existence
248
+ halt 400, return_error_xml('Sender', 'NonExistentQueue', 'The specified queue does not exist for this wsdl version.') if queue.empty?
249
+ end
250
+
251
+ def now
252
+ (Time.now.to_f * 1000.0).to_i
253
+ end
254
+ end
255
+
256
+ dispatch!
257
+ end
data/spec/q3_spec.rb ADDED
@@ -0,0 +1,341 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Q3" do
4
+ before do
5
+ client.list_queues[:queue_urls].each do |queue_url|
6
+ client.delete_queue(:queue_url => queue_url)
7
+ end
8
+ end
9
+
10
+ after do
11
+ client.list_queues[:queue_urls].each do |queue_url|
12
+ client.delete_queue(:queue_url => queue_url)
13
+ end
14
+ end
15
+
16
+ context 'Actions' do
17
+ context 'Queue' do
18
+ context "CreateQueue" do
19
+ it 'should create queue' do
20
+ client.create_queue(:queue_name => 'myqueue001')
21
+ expect(client.list_queues[:queue_urls]).to include('http://localhost/*/myqueue001')
22
+ end
23
+ end
24
+
25
+ context "ListQueues" do
26
+ it 'should list queues' do
27
+ 1.upto(5) do |i|
28
+ client.create_queue(:queue_name => "myqueue00#{i}")
29
+ end
30
+ expect(client.list_queues[:queue_urls]).to eq %w(
31
+ http://localhost/*/myqueue001
32
+ http://localhost/*/myqueue002
33
+ http://localhost/*/myqueue003
34
+ http://localhost/*/myqueue004
35
+ http://localhost/*/myqueue005
36
+ )
37
+ end
38
+ end
39
+
40
+ context "GetQueueUrl" do
41
+ it 'should get queue url' do
42
+ client.create_queue(queue_name: 'myqueue001')
43
+ res = client.get_queue_url(queue_name: 'myqueue001')
44
+ expect(res[:queue_url]).to eq('http://localhost/*/myqueue001')
45
+ end
46
+
47
+ it 'should raise error when non existent queue is specified' do
48
+ expect {
49
+ client.get_queue_url(queue_name: 'myqueue002')
50
+ }.to raise_error(AWS::SQS::Errors::NonExistentQueue)
51
+ end
52
+ end
53
+
54
+ context "GetQueueAttributes" do
55
+ it 'should get queue attributes' do
56
+ now = (Time.now.to_f * 1000.0).to_i
57
+ client.create_queue(queue_name: 'myqueue001')
58
+ res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
59
+ expect(res[:attributes]["CreateTimestamp"].to_i).to be_within(5000).of(now)
60
+ expect(res[:attributes]["LastModifiedTimestamp"].to_i).to be_within(5000).of(now)
61
+ expect(res[:attributes]["VisibilityTimeout"]).to eq("30")
62
+ expect(res[:attributes]["MessageRetentionPeriod"]).to eq("345600")
63
+ expect(res[:attributes]["MaximumMessageSize"]).to eq("262144")
64
+ expect(res[:attributes]["DelaySeconds"]).to eq("0")
65
+ expect(res[:attributes]["ReceiveMessageWaitTimeSeconds"]).to eq("0")
66
+ expect(res[:attributes]["ApproximateNumberOfMessages"]).to eq("0")
67
+ expect(res[:attributes]["ApproximateNumberOfMessagesNotVisible"]).to eq("0")
68
+ expect(res[:attributes]["ApproximateNumberOfMessagesDelayed"]).to eq("0")
69
+ end
70
+
71
+ it 'should get updated LastModifiedTimestamp' do
72
+ past = (Time.now.to_f * 1000.0).to_i
73
+ queue = q3.queues.create('myqueue001')
74
+ sleep 3
75
+ now = (Time.now.to_f * 1000.0).to_i
76
+ queue.visibility_timeout = 1
77
+ expect(queue.last_modified_timestamp.to_i).not_to be_within(2000).of(past)
78
+ expect(queue.last_modified_timestamp.to_i).to be_within(2000).of(now)
79
+ end
80
+
81
+ it 'should get precise ApproximateNumberOfMessages*' do
82
+ queue = q3.queues.create('myqueue001')
83
+
84
+ queue.send_message('hello', delay_seconds: 5)
85
+ queue.send_message('hello')
86
+ queue.send_message('hello')
87
+ queue.receive_message
88
+
89
+ res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
90
+ expect(res[:attributes]["ApproximateNumberOfMessages"]).to eq("1")
91
+ expect(res[:attributes]["ApproximateNumberOfMessagesNotVisible"]).to eq("1")
92
+ expect(res[:attributes]["ApproximateNumberOfMessagesDelayed"]).to eq("1")
93
+ end
94
+ end
95
+
96
+ context "SetQueueAttributes" do
97
+ it 'should set queue attributes' do
98
+ client.create_queue(queue_name: 'myqueue001')
99
+ client.set_queue_attributes(
100
+ queue_url: 'http://localhost/*/myqueue001',
101
+ attributes: {
102
+ 'VisibilityTimeout' => '1',
103
+ 'MessageRetentionPeriod' => '2',
104
+ 'MaximumMessageSize' => '3',
105
+ 'DelaySeconds' => '4',
106
+ 'ReceiveMessageWaitTimeSeconds' => '5',
107
+ }
108
+ )
109
+ res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
110
+ expect(res[:attributes]["VisibilityTimeout"]).to eq("1")
111
+ expect(res[:attributes]["MessageRetentionPeriod"]).to eq("2")
112
+ expect(res[:attributes]["MaximumMessageSize"]).to eq("3")
113
+ expect(res[:attributes]["DelaySeconds"]).to eq("4")
114
+ expect(res[:attributes]["ReceiveMessageWaitTimeSeconds"]).to eq("5")
115
+ end
116
+ end
117
+
118
+ context "DeleteQueue" do
119
+ it 'should delete queue' do
120
+ client.create_queue(queue_name: 'myqueue001')
121
+ client.delete_queue(queue_url: 'http://localhost/*/myqueue001')
122
+ expect(client.list_queues[:queue_urls]).not_to include('http://localhost/*/myqueue001')
123
+ end
124
+ end
125
+ end
126
+
127
+ context 'Message' do
128
+ context "SendMessage" do
129
+ it 'should send message' do
130
+ client.create_queue(queue_name: 'myqueue001')
131
+ client.send_message(
132
+ queue_url: 'http://localhost/*/myqueue001',
133
+ message_body: 'hello'
134
+ )
135
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
136
+ expect(res[:messages].first[:body]).to eq('hello')
137
+ end
138
+ end
139
+
140
+ context "ReceiveMessage" do
141
+ it 'should receive message' do
142
+ client.create_queue(queue_name: 'myqueue001')
143
+ client.send_message(
144
+ queue_url: 'http://localhost/*/myqueue001',
145
+ message_body: 'hello'
146
+ )
147
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
148
+ expect(res[:messages].first[:body]).to eq('hello')
149
+ end
150
+
151
+ it 'should receive message attributes' do
152
+ queue = q3.queues.create('myqueue001')
153
+ sent_at = Time.now.to_i
154
+ queue.send_message('hello')
155
+
156
+ sleep 3
157
+ now = Time.now.to_i
158
+ message = queue.receive_message(visibility_timeout: 1)
159
+ expect(message.sender_id).to eq('*')
160
+ expect(message.sent_timestamp.to_i).to be_within(1).of(sent_at)
161
+ expect(message.approximate_first_receive_timestamp.to_i).to be_within(1).of(now)
162
+ expect(message.approximate_receive_count.to_i).to eq(1)
163
+
164
+ sleep 2
165
+ message = queue.receive_message(visibility_timeout: 1)
166
+ expect(message.approximate_receive_count.to_i).to eq(2)
167
+ end
168
+
169
+ it 'should receive message attributes' do
170
+ queue = q3.queues.create('myqueue001')
171
+ 1.upto(10) do |i|
172
+ queue.send_message("hello #{i}")
173
+ end
174
+ res = q3.client.receive_message(
175
+ queue_url: 'http://localhost/*/myqueue001',
176
+ max_number_of_messages: 5
177
+ )
178
+ expect(res[:messages].size).to eq(5)
179
+ end
180
+ end
181
+
182
+ context "ChangeMessageVisibility" do
183
+ it 'should change message visibility' do
184
+ client.create_queue(queue_name: 'myqueue001')
185
+ client.send_message(
186
+ queue_url: 'http://localhost/*/myqueue001',
187
+ message_body: 'hello'
188
+ )
189
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
190
+ receipt_handle = res[:messages].first[:receipt_handle]
191
+ client.change_message_visibility(
192
+ queue_url: 'http://localhost/*/myqueue001',
193
+ receipt_handle: receipt_handle,
194
+ visibility_timeout: 3,
195
+ )
196
+ sleep 5
197
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
198
+ expect(res[:messages].first[:body]).to eq('hello')
199
+ end
200
+ end
201
+
202
+ context "DeleteMessage" do
203
+ it 'should delete message' do
204
+ client.create_queue(queue_name: 'myqueue001')
205
+ client.send_message(
206
+ queue_url: 'http://localhost/*/myqueue001',
207
+ message_body: 'hello'
208
+ )
209
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
210
+ receipt_handle = res[:messages].first[:receipt_handle]
211
+ client.delete_message(
212
+ queue_url: 'http://localhost/*/myqueue001',
213
+ receipt_handle: receipt_handle
214
+ )
215
+ res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
216
+ expect(res[:messages]).to be_empty
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ context 'Features' do
223
+ context 'Message Extention Period' do
224
+ it 'with CreateQueue MessageExtentionPeriod' do
225
+ queue = q3.queues.create('myqueue001', :message_retention_period => 3)
226
+
227
+ queue.send_message('hello')
228
+ message = queue.receive_message
229
+ expect(message.body).to eq('hello')
230
+
231
+ queue.send_message('hello')
232
+ sleep 4
233
+ message = queue.receive_message
234
+ expect(message).to be_nil
235
+ end
236
+
237
+ it 'with SetQueueAttributes MessageExtentionPeriod' do
238
+ queue = q3.queues.create('myqueue001')
239
+ queue.message_retention_period = 3
240
+
241
+ queue.send_message('hello')
242
+ message = queue.receive_message
243
+ expect(message.body).to eq('hello')
244
+
245
+ queue.send_message('hello')
246
+ sleep 4
247
+ message = queue.receive_message
248
+ expect(message).to be_nil
249
+ end
250
+ end
251
+
252
+ context 'First In First Out' do
253
+ it 'First in first out' do
254
+ queue = q3.queues.create('myqueue001', :visibility_timeout => 3)
255
+
256
+ 1.upto(10) do |i|
257
+ queue.send_message("hello #{i}")
258
+ end
259
+
260
+ 1.upto(10) do |i|
261
+ message = queue.receive_message
262
+ expect(message.body).to eq("hello #{i}")
263
+ end
264
+ end
265
+ end
266
+
267
+ context 'Long Polling' do
268
+ pending 'not implemented yet'
269
+ end
270
+
271
+ context 'Visibility Timeout' do
272
+ it 'with CreateQueue VisibilityTimeout' do
273
+ queue = q3.queues.create('myqueue001', :visibility_timeout => 3)
274
+
275
+ queue.send_message('hello')
276
+ message = queue.receive_message
277
+ expect(message.body).to eq('hello')
278
+
279
+ message = queue.receive_message
280
+ expect(message).to be_nil
281
+
282
+ sleep 4
283
+ message = queue.receive_message
284
+ expect(message.body).to eq('hello')
285
+ end
286
+
287
+ it 'with SetQueueAttributes VisibilityTimeout' do
288
+ queue = q3.queues.create('myqueue001')
289
+ queue.visibility_timeout = 3
290
+
291
+ queue.send_message('hello')
292
+ message = queue.receive_message
293
+ expect(message.body).to eq('hello')
294
+
295
+ message = queue.receive_message
296
+ expect(message).to be_nil
297
+
298
+ sleep 4
299
+ message = queue.receive_message
300
+ expect(message.body).to eq('hello')
301
+ end
302
+
303
+ it 'with ReceiveMessage VisibilityTimeout' do
304
+ queue = q3.queues.create('myqueue001')
305
+
306
+ queue.send_message('hello')
307
+ message = queue.receive_message(visibility_timeout: 3)
308
+ expect(message.body).to eq('hello')
309
+
310
+ message = queue.receive_message
311
+ expect(message).to be_nil
312
+
313
+ sleep 4
314
+ message = queue.receive_message
315
+ expect(message.body).to eq('hello')
316
+ end
317
+ end
318
+
319
+ context 'Delayed Message' do
320
+ it 'with CreateQueue DelaySeconds' do
321
+ queue = q3.queues.create('myqueue001', :delay_seconds => 3)
322
+ queue.send_message('hello')
323
+ message = queue.receive_message
324
+ expect(message).to be_nil
325
+ sleep 4
326
+ message = queue.receive_message
327
+ expect(message.body).to eq('hello')
328
+ end
329
+
330
+ it 'with SendMessage DelaySeconds' do
331
+ queue = q3.queues.create('myqueue001')
332
+ queue.send_message('hello', :delay_seconds => 3)
333
+ message = queue.receive_message
334
+ expect(message).to be_nil
335
+ sleep 4
336
+ message = queue.receive_message
337
+ expect(message.body).to eq('hello')
338
+ end
339
+ end
340
+ end
341
+ end
@@ -0,0 +1,32 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'q3'
5
+ require 'aws-sdk'
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
+
11
+ RSpec.configure do |config|
12
+
13
+ end
14
+
15
+ #AWS.config(
16
+ # logger: Logger.new($stdout),
17
+ # log_level: :debug,
18
+ # http_wire_trace: true
19
+ #)
20
+
21
+ def q3
22
+ @q3 ||= AWS::SQS.new(
23
+ :sqs_endpoint => 'localhost',
24
+ :access_key_id => 'dummy',
25
+ :secret_access_key => 'dummy',
26
+ :use_ssl => false
27
+ )
28
+ end
29
+
30
+ def client
31
+ q3.client
32
+ end
metadata ADDED
@@ -0,0 +1,175 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: q3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - tily
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sinatra
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: builder
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: redis
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: redis-namespace
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 2.3.0
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 2.3.0
94
+ - !ruby/object:Gem::Dependency
95
+ name: bundler
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 1.0.0
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 1.0.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: jeweler
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: 1.6.4
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ version: 1.6.4
126
+ description: Simple Amazon SQS compatible API implementation with sinatra and redis
127
+ email: tily05@gmail.com
128
+ executables:
129
+ - q3
130
+ extensions: []
131
+ extra_rdoc_files:
132
+ - LICENSE.txt
133
+ - README.md
134
+ files:
135
+ - .document
136
+ - .rspec
137
+ - Gemfile
138
+ - Gemfile.lock
139
+ - LICENSE.txt
140
+ - README.md
141
+ - Rakefile
142
+ - bin/q3
143
+ - config.ru
144
+ - lib/q3.rb
145
+ - spec/q3_spec.rb
146
+ - spec/spec_helper.rb
147
+ homepage: http://github.com/tily/q3
148
+ licenses:
149
+ - MIT
150
+ post_install_message:
151
+ rdoc_options: []
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ! '>='
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ segments:
161
+ - 0
162
+ hash: 1071069083813068665
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ! '>='
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubyforge_project:
171
+ rubygems_version: 1.8.23
172
+ signing_key:
173
+ specification_version: 3
174
+ summary: Simple Amazon SQS compatible API implementation with sinatra and redis
175
+ test_files: []