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.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module LSQS
2
+ VERSION = '0.0.1'
3
+ end # LSQS
@@ -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