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