sqs_transport 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Ben Koski
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 ADDED
@@ -0,0 +1,24 @@
1
+ = sqs_transport
2
+
3
+ == OVERVIEW
4
+
5
+ sqs_transport is a wrapper around RightScale's excellent {RightAws::SqsGen2 library}[http://rightscale.rubyforge.org/right_aws_gem_doc/]
6
+ to facilitate simple queue and receive operations.
7
+
8
+ sqs_transport has four major components:
9
+
10
+ * SQS::Config, a class to hold AWS credentials. You'll need to define credential values for queueing to actually work.
11
+ * SQS::Queue, a class to easily create RightAws::SqsGen2 instances, given a queue name
12
+ * SQS::Message, a class allowing arbitrary contents (strings, hashes, arrays) to be queued and dequeued from SQS
13
+ * SQS::Transport, a mixin to quickly add <tt>queue!</tt>, <tt>dequeue!</tt>, and <tt>receive!</tt> methods to push class data through SQS.
14
+
15
+ See class docs for specific examples.
16
+
17
+ == LINKS
18
+
19
+ * Source code: http://github.com/bkoski/sqs_transport
20
+ * Documentation: http://sqs-transport.rubyforge.org
21
+
22
+ == AUTHOR
23
+
24
+ Ben Koski, http://github.com/bkoski, ben.koski (at) gmail.com
data/lib/sqs/config.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'facets'
2
+
3
+
4
+ module SQS
5
+
6
+ # SQS::Config stores your AWS access_key and secret_access_key. These attributes raise exceptions
7
+ # if they are checked but have not been set.
8
+ class Config
9
+
10
+ @@access_key = nil
11
+ @@secret_access_key = nil
12
+
13
+ cattr_writer :access_key, :secret_access_key
14
+
15
+ def self.access_key
16
+ raise ArgumentError, "No access key defined!" if @@access_key.nil?
17
+ return @@access_key
18
+ end
19
+
20
+ def self.secret_access_key
21
+ raise ArgumentError, "No secret access key defined!" if @@secret_access_key.nil?
22
+ return @@secret_access_key
23
+ end
24
+
25
+ # Load configuration from a YAML::load-generated hash
26
+ def self.parse yaml_hash
27
+ self.access_key = yaml_hash['access_key'] unless yaml_hash['access_key'].nil? || yaml_hash['access_key'].empty?
28
+ self.secret_access_key = yaml_hash['secret_access_key'] unless yaml_hash['secret_access_key'].nil? || yaml_hash['secret_access_key'].empty?
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,132 @@
1
+ module SQS
2
+
3
+ # SQS::Message provides a light wrapper over the RightAWS SQS API. It embeds the message payload
4
+ # in a JSON container to allow enqueue time and time_in_queue calculations.
5
+ #
6
+ # Note that by default, JSON serialization is used to encode <tt>contents</tt>. This means that
7
+ # <tt>contents</tt> must be JSON-serializable (i.e. no cyclical objects, singleton strings, etc.)
8
+ # If this is problematic, the serialization mechanism can be customized by overriding the <tt>serialize</tt>
9
+ # and <tt>self.deserialize</tt> methods.
10
+ #
11
+ # To use, simply instantiate, set the <tt>contents</tt> and call queue!
12
+ # For example:
13
+ #
14
+ # m = SQS::Message.new 'my_test_queue'
15
+ # m.contents = {:var1 => 1, :var2 => 2}
16
+ # m.queue!
17
+ #
18
+ # pushes a message containing {:var1 => 1, :var2 => 2} to the 'my_test_queue' queue on SQS
19
+ #
20
+ # To retrieve the message, simply call:
21
+ # m = SQS::Message.receive 'my_test_queue'
22
+ #
23
+ # You can then inspect the contents
24
+ # m.contents # returns {'var1' => 1, 'var2' => 2 }
25
+ # remove from the SQS queue
26
+ # m.dequeue!
27
+ # or check the time in queue
28
+ # m.time_in_queue
29
+ # You can also retrieve the SQS id for the message with the <tt>id</tt> method.
30
+ #
31
+ class Message
32
+
33
+ # The raw RightAws message retrieved from SQS
34
+ attr_accessor :raw_message
35
+
36
+ # Payload to be sent across the wire
37
+ attr_accessor :contents
38
+
39
+ attr_accessor :enqueued_at, :received_at, :queue_name
40
+
41
+ # Retrieves one or more Message objects from an SQS queue
42
+ # * <tt>queue_name</tt> is the name of the SQS queue
43
+ # * <tt>number_to_receive</tt> sets the maximum number of messages to return. If set to 1, a single instance is returned; if > 1 an array is returned. Defaults to 1.
44
+ #
45
+ # Valid options:
46
+ # * <tt>:visiblity</tt> - sets the visiblity timeout on the message in seconds
47
+ def self.receive queue_name, number_to_receive=1, opts={}
48
+ messages = SQS::Queue[queue_name].receive_messages(number_to_receive, opts[:visibility])
49
+ received_at = Time.now.utc
50
+ messages.map! { |m| self.create_from_raw_message(queue_name, m, received_at) }
51
+
52
+ number_to_receive == 1 ? messages.first : messages
53
+ end
54
+
55
+ # Retrives one or more messages from the queue, but sets their visiblity to zero so that
56
+ # they can immediately be picked up by another worker. Useful for previewing messages on the queue.
57
+ # * <tt>queue_name</tt> is the name of the SQS queue
58
+ # * <tt>number_to_view</tt> sets the maximum number of messages to return. If set to 1, a single instance is returned; if > 1 an array is returned. Defaults to 1.
59
+ # No options at this time.
60
+ def self.peek queue_name, number_to_view=1, opts={}
61
+ self.receive(queue_name, number_to_view, :visibility => 0)
62
+ end
63
+
64
+ # Initialize with the name of the name of the SQS queue. When queue! is called on an instance,
65
+ # data will be pushed to this queue.
66
+ def initialize new_queue_name
67
+ @queue_name = new_queue_name
68
+ end
69
+
70
+ # Returns the SQS id for this message; nil if the message has not yet been queued.
71
+ def id
72
+ raw_message ? raw_message.id : nil
73
+ end
74
+
75
+ # Serializes <tt>contents</tt> and pushes data onto the SQS queue.
76
+ def queue!
77
+ @enqueued_at = Time.now.utc
78
+ raise SQS::MessageSizeError, "Message exceeds 8k limit by #{sqs_message_body.size - 8_000} bytes" if sqs_message_body.size > 8_000
79
+ raw_message = SQS::Queue[queue_name].push(sqs_message_body)
80
+ end
81
+
82
+ # Call after retrieving to delete the message from SQS so that it cannot be retrieved by another client.
83
+ def dequeue!
84
+ raw_message.delete
85
+ end
86
+
87
+ # Returns time message spent on queue in seconds.
88
+ # Raises an ArgumentError if called on a message that has not been dequeued.
89
+ def time_in_queue
90
+ raise ArgumentError, "This message has not been dequeued properly; either enqueued_at or received_at is nil" if enqueued_at.nil? || received_at.nil?
91
+ enqueued_at - received_at
92
+ end
93
+
94
+ # Given an object, serialize it to a string. By default, this simply calls to_json
95
+ # on the object, but could be overriden to serialize using YAML or Marshal.dump.
96
+ def serialize object
97
+ object.to_json
98
+ end
99
+
100
+ # Given a string, deserialize an object. By default this calls JSON.parse
101
+ # but could be overriden to use another deserialization method.
102
+ def self.deserialize str
103
+ str.nil? ? nil : JSON.parse(str)
104
+ end
105
+
106
+ private
107
+ # Build the message to send to SQS
108
+ def sqs_message_body
109
+ {:contents => self.serialize(contents), :enqueued_at => enqueued_at}.to_json
110
+ end
111
+
112
+ # Create a SQSMessage instance from its compnent parts
113
+ def self.create_from_raw_message queue_name, raw_message, received_at
114
+ message = self.new(queue_name)
115
+ data = self.parse_sqs_message_body(raw_message.body)
116
+
117
+ message.contents = data['contents']
118
+ message.raw_message = raw_message
119
+ message.enqueued_at = data['enqueued_at']
120
+ message.received_at = received_at
121
+
122
+ return message
123
+ end
124
+
125
+ # Parse a RightAws SQS message body to the :contents, :enqueued_at hash needed to create a new message
126
+ def self.parse_sqs_message_body message_string
127
+ message = message_string.blank? ? {} : JSON.parse(message_string)
128
+ message['contents'] = self.deserialize(message['contents'])
129
+ return message
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,4 @@
1
+ module SQS
2
+ class MessageSizeError < RuntimeError
3
+ end
4
+ end
data/lib/sqs/queue.rb ADDED
@@ -0,0 +1,17 @@
1
+ module SQS
2
+
3
+ # Call SQS::Queue['queue_name'] to retrieve a RightAws::SqsGen2 queue instance
4
+ module Queue
5
+
6
+ def self.[] queue_name
7
+ @sqs ||= RightAws::SqsGen2.new(SQS::Config.access_key, SQS::Config.secret_access_key, :logger => self.logger)
8
+ @sqs.queue(queue_name, true)
9
+ end
10
+
11
+ def self.logger
12
+ logger = Logger.new(STDOUT)
13
+ logger.level = Logger::FATAL
14
+ return logger
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,176 @@
1
+ module SQS
2
+
3
+ # Include the SQS::Transport mixin in a class to add SQS enqueue and dequeue capabilities.
4
+ #
5
+ # After including, call <tt>set_queue_name</tt> to specify which SQS queue will receive messages.
6
+ #
7
+ # There are two ways to define the data that will be included in queued messages:
8
+ # * call <tt>include_in_sqs_message</tt> with an array of attributes to include
9
+ # * implement <tt>to_sqs_message</tt> and return an :attr => value hash
10
+ #
11
+ # <tt>validate_message</tt> can be implemented to validate message data before it is pushed to SQS.
12
+ # If this method raises an exception, the message will not be queued.
13
+ #
14
+ # As an example:
15
+ #
16
+ # class MyMessage
17
+ # include SQS::Transport
18
+ #
19
+ # set_queue_name :widgets
20
+ #
21
+ # attr_accessor :color, :size
22
+ # include_in_sqs_message :color, :size
23
+ #
24
+ # def validate_message
25
+ # raise "Must have both size and color" if color.nil? || size.nil?
26
+ # end
27
+ #
28
+ # end
29
+ #
30
+ # creates a class that would then allow:
31
+ #
32
+ # message = MyMessage.new
33
+ # message.color = 'red'
34
+ # message.size = 'xtrasmall'
35
+ # message.queue!
36
+ #
37
+ # at which point a :size => trasmall, :color => red message would be pushed onto the "widgets" SQS queue
38
+ #
39
+ # A client using the same class could then call:
40
+ #
41
+ # incoming = MyMessage.receive
42
+ # puts incoming.color # "red"
43
+ # puts incoming.size # "xtrasmall"
44
+ # incoming.dequeue! # message is removed from SQS queue
45
+ #
46
+ # Note that by default JSON serializtion is used, so some type semanitics are not preseved.
47
+ # For example, if you set <tt>incoming.color</tt> to <tt>{:shade => :red}</tt> the data would be received as a string (<tt>{'shade' => 'red'}</tt>)
48
+ # The serialization method is customizable by overriding <tt>serialize</tt> on SQS::Message.
49
+ #
50
+ # SQS::Transport stamps the enqueue and dequeue times, which can be used to calculate time in queue
51
+ #
52
+ # puts incoming.enqueued_at # Mon Jul 06 22:16:15 UTC 2009
53
+ # puts incoming.dequeued_at # Mon Jul 06 22:16:25 UTC 2009
54
+ # puts incoming.time_in_queue # 10
55
+ #
56
+ # In addition, the class includes a <tt>peek</tt> method that can be used to inspect queued messages without removing them from the queue.
57
+ #
58
+ module Transport
59
+
60
+ def self.included(base) # :nodoc:
61
+ base.extend ClassMethods
62
+ end
63
+
64
+ module ClassMethods
65
+
66
+ # Defines associated SQS queue
67
+ def set_queue_name name
68
+ @sqs_queue_name = name.to_s
69
+ end
70
+
71
+ # Returns name of associated SQS Queue; raises exception if name has not yet been defined
72
+ def sqs_queue_name
73
+ raise "undefined queue name!" if @sqs_queue_name.nil?
74
+ @sqs_queue_name
75
+ end
76
+
77
+ # Defines the attributes to be included in the message sent to SQS. Not necessary if <tt>to_sqs_message</tt> is overridden.
78
+ def include_in_sqs_message *attrs_to_include
79
+ @attrs_for_sqs_message = attrs_to_include
80
+ end
81
+
82
+ # Returns the list of attrs to be sent to SQS; raises an exception if attrs have not yet been defined.
83
+ def attrs_for_sqs_message
84
+ raise "Message requires at least one attribute. Call include_in_sqs_message to define which attributes are included in SQS message" if @attrs_for_sqs_message.nil? || @attrs_for_sqs_message.empty?
85
+ @attrs_for_sqs_message
86
+ end
87
+
88
+ # Pops one or more messages off the SQS queue, parses the contents, and returns them as instances of the class.
89
+ # * <tt>number_to_recieve</tt> is optional. If not passed, a single message will be returned. If number_to_receive == 1, a single instance will be returned; otherwise an array will be returned.
90
+ #
91
+ # Valid options:
92
+ # * <tt>:visiblity</tt> - sets the visiblity timeout on the message in seconds
93
+ def receive number_to_receive=1, opts={}
94
+ result = SQS::Message.receive(self.sqs_queue_name, number_to_receive, opts)
95
+
96
+ if number_to_receive == 1
97
+ reconstitute_instance(result)
98
+ else
99
+ result.map {|m| reconstitute_instance(m)}
100
+ end
101
+ end
102
+
103
+ def reconstitute_instance sqs_message
104
+ new_instance = self.new
105
+ sqs_message.contents.each { |field, value| new_instance.send("#{field}=", value) }
106
+ new_instance.sqs_message = sqs_message
107
+ return new_instance
108
+ end
109
+
110
+ # Retrives one or more messages from the queue, but sets their visiblity to zero so that
111
+ # they can immediately be picked up by another worker. Useful for previewing messages on the queue.
112
+ # * <tt>queue_name</tt> is the name of the SQS queue
113
+ # * <tt>number_to_view</tt> sets the maximum number of messages to return. If set to 1, a single instance is returned; if > 1 an array is returned. Defaults to 1.
114
+ # No options at this time.
115
+ def peek number_to_view=1, opts={}
116
+ self.receive(number_to_view, opts.merge(:visibility => 0))
117
+ end
118
+
119
+ end
120
+
121
+ attr_writer :sqs_message
122
+
123
+ # Returns a hash that will be serialized and sent to SQS. By default, returns a <tt>{'attr' => 'value'}</tt>
124
+ # hash containing all attributes defined by the <tt>include_in_sqs_message</tt> call.
125
+ #
126
+ # Override this to return a custom {'attr' => 'value'} hash for more control over serialization.
127
+ def to_sqs_message
128
+ Hash[*self.class.attrs_for_sqs_message.collect {|a| [a, self.send(a)]}.flatten]
129
+ end
130
+
131
+ # Implement <tt>validate_message</tt> to check message data before it is sent to SQS.
132
+ # If this method raises an exception, message will not be queued.
133
+ def validate_message
134
+ end
135
+
136
+ # Validates the message, then pushes it onto the associated queue.
137
+ # If validation raises an exception, the message will not be queued and the exception will bubble up.
138
+ def queue!
139
+ validate_message
140
+ sqs_message.contents = to_sqs_message
141
+ sqs_message.queue!
142
+ end
143
+
144
+ # Removes a retrieved instance from the SQS queue.
145
+ def dequeue!
146
+ sqs_message.dequeue!
147
+ end
148
+
149
+ # Returns the associated SQS::Message instance
150
+ def sqs_message
151
+ @sqs_message ||= SQS::Message.new(self.class.sqs_queue_name)
152
+ return @sqs_message
153
+ end
154
+
155
+ # Returns the Amazon id for the SQS message.
156
+ def sqs_message_id
157
+ sqs_message.id
158
+ end
159
+
160
+ # Returns the time the message was queued
161
+ def enqueued_at
162
+ sqs_message.enqueued_at
163
+ end
164
+
165
+ # Returns the time the message was dequeued
166
+ def dequeued_at
167
+ sqs_message.dequeued_at
168
+ end
169
+
170
+ # Returns the amount of time a message spent in the queue, in seconds.
171
+ def time_in_queue
172
+ sqs_message.time_in_queue
173
+ end
174
+
175
+ end
176
+ end
@@ -0,0 +1,9 @@
1
+ require 'json'
2
+ require 'json/add/core'
3
+ require 'right_aws'
4
+
5
+ require File.join(File.dirname(__FILE__), 'sqs/config')
6
+ require File.join(File.dirname(__FILE__), 'sqs/queue')
7
+ require File.join(File.dirname(__FILE__), 'sqs/message_size_error')
8
+ require File.join(File.dirname(__FILE__), 'sqs/message')
9
+ require File.join(File.dirname(__FILE__), 'sqs/transport')
@@ -0,0 +1,13 @@
1
+ class MockQueue
2
+
3
+ attr_accessor :messages
4
+
5
+ def initialize
6
+ @messages = []
7
+ end
8
+
9
+ def push message
10
+ messages << message
11
+ end
12
+
13
+ end
@@ -0,0 +1,15 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'time_warp' # on github as 'iridesco-time-warp'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'sqs_transport'
9
+ require 'mock_queue'
10
+
11
+ SQS::Config.access_key = 'test1234'
12
+ SQS::Config.secret_access_key = 'ASDF KEY'
13
+
14
+ class Test::Unit::TestCase
15
+ end
@@ -0,0 +1,57 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class TestSQSConfig < Test::Unit::TestCase
4
+
5
+ context "defaults" do
6
+ setup do
7
+ SQS::Config.access_key = nil
8
+ SQS::Config.secret_access_key = nil
9
+ end
10
+
11
+
12
+ should "raise an ArgumentError if access_key is undefined" do
13
+ assert_raises(ArgumentError) { SQS::Config.access_key }
14
+ end
15
+
16
+ should "raise an ArgumentError if secret_access_key is undefined" do
17
+ assert_raises(ArgumentError) { SQS::Config.secret_access_key }
18
+ end
19
+ end
20
+
21
+ context "setters" do
22
+ should "return value for access_key if defined" do
23
+ SQS::Config.access_key = 'TeSt'
24
+ assert_equal 'TeSt', SQS::Config.access_key
25
+ end
26
+
27
+ should "return value for secret_access_key if defined" do
28
+ SQS::Config.secret_access_key = 'TeStSecr3t'
29
+ assert_equal 'TeStSecr3t', SQS::Config.secret_access_key
30
+ end
31
+ end
32
+
33
+ context "parse" do
34
+ should "set access_key if provided" do
35
+ SQS::Config.parse({'access_key' => '1234'})
36
+ assert_equal '1234', SQS::Config.access_key
37
+ end
38
+
39
+ should "not set access_key if provided as blank string" do
40
+ SQS::Config.parse({'access_key' => '1234'})
41
+ SQS::Config.parse({'access_key' => ''})
42
+ assert_equal '1234', SQS::Config.access_key
43
+ end
44
+
45
+ should "set secret_access_key if provided" do
46
+ SQS::Config.parse({'secret_access_key' => '1234ABC'})
47
+ assert_equal '1234ABC', SQS::Config.secret_access_key
48
+ end
49
+
50
+ should "not set secret_access_key if provided as blank string" do
51
+ SQS::Config.parse({'secret_access_key' => '1234ABC'})
52
+ SQS::Config.parse({'secret_access_key' => ''})
53
+ assert_equal '1234ABC', SQS::Config.secret_access_key
54
+ end
55
+ end
56
+
57
+ end
@@ -0,0 +1,221 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class SQS::Message
4
+ public :sqs_message_body
5
+ end
6
+
7
+ class TestSQSMessage < Test::Unit::TestCase
8
+
9
+ context "self.receive" do
10
+ setup do
11
+ @mock_queue = MockQueue.new
12
+ SQS::Queue.stubs(:[]).with('test_queue').returns(@mock_queue)
13
+ end
14
+
15
+ should "call sqs_queue.recieve_messages for the specified number of messages" do
16
+ @mock_queue.expects(:receive_messages).with(7, anything).returns([])
17
+ SQS::Message.receive('test_queue', 7)
18
+ end
19
+
20
+ should "call sqs_queue.receive for a single message if no message count is specified" do
21
+ @mock_queue.expects(:receive_messages).with(1, anything).returns([])
22
+ SQS::Message.receive('test_queue')
23
+ end
24
+
25
+ should "call sqs_queue.receive with visibility specified as :visibility in options" do
26
+ @mock_queue.expects(:receive_messages).with(anything, 23).returns([])
27
+ SQS::Message.receive('test_queue', 1, :visibility => 23)
28
+ end
29
+
30
+ should "call sqs_queue.receive with nil visiblity if :visiblity not provided in options" do
31
+ @mock_queue.expects(:receive_messages).with(anything, nil).returns([])
32
+ SQS::Message.receive('test_queue', 1)
33
+ end
34
+
35
+ should "return a single instance of class if a single message is requested" do
36
+ @mock_queue.stubs(:receive_messages).returns([mock_message])
37
+ assert_kind_of SQS::Message, SQS::Message.receive('test_queue', 1)
38
+ end
39
+
40
+ should "return an array of objects if multiple messages are requested" do
41
+ message_array = [mock_message, mock_message, mock_message]
42
+ @mock_queue.stubs(:receive_messages).returns(message_array)
43
+ assert_equal message_array, SQS::Message.receive('test_queue', 7)
44
+ end
45
+
46
+ should "return an array of objects if multiple messages are requested, even if there is only a single message in the queue" do
47
+ message_array = [mock_message]
48
+ @mock_queue.stubs(:receive_messages).returns(message_array)
49
+ assert_equal message_array, SQS::Message.receive('test_queue', 7)
50
+ end
51
+
52
+ should "set received_at on returned messages to the time dequeue! was invoked" do
53
+ message_array = [mock_message]
54
+ @mock_queue.stubs(:receive_messages).returns(message_array)
55
+
56
+ expected_received_at = Time.now.utc - 5_000
57
+ received_message = nil
58
+
59
+ pretend_now_is(expected_received_at) do
60
+ received_message = SQS::Message.receive('test_queue')
61
+ end
62
+
63
+ assert_in_delta expected_received_at, received_message.received_at, 0.5
64
+ end
65
+
66
+ should "stamp a consistent received_at across all messages returned" do
67
+ message_array = [mock_message, mock_message, mock_message]
68
+ @mock_queue.stubs(:receive_messages).returns(message_array)
69
+
70
+ messages = SQS::Message.receive('test_queue', 3)
71
+
72
+ assert_equal 1, messages.collect { |m| m.received_at }.uniq.length
73
+ end
74
+
75
+ should "assign the enqueued_at time on returned message" do
76
+ expected_message = SQS::Message.new 'test_queue'
77
+ expected_message.contents = {:value_1 => 1}
78
+
79
+ @mock_queue.stubs(:receive_messages).returns([mock_message(expected_message.sqs_message_body)])
80
+
81
+ message = SQS::Message.receive('test_queue')
82
+ assert_equal expected_message.enqueued_at, message.enqueued_at
83
+ end
84
+
85
+ should "assign the contents on returned message" do
86
+ expected_message = SQS::Message.new 'test_queue'
87
+ expected_message.contents = {'value_1' => 1}
88
+
89
+ @mock_queue.stubs(:receive_messages).returns([mock_message(expected_message.sqs_message_body)])
90
+
91
+ message = SQS::Message.receive('test_queue')
92
+ assert_equal expected_message.contents, message.contents
93
+ end
94
+
95
+ should "assign the raw message returned from SQS to raw_message" do
96
+ expected_message = mock()
97
+ expected_message.stubs(:body)
98
+ @mock_queue.stubs(:receive_messages).returns([expected_message])
99
+ message = SQS::Message.receive('test_queue')
100
+ assert_equal expected_message.object_id, message.raw_message.object_id
101
+ end
102
+ end
103
+
104
+ context "self.peek" do
105
+ should "receive number_to_view with :visibility => 0" do
106
+ SQS::Message.expects(:receive).with('test_queue', 10, :visibility => 0)
107
+ SQS::Message.peek('test_queue', 10)
108
+ end
109
+
110
+ should "receive 1 message with :visiblity => 0 if number_to_view not specified" do
111
+ SQS::Message.expects(:receive).with('test_queue', 1, :visibility => 0)
112
+ SQS::Message.peek('test_queue')
113
+ end
114
+ end
115
+
116
+ context "queue!" do
117
+ setup do
118
+ @expected_contents = {:value_1 => 1, :value_2 => 2}
119
+ @mock_queue = MockQueue.new
120
+ SQS::Queue.stubs(:[]).with('test_queue').returns(@mock_queue)
121
+ @test_instance = SQS::Message.new 'test_queue'
122
+ end
123
+
124
+ should "push a JSON message containing :content => serialized_content onto queue specified in init" do
125
+ @test_instance.contents = @expected_contents
126
+ @test_instance.queue!
127
+
128
+ assert_equal @test_instance.serialize(@expected_contents), JSON.parse(@mock_queue.messages.first)['contents']
129
+ end
130
+
131
+ should "push a JSON message containing :enqueue_time => Time.now.utc onto queue specified in init" do
132
+ enqueue_time = Time.now
133
+
134
+ @test_instance.contents = {:value_1 => 1}
135
+ pretend_now_is(enqueue_time) do
136
+ @test_instance.queue!
137
+ end
138
+
139
+ assert_equal enqueue_time.to_i, JSON.parse(@mock_queue.messages.first)['enqueued_at'].to_i
140
+ end
141
+
142
+ should "raise a SQS::MessageSizeError if message contents are too big" do
143
+ @test_instance.stubs(:sqs_message_body).returns('a' * 10_000)
144
+ assert_raises(SQS::MessageSizeError) { @test_instance.queue! }
145
+ end
146
+ end
147
+
148
+ context "dequeue!" do
149
+ should "call delete on the raw message" do
150
+ raw_message = mock()
151
+ raw_message.expects(:delete)
152
+
153
+ instance = SQS::Message.new 'test_queue'
154
+ instance.stubs(:raw_message).returns(raw_message)
155
+ instance.dequeue!
156
+ end
157
+ end
158
+
159
+ context "id" do
160
+ should "return the id from raw_message if it is set" do
161
+ expected_id = 'ASDF-1234'
162
+ raw_message = mock()
163
+ raw_message.stubs(:id).returns(expected_id)
164
+
165
+ instance = SQS::Message.new 'test_queue'
166
+ instance.stubs(:raw_message).returns(raw_message)
167
+
168
+ assert_equal expected_id, instance.id
169
+ end
170
+
171
+ should "return nil if raw_message is not set" do
172
+ instance = SQS::Message.new 'test_queue'
173
+ instance.stubs(:raw_message).returns(nil)
174
+ assert_nil instance.id
175
+ end
176
+ end
177
+
178
+ context "queue_name" do
179
+ should "return the queue name specified at init time" do
180
+ expected_name = 'test_queue'
181
+ instance = SQS::Message.new expected_name
182
+ assert_equal expected_name, instance.queue_name
183
+ end
184
+ end
185
+
186
+ context "time_in_queue" do
187
+ should "raise an ArgumentError if enqueued_at is nil" do
188
+ instance = SQS::Message.new 'test_queue'
189
+ instance.stubs(:enqueued_at).returns(nil)
190
+ instance.stubs(:received_at).returns(DateTime.now)
191
+ assert_raises(ArgumentError) { instance.time_in_queue }
192
+ end
193
+
194
+ should "raise an ArgumentError if received_at is nil" do
195
+ instance = SQS::Message.new 'test_queue'
196
+ instance.stubs(:enqueued_at).returns(nil)
197
+ instance.stubs(:received_at).returns(DateTime.now)
198
+ assert_raises(ArgumentError) { instance.time_in_queue }
199
+ end
200
+
201
+ should "return received_at - enqueued_at" do
202
+ instance = SQS::Message.new 'test_queue'
203
+
204
+ expected_received_at = DateTime.now
205
+ expected_enqueued_at = expected_received_at - 200
206
+
207
+ instance.stubs(:enqueued_at).returns(expected_enqueued_at)
208
+ instance.stubs(:received_at).returns(expected_received_at)
209
+ assert_equal expected_enqueued_at - expected_received_at, instance.time_in_queue
210
+ end
211
+ end
212
+
213
+ private
214
+ def mock_message mock_body=nil
215
+ message = mock()
216
+ message.stubs(:body).returns(mock_body)
217
+
218
+ return message
219
+ end
220
+
221
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # Queue caches its RightAws instance; this needs to be busted between tests
4
+ module SQS
5
+ module Queue
6
+ def self.flush_cache
7
+ @sqs = nil
8
+ end
9
+ end
10
+ end
11
+
12
+ class TestSQSQueue < Test::Unit::TestCase
13
+
14
+ def setup
15
+ @mock_right_aws = mock()
16
+ @mock_right_aws.stubs(:queue)
17
+ end
18
+
19
+ def teardown
20
+ SQS::Queue.flush_cache
21
+ end
22
+
23
+ should "instantiate a new RightAws::SQSGen2 using the access key from the SQS::Config" do
24
+ SQS::Config.access_key = 'ASDF'
25
+ RightAws::SqsGen2.expects(:new).with(SQS::Config.access_key, anything, anything).returns(@mock_right_aws)
26
+ SQS::Queue['test_queue']
27
+ end
28
+
29
+ should "instantiate a new RightAws::SQSGen2 using the secret access key from the SQS::Config" do
30
+ SQS::Config.secret_access_key = 'ASDF123'
31
+ RightAws::SqsGen2.expects(:new).with(anything, SQS::Config.secret_access_key, anything).returns(@mock_right_aws)
32
+ SQS::Queue['test_queue']
33
+ end
34
+
35
+ should "make a find/create call for the specified queue" do
36
+ SQS::Config.secret_access_key = 'ASDF123'
37
+ RightAws::SqsGen2.stubs(:new).returns(@mock_right_aws)
38
+ @mock_right_aws.expects(:queue).with('test_queue', true)
39
+ SQS::Queue['test_queue']
40
+ end
41
+
42
+ should "return a queue object" do
43
+ SQS::Config.secret_access_key = 'ASDF123'
44
+ RightAws::SqsGen2.stubs(:new).returns(@mock_right_aws)
45
+
46
+ mock_queue = mock()
47
+ @mock_right_aws.stubs(:queue).returns(mock_queue)
48
+
49
+ assert_equal mock_queue.object_id, SQS::Queue['test_queue'].object_id
50
+ end
51
+
52
+ end
@@ -0,0 +1,186 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class TestTransport
4
+ include SQS::Transport
5
+
6
+ attr_reader :source_message # allow source to be read for testing
7
+ attr_accessor :value1, :value2
8
+ include_in_sqs_message :value1, :value2
9
+
10
+ set_queue_name 'testQ'
11
+ end
12
+
13
+ class TestTransportNoQueue
14
+ include SQS::Transport
15
+
16
+ attr_accessor :value1
17
+ end
18
+
19
+ class TestTransportSymbolQueue
20
+ include SQS::Transport
21
+ set_queue_name :testQueue2
22
+
23
+ attr_accessor :value1
24
+ include_in_sqs_message :value1
25
+ end
26
+
27
+
28
+ class TestSQSTransport < Test::Unit::TestCase
29
+
30
+ context "self.sqs_queue_name" do
31
+ should "raise an exception if set_queue_name has not been called" do
32
+ assert_raises(RuntimeError) { TestTransportNoQueue.sqs_queue_name }
33
+ end
34
+
35
+ should "return value set using set_queue_name" do
36
+ assert_equal 'testQ', TestTransport.sqs_queue_name
37
+ end
38
+
39
+ should "return string, even if set_queue_name awas called with symbol" do
40
+ assert_equal 'testQueue2', TestTransportSymbolQueue.sqs_queue_name
41
+ assert_kind_of String, TestTransportSymbolQueue.sqs_queue_name
42
+ end
43
+ end
44
+
45
+ context "self.attrs_for_sqs_message" do
46
+ should "raise an exception if include_in_sqs_message has not been called" do
47
+ assert_raises(RuntimeError) { TestTransportNoQueue.attrs_for_sqs_message }
48
+ end
49
+
50
+ should "return value set using include_in_sqs_message" do
51
+ assert_equal [:value1, :value2], TestTransport.attrs_for_sqs_message
52
+ end
53
+ end
54
+
55
+ context "self.receive" do
56
+ should "proxy request to SQS::Message.receive" do
57
+ SQS::Message.expects(:receive).with(TestTransport.sqs_queue_name, 5, :visibility => 10).returns([])
58
+ TestTransport.receive(5, :visibility => 10)
59
+ end
60
+
61
+ should "return reconstituted instance of original if number_to_receive = 1" do
62
+ original = TestTransport.new
63
+ original.value1 = 'test'
64
+ original.value2 = 10
65
+
66
+ SQS::Message.any_instance.stubs(:queue!)
67
+ original.queue!
68
+ SQS::Message.stubs(:receive).returns(original.sqs_message)
69
+
70
+ new_instance = TestTransport.receive
71
+ assert_equal original.value1, new_instance.value1
72
+ assert_equal original.value2, new_instance.value2
73
+ end
74
+
75
+ should "set the new instance's sqs_message = received message" do
76
+ original = TestTransport.new
77
+ original.value1 = 'test'
78
+ original.value2 = 10
79
+
80
+ SQS::Message.any_instance.stubs(:queue!)
81
+ original.queue!
82
+ SQS::Message.stubs(:receive).returns(original.sqs_message)
83
+
84
+ new_instance = TestTransport.receive
85
+ assert_equal original.sqs_message, new_instance.sqs_message
86
+ end
87
+
88
+ should "return array of reconstituted originals if number_to_receive > 1" do
89
+ originals = []
90
+
91
+ original1 = TestTransport.new
92
+ original1.value1 = 'test'
93
+ original1.value2 = 10
94
+ originals << original1
95
+
96
+ original2 = TestTransport.new
97
+ original2.value1 = 'test2'
98
+ original2.value2 = 20
99
+ originals << original2
100
+
101
+ SQS::Message.any_instance.stubs(:queue!)
102
+ originals.each {|o| o.queue!}
103
+ SQS::Message.stubs(:receive).returns(originals.collect {|o| o.sqs_message})
104
+
105
+ instances = TestTransport.receive(3)
106
+ instances.each_with_index do |actual, i|
107
+ assert_equal originals[i].value1, actual.value1
108
+ assert_equal originals[i].value2, actual.value2
109
+ end
110
+ end
111
+ end
112
+
113
+ context "self.peek" do
114
+ should "call self.receive with :visibility => 0" do
115
+ TestTransport.expects(:receive).with(5, :visibility => 0)
116
+ TestTransport.peek(5)
117
+ end
118
+
119
+ should "receive a single instance by default" do
120
+ TestTransport.expects(:receive).with(1, :visibility => 0)
121
+ TestTransport.peek
122
+ end
123
+ end
124
+
125
+ context "queue!" do
126
+ setup do
127
+ @test_instance = TestTransport.new
128
+ @test_instance.value1 = 'test1'
129
+ @test_instance.value2 = 'test2'
130
+ SQS::Message.any_instance.stubs(:queue!)
131
+ end
132
+
133
+ should "call validate_message" do
134
+ @test_instance.expects(:validate_message)
135
+ @test_instance.queue!
136
+ end
137
+
138
+ should "not call sqs_message.queue! if validate message throws exception" do
139
+ @test_instance.stubs(:validate_message).raises(RuntimeError)
140
+ SQS::Message.any_instance.expects(:queue!).never
141
+ begin
142
+ @test_instance.queue!
143
+ rescue
144
+ end
145
+ end
146
+
147
+ should "set sqs_message.contents = to_sqs_message" do
148
+ @test_instance.queue!
149
+ assert_equal @test_instance.to_sqs_message, @test_instance.sqs_message.contents
150
+ end
151
+
152
+ should "invoke queue! on the sqs_message" do
153
+ @test_instance.sqs_message.expects(:queue!)
154
+ @test_instance.queue!
155
+ end
156
+ end
157
+
158
+ context "to_sqs_message - default implementation" do
159
+ should "return attrs_for_sqs_message in hash, with attrs as keys and values as values" do
160
+ instance = TestTransport.new
161
+ instance.value1 = '1234'
162
+ instance.value2 = 'abcd'
163
+
164
+ assert_equal({:value1 => '1234', :value2 => 'abcd'}, instance.to_sqs_message)
165
+ end
166
+ end
167
+
168
+ context "proxy to SQS::Message instance" do
169
+ setup do
170
+ @test_instance = TestTransport.new
171
+ end
172
+
173
+ should "proxy sqs_message_id to sqs_message.id" do
174
+ @test_instance.sqs_message.expects(:id)
175
+ @test_instance.sqs_message_id
176
+ end
177
+
178
+ ['dequeue!','enqueued_at','dequeued_at','time_in_queue'].each do |proxy_method|
179
+ should "proxy #{proxy_method} to sqs_message.#{proxy_method}" do
180
+ @test_instance.sqs_message.expects(proxy_method)
181
+ @test_instance.send(proxy_method)
182
+ end
183
+ end
184
+ end
185
+
186
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sqs_transport
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ben Koski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-10 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: right_aws
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: Provides simple enqueue/dequeue facilities for Amazon SQS.
36
+ email: gems@benkoski.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - lib/sqs
45
+ - lib/sqs_transport.rb
46
+ - lib/sqs/config.rb
47
+ - lib/sqs/message.rb
48
+ - lib/sqs/message_size_error.rb
49
+ - lib/sqs/queue.rb
50
+ - lib/sqs/transport.rb
51
+ - test/mock_queue.rb
52
+ - test/test_helper.rb
53
+ - test/test_sqs_config.rb
54
+ - test/test_sqs_message.rb
55
+ - test/test_sqs_queue.rb
56
+ - test/test_sqs_transport.rb
57
+ - README
58
+ - LICENSE
59
+ has_rdoc: true
60
+ homepage: http://github.com/bkoski/sqs_transport
61
+ post_install_message:
62
+ rdoc_options:
63
+ - --inline-source
64
+ - --charset=UTF-8
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.1
83
+ signing_key:
84
+ specification_version: 2
85
+ summary: Provides simple enqueue/dequeue facilities for Amazon SQS.
86
+ test_files: []
87
+