lsqs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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