lsqs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +54 -0
- data/Rakefile +1 -0
- data/bin/console +6 -0
- data/bin/lsqs-server +4 -0
- data/config/template.xml.liquid +9 -0
- data/config.ru +4 -0
- data/lib/lsqs/action_router.rb +35 -0
- data/lib/lsqs/actions/base.rb +51 -0
- data/lib/lsqs/actions/change_message_visibility.rb +23 -0
- data/lib/lsqs/actions/create_queue.rb +22 -0
- data/lib/lsqs/actions/delete_message.rb +22 -0
- data/lib/lsqs/actions/delete_message_batch.rb +36 -0
- data/lib/lsqs/actions/delete_queue.rb +18 -0
- data/lib/lsqs/actions/get_queue_url.rb +23 -0
- data/lib/lsqs/actions/list_queues.rb +22 -0
- data/lib/lsqs/actions/purge_queue.rb +19 -0
- data/lib/lsqs/actions/receive_message.rb +27 -0
- data/lib/lsqs/actions/send_message.rb +21 -0
- data/lib/lsqs/actions/send_message_batch.rb +42 -0
- data/lib/lsqs/error_handler.rb +65 -0
- data/lib/lsqs/message.rb +49 -0
- data/lib/lsqs/queue.rb +176 -0
- data/lib/lsqs/queue_list.rb +90 -0
- data/lib/lsqs/server.rb +59 -0
- data/lib/lsqs/version.rb +3 -0
- data/lib/lsqs/xml_template.rb +54 -0
- data/lib/lsqs.rb +45 -0
- data/lsqs.gemspec +31 -0
- data/spec/lsqs/action_router_spec.rb +21 -0
- data/spec/lsqs/aws_sdk_actions_spec.rb +346 -0
- data/spec/lsqs/message_spec.rb +61 -0
- data/spec/lsqs/queue_list_spec.rb +99 -0
- data/spec/lsqs/queue_spec.rb +192 -0
- data/spec/lsqs/xml_template_spec.rb +64 -0
- data/spec/spec_helper.rb +24 -0
- metadata +232 -0
data/lib/lsqs/queue.rb
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
module LSQS
|
2
|
+
class Queue
|
3
|
+
attr_accessor :name, :messages, :in_flight, :attributes, :monitor
|
4
|
+
|
5
|
+
DEFAULT_TIMEOUT = 30
|
6
|
+
|
7
|
+
def initialize(name, params = {})
|
8
|
+
@name = name
|
9
|
+
@attributes = params.fetch('Attributes'){Hash.new}
|
10
|
+
@monitor = Monitor.new
|
11
|
+
@messages = []
|
12
|
+
@in_flight = {}
|
13
|
+
@timeout = true
|
14
|
+
|
15
|
+
check_timeout
|
16
|
+
end
|
17
|
+
|
18
|
+
##
|
19
|
+
# Sets the default timeout of a queue. It takes the value from the
|
20
|
+
# attributes, if it is set, otherwise it uses the `DEFAULT_TIMEOUT`
|
21
|
+
# constant.
|
22
|
+
#
|
23
|
+
# @return [Fixnum]
|
24
|
+
#
|
25
|
+
def visibility_timeout
|
26
|
+
attributes['VisibilityTimeout'] || DEFAULT_TIMEOUT
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Creates a new message in the queue.
|
31
|
+
#
|
32
|
+
# @param [Hash] options
|
33
|
+
#
|
34
|
+
# @return [Message]
|
35
|
+
#
|
36
|
+
def create_message(options = {})
|
37
|
+
lock do
|
38
|
+
message = Message.new(options)
|
39
|
+
@messages << message
|
40
|
+
return message
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# Gets a number of messages based on the MaxNumberOfMessages field.
|
46
|
+
#
|
47
|
+
# @param [Hash] options
|
48
|
+
#
|
49
|
+
# @return [Hash]
|
50
|
+
#
|
51
|
+
def get_messages(options = {})
|
52
|
+
number_of_messages = options.fetch('MaxNumberOfMessages'){1}.to_i
|
53
|
+
|
54
|
+
raise 'ReadCountOutOfRange' if number_of_messages > 10
|
55
|
+
|
56
|
+
result = {}
|
57
|
+
|
58
|
+
lock do
|
59
|
+
amount = number_of_messages > size ? size : number_of_messages
|
60
|
+
|
61
|
+
amount.times do
|
62
|
+
message = messages.delete_at(rand(size))
|
63
|
+
message.expire_in(visibility_timeout)
|
64
|
+
receipt = generate_receipt
|
65
|
+
@in_flight[receipt] = message
|
66
|
+
result[receipt] = message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
return result
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Deletes a message from the messages that are in-flight.
|
75
|
+
#
|
76
|
+
# @param [String] receipt
|
77
|
+
#
|
78
|
+
def delete_message(receipt)
|
79
|
+
lock do
|
80
|
+
in_flight.delete(receipt)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Deletes all messages in queue and in flight.
|
86
|
+
#
|
87
|
+
def purge
|
88
|
+
lock do
|
89
|
+
@messages = []
|
90
|
+
@in_flight = {}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Returns the amount of messages in the queue.
|
96
|
+
#
|
97
|
+
# @return [Fixnum]
|
98
|
+
#
|
99
|
+
def size
|
100
|
+
messages.size
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Generates a hex receipt for the message
|
105
|
+
#
|
106
|
+
# @return [String]
|
107
|
+
#
|
108
|
+
def generate_receipt
|
109
|
+
SecureRandom.hex(16)
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Change the visibility of a message that is in flight. If visibility
|
114
|
+
# is set to 0, put back in the queue.
|
115
|
+
#
|
116
|
+
# @param [String] receipt
|
117
|
+
# @param [Fixnum] seconds
|
118
|
+
#
|
119
|
+
def change_message_visibility(receipt, seconds)
|
120
|
+
lock do
|
121
|
+
message = @in_flight[receipt]
|
122
|
+
raise 'MessageNotInflight' unless message
|
123
|
+
|
124
|
+
if seconds == 0
|
125
|
+
message.expire
|
126
|
+
@messages << message
|
127
|
+
delete_message(receipt)
|
128
|
+
else
|
129
|
+
message.expire_in(seconds)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
##
|
135
|
+
# Checks if in-fligh messages need to be put back in the queue.
|
136
|
+
#
|
137
|
+
def timeout_messages
|
138
|
+
lock do
|
139
|
+
in_flight.each do |key, message|
|
140
|
+
if message.expired?
|
141
|
+
message.expire
|
142
|
+
@messages << message
|
143
|
+
delete_message(key)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
protected
|
150
|
+
|
151
|
+
##
|
152
|
+
# Initializes a thread that checks every 5 seconds if messages need
|
153
|
+
# to be put back from flight to the message queue.
|
154
|
+
#
|
155
|
+
def check_timeout
|
156
|
+
Thread.new do
|
157
|
+
while @timeout
|
158
|
+
unless in_flight.empty?
|
159
|
+
timeout_messages
|
160
|
+
end
|
161
|
+
sleep(5)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Locks a block, in order to ensure that there is no conflict if more than
|
168
|
+
# one processes try to access the object.
|
169
|
+
#
|
170
|
+
def lock
|
171
|
+
@monitor.synchronize do
|
172
|
+
yield
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end # Queue
|
176
|
+
end # LSQS
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module LSQS
|
2
|
+
class QueueList
|
3
|
+
include MonitorMixin
|
4
|
+
|
5
|
+
attr_accessor :queues
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
@queues = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Purges the current queue list.
|
14
|
+
#
|
15
|
+
# @return [Hash]
|
16
|
+
#
|
17
|
+
def purge
|
18
|
+
@queues = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
##
|
22
|
+
# Creates a new queue, if it doesn't exist already.
|
23
|
+
#
|
24
|
+
# @param [String] name
|
25
|
+
# @param [Hash] options
|
26
|
+
#
|
27
|
+
# @return [LSQS::Queue]
|
28
|
+
#
|
29
|
+
def create(name, options = {})
|
30
|
+
unless queues[name]
|
31
|
+
@queues[name] = Queue.new(name, options)
|
32
|
+
else
|
33
|
+
raise 'QueueNameExists'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Returns the names of the existing queues.
|
39
|
+
#
|
40
|
+
# @param [Hash] options
|
41
|
+
#
|
42
|
+
# @return [Array]
|
43
|
+
#
|
44
|
+
def inspect(options = {})
|
45
|
+
if prefix = options['QueueNamePrefix']
|
46
|
+
queues.select { |name, queue| name.start_with?(prefix) }.values.map(&:name)
|
47
|
+
else
|
48
|
+
queues.values.map(&:name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Searches for a queue in the list by name.
|
54
|
+
# If it doesn't find it, it creates it.
|
55
|
+
#
|
56
|
+
# @param [String] name
|
57
|
+
#
|
58
|
+
# @return [Queue]
|
59
|
+
#
|
60
|
+
def find(name)
|
61
|
+
if queue = queues[name]
|
62
|
+
return queue
|
63
|
+
else
|
64
|
+
raise 'NonExistentQueue'
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Deletes a queue if it exists.
|
70
|
+
#
|
71
|
+
# @param [name]
|
72
|
+
#
|
73
|
+
def delete(name)
|
74
|
+
if queues[name]
|
75
|
+
queues.delete(name)
|
76
|
+
else
|
77
|
+
raise 'NonExistentQueue'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Queries the list of queues.
|
83
|
+
#
|
84
|
+
def query
|
85
|
+
synchronize do
|
86
|
+
yield
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end # QueueList
|
90
|
+
end # LSQS
|
data/lib/lsqs/server.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module LSQS
|
2
|
+
class Server < Sinatra::Base
|
3
|
+
configure do
|
4
|
+
set :dump_errors, false
|
5
|
+
set :raise_errors, true
|
6
|
+
set :show_exceptions, false
|
7
|
+
|
8
|
+
use LSQS::ErrorHandler
|
9
|
+
end
|
10
|
+
|
11
|
+
helpers do
|
12
|
+
|
13
|
+
##
|
14
|
+
# Gets the action that was requested.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
#
|
18
|
+
def action
|
19
|
+
params['Action']
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Retrieves a XML template and renders it.
|
24
|
+
#
|
25
|
+
# @param [LSQS::Actions::Base] action
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
def render(action)
|
30
|
+
LSQS.template.render(action)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Returns the base URL of the server.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
#
|
38
|
+
def base_url
|
39
|
+
request.base_url
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
post '/' do
|
44
|
+
params['base_url'] = base_url
|
45
|
+
|
46
|
+
result = LSQS.router.distribute(action, params)
|
47
|
+
|
48
|
+
return {:body => render(result)}.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
# To keep the Amazon convention the queue name is stored
|
52
|
+
# in params['QueueName']
|
53
|
+
post "/:QueueName" do
|
54
|
+
result = LSQS.router.distribute(action, params)
|
55
|
+
|
56
|
+
return {:body => render(result)}.to_json
|
57
|
+
end
|
58
|
+
end # Server
|
59
|
+
end # LSQS
|
data/lib/lsqs/version.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module LSQS
|
2
|
+
class XMLTemplate
|
3
|
+
attr_reader :template
|
4
|
+
|
5
|
+
TEMPLATE_DIR = File.expand_path('../../../config', __FILE__)
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
xml = File.read("#{TEMPLATE_DIR}/template.xml.liquid")
|
9
|
+
@template = Liquid::Template.parse(xml)
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Renders the XML that is required as a body response.
|
14
|
+
#
|
15
|
+
# @param [Actions::Base] action
|
16
|
+
#
|
17
|
+
# @return [String]
|
18
|
+
#
|
19
|
+
def render(action)
|
20
|
+
options = {
|
21
|
+
'action' => action.name,
|
22
|
+
'content' => action.to_xml,
|
23
|
+
'request_id' => request_id
|
24
|
+
}
|
25
|
+
|
26
|
+
template.render(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
# Renders the XML that is required for an error response
|
31
|
+
#
|
32
|
+
# @param [String] error
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
36
|
+
def render_error(error)
|
37
|
+
options = {
|
38
|
+
'error' => error,
|
39
|
+
'request_id' => request_id
|
40
|
+
}
|
41
|
+
|
42
|
+
template.render(options)
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Generates a request ID.
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
#
|
50
|
+
def request_id
|
51
|
+
SecureRandom.uuid
|
52
|
+
end
|
53
|
+
end # XMLTemplate
|
54
|
+
end #LSQS
|
data/lib/lsqs.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'puma/cli'
|
3
|
+
require 'liquid'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'digest/md5'
|
6
|
+
require 'active_support/core_ext/string'
|
7
|
+
require 'json'
|
8
|
+
require 'builder'
|
9
|
+
|
10
|
+
require_relative 'lsqs/version'
|
11
|
+
require_relative 'lsqs/error_handler'
|
12
|
+
require_relative 'lsqs/server'
|
13
|
+
require_relative 'lsqs/xml_template'
|
14
|
+
require_relative 'lsqs/action_router'
|
15
|
+
require_relative 'lsqs/queue_list'
|
16
|
+
require_relative 'lsqs/queue'
|
17
|
+
require_relative 'lsqs/message'
|
18
|
+
|
19
|
+
require_relative 'lsqs/actions/base'
|
20
|
+
require_relative 'lsqs/actions/get_queue_url'
|
21
|
+
require_relative 'lsqs/actions/receive_message'
|
22
|
+
require_relative 'lsqs/actions/create_queue'
|
23
|
+
require_relative 'lsqs/actions/send_message'
|
24
|
+
require_relative 'lsqs/actions/delete_message_batch'
|
25
|
+
require_relative 'lsqs/actions/purge_queue'
|
26
|
+
require_relative 'lsqs/actions/list_queues'
|
27
|
+
require_relative 'lsqs/actions/delete_queue'
|
28
|
+
require_relative 'lsqs/actions/delete_message'
|
29
|
+
require_relative 'lsqs/actions/send_message_batch'
|
30
|
+
require_relative 'lsqs/actions/change_message_visibility'
|
31
|
+
|
32
|
+
module LSQS
|
33
|
+
def self.template
|
34
|
+
@template ||= XMLTemplate.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.router
|
38
|
+
@router ||= ActionRouter.new(queue_list)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.queue_list
|
42
|
+
QueueList.new
|
43
|
+
end
|
44
|
+
end # LSQS
|
45
|
+
|
data/lsqs.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lsqs/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'lsqs'
|
8
|
+
gem.version = LSQS::VERSION
|
9
|
+
gem.authors = ['giannismelidis']
|
10
|
+
gem.email = ['gmelidis@engineer.com']
|
11
|
+
gem.summary = 'Gem that allows you to run a version of SQS server locally.'
|
12
|
+
gem.description = gem.summary
|
13
|
+
gem.homepage = 'http://github.com/giannismelidis/lsqs'
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(spec)/})
|
18
|
+
gem.require_paths = ['lib', 'config']
|
19
|
+
|
20
|
+
gem.add_dependency 'liquid'
|
21
|
+
gem.add_dependency 'sinatra'
|
22
|
+
gem.add_dependency 'puma'
|
23
|
+
gem.add_dependency 'activesupport'
|
24
|
+
gem.add_dependency 'builder'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rspec'
|
27
|
+
gem.add_development_dependency 'simplecov'
|
28
|
+
gem.add_development_dependency 'pry'
|
29
|
+
gem.add_development_dependency 'aws-sdk'
|
30
|
+
gem.add_development_dependency 'webmock'
|
31
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LSQS::ActionRouter do
|
4
|
+
before do
|
5
|
+
@router = described_class.new(QueueList.new)
|
6
|
+
end
|
7
|
+
describe '#distribute' do
|
8
|
+
it 'throws an error for undefined action' do
|
9
|
+
expect{@router.distribute('Foo', {})}.to raise_error(LSQS::ActionRouter::ActionError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'does not throw an error for existing action' do
|
13
|
+
expect{@router.distribute('CreateQueue', {})}.not_to raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns an action object' do
|
17
|
+
@result = @router.distribute('CreateQueue', {})
|
18
|
+
@result.kind_of?(LSQS::Actions::CreateQueue).should be_truthy
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|