sqs_transport 1.0.0

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/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
+