propono 1.0.0.rc2 → 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/lib/propono/components/queue_subscription.rb +4 -2
- data/lib/propono/components/sqs_message.rb +19 -3
- data/lib/propono/configuration.rb +3 -1
- data/lib/propono/services/queue_listener.rb +34 -18
- data/lib/propono/version.rb +1 -1
- data/test/components/queue_subscription_test.rb +14 -3
- data/test/configuration_test.rb +10 -0
- data/test/integration/sns_to_sqs_test.rb +56 -0
- data/test/services/queue_listener_test.rb +42 -11
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87002e015e85197a83721a25009e5a941535ac1a
|
4
|
+
data.tar.gz: b9e14c3b2b00450d0accb003e6a48907c6e74a5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a8351c78c0b9c97aa88fcbdc617ebcbc6b858c9746e6d57c21e2e1d6d660018eb52de04de61bf3a8ba1deb11c034fd8a954a2252189f8faef42b9903ce718ef
|
7
|
+
data.tar.gz: 7f1f640ddff9e72c7aaf4142bb7dca54c976ff33e184e99d329ede85087651c23e7e2e512166304c9871a1fb6772d89e163da84b948bd84c16a8ff7557521949
|
data/CHANGELOG.md
CHANGED
@@ -4,7 +4,7 @@ module Propono
|
|
4
4
|
include Sns
|
5
5
|
include Sqs
|
6
6
|
|
7
|
-
attr_reader :topic_arn, :queue_name, :queue
|
7
|
+
attr_reader :topic_arn, :queue_name, :queue, :failed_queue, :corrupt_queue
|
8
8
|
|
9
9
|
def self.create(topic_id, options = {})
|
10
10
|
new(topic_id, options).tap do |subscription|
|
@@ -15,13 +15,15 @@ module Propono
|
|
15
15
|
def initialize(topic_id, options = {})
|
16
16
|
@topic_id = topic_id
|
17
17
|
@suffixed_topic_id = "#{topic_id}#{Propono.config.queue_suffix}"
|
18
|
-
@queue_name = "#{Propono.config.application_name.gsub(" ", "_")}-#{@suffixed_topic_id}
|
18
|
+
@queue_name = "#{Propono.config.application_name.gsub(" ", "_")}-#{@suffixed_topic_id}"
|
19
19
|
end
|
20
20
|
|
21
21
|
def create
|
22
22
|
raise ProponoError.new("topic_id is nil") unless @topic_id
|
23
23
|
@topic = TopicCreator.find_or_create(@suffixed_topic_id)
|
24
24
|
@queue = QueueCreator.find_or_create(queue_name)
|
25
|
+
@failed_queue = QueueCreator.find_or_create("#{queue_name}-failed")
|
26
|
+
@corrupt_queue = QueueCreator.find_or_create("#{queue_name}-corrupt")
|
25
27
|
sns.subscribe(@topic.arn, @queue.arn, 'sqs')
|
26
28
|
sqs.set_queue_attributes(@queue.url, "Policy", generate_policy)
|
27
29
|
end
|
@@ -2,17 +2,33 @@ module Propono
|
|
2
2
|
class SqsMessage
|
3
3
|
include Sqs
|
4
4
|
|
5
|
-
attr_reader :context, :message, :raw_message, :receipt_handle
|
5
|
+
attr_reader :context, :message, :raw_message, :receipt_handle, :failure_count
|
6
6
|
def initialize(raw_message)
|
7
|
-
|
8
|
-
|
7
|
+
raw_body = raw_message["Body"]
|
8
|
+
@raw_body_json = JSON.parse(raw_body)
|
9
|
+
body = JSON.parse(@raw_body_json["Message"])
|
9
10
|
|
10
11
|
@raw_message = raw_message
|
11
12
|
@context = body.symbolize_keys
|
13
|
+
@failure_count = context[:num_failures] || 0
|
12
14
|
@message = context.delete(:message)
|
13
15
|
@receipt_handle = raw_message["receipt_handle"]
|
14
16
|
end
|
15
17
|
|
18
|
+
def to_json_with_exception(exception)
|
19
|
+
message = @raw_body_json.dup
|
20
|
+
context = {}
|
21
|
+
context[:id] = @context[:id]
|
22
|
+
context[:message] = @message
|
23
|
+
context[:last_exception_message] = exception.message
|
24
|
+
context[:last_exception_stack_trace] = exception.backtrace
|
25
|
+
context[:last_exception_time] = Time.now
|
26
|
+
context[:num_failures] = failure_count + 1
|
27
|
+
context[:last_context] = @context
|
28
|
+
message['Message'] = context.to_json
|
29
|
+
JSON.pretty_generate(message)
|
30
|
+
end
|
31
|
+
|
16
32
|
def ==(other)
|
17
33
|
other.is_a?(SqsMessage) && other.receipt_handle == @receipt_handle
|
18
34
|
end
|
@@ -10,7 +10,8 @@ module Propono
|
|
10
10
|
:application_name,
|
11
11
|
:udp_host, :udp_port,
|
12
12
|
:tcp_host, :tcp_port,
|
13
|
-
:logger
|
13
|
+
:logger,
|
14
|
+
:max_retries
|
14
15
|
]
|
15
16
|
attr_writer *SETTINGS
|
16
17
|
|
@@ -18,6 +19,7 @@ module Propono
|
|
18
19
|
self.logger = Propono::Logger.new
|
19
20
|
self.queue_suffix = ""
|
20
21
|
self.use_iam_profile = false
|
22
|
+
self.max_retries = 0
|
21
23
|
end
|
22
24
|
|
23
25
|
SETTINGS.each do |setting|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module Propono
|
2
|
+
|
2
3
|
class QueueListener
|
3
4
|
include Sqs
|
4
5
|
|
@@ -36,7 +37,7 @@ module Propono
|
|
36
37
|
raise $!
|
37
38
|
rescue
|
38
39
|
Propono.config.logger.error "Unexpected error reading from queue #{queue_url}"
|
39
|
-
Propono.config.logger.error
|
40
|
+
Propono.config.logger.error $!, $!.backtrace
|
40
41
|
end
|
41
42
|
|
42
43
|
# The calls to delete_message are deliberately duplicated so
|
@@ -44,35 +45,42 @@ module Propono
|
|
44
45
|
# has completed succesfully. We do *not* want to ensure that the
|
45
46
|
# message is deleted regardless of what happens in this method.
|
46
47
|
def process_raw_message(raw_sqs_message)
|
47
|
-
|
48
|
-
|
48
|
+
sqs_message = parse(raw_sqs_message)
|
49
|
+
unless sqs_message.nil?
|
49
50
|
Propono.config.logger.info "Propono [#{sqs_message.context[:id]}]: Received from sqs."
|
50
|
-
|
51
|
-
begin
|
52
|
-
process_message(sqs_message)
|
53
|
-
delete_message(raw_sqs_message)
|
54
|
-
rescue
|
55
|
-
move_to_failed_queue(sqs_message)
|
56
|
-
delete_message(raw_sqs_message)
|
57
|
-
end
|
58
|
-
rescue
|
59
|
-
move_to_corrupt_queue(raw_sqs_message)
|
51
|
+
handle(sqs_message)
|
60
52
|
delete_message(raw_sqs_message)
|
61
53
|
end
|
62
54
|
end
|
63
55
|
|
56
|
+
def parse(raw_sqs_message)
|
57
|
+
SqsMessage.new(raw_sqs_message)
|
58
|
+
rescue
|
59
|
+
Propono.config.logger.error "Error parsing message, moving to corrupt queue", $!, $!.backtrace
|
60
|
+
move_to_corrupt_queue(raw_sqs_message)
|
61
|
+
delete_message(raw_sqs_message)
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def handle(sqs_message)
|
66
|
+
process_message(sqs_message)
|
67
|
+
rescue => e
|
68
|
+
Propono.config.logger.error("Failed to handle message #{e.message} #{e.backtrace}")
|
69
|
+
requeue_message_on_failure(sqs_message, e)
|
70
|
+
end
|
71
|
+
|
64
72
|
def process_message(sqs_message)
|
65
73
|
@message_processor.call(sqs_message.message, sqs_message.context)
|
66
74
|
end
|
67
75
|
|
68
76
|
def move_to_corrupt_queue(raw_sqs_message)
|
69
|
-
|
70
|
-
Propono.publish("#{@topic_id}-corrupt", raw_sqs_message)
|
77
|
+
sqs.send_message(corrupt_queue_url, raw_sqs_message["Body"])
|
71
78
|
end
|
72
79
|
|
73
|
-
def
|
74
|
-
|
75
|
-
Propono.
|
80
|
+
def requeue_message_on_failure(sqs_message, exception)
|
81
|
+
next_queue = (sqs_message.failure_count < Propono.config.max_retries) ? queue_url : failed_queue_url
|
82
|
+
Propono.config.logger.error "Error proessing message, moving to queue: #{next_queue}"
|
83
|
+
sqs.send_message(next_queue, sqs_message.to_json_with_exception(exception))
|
76
84
|
end
|
77
85
|
|
78
86
|
def delete_message(raw_sqs_message)
|
@@ -83,6 +91,14 @@ module Propono
|
|
83
91
|
@queue_url ||= subscription.queue.url
|
84
92
|
end
|
85
93
|
|
94
|
+
def failed_queue_url
|
95
|
+
@failed_queue_url ||= subscription.failed_queue.url
|
96
|
+
end
|
97
|
+
|
98
|
+
def corrupt_queue_url
|
99
|
+
@corrupt_queue_url ||= subscription.corrupt_queue.url
|
100
|
+
end
|
101
|
+
|
86
102
|
def subscription
|
87
103
|
@subscription ||= QueueSubscription.create(@topic_id)
|
88
104
|
end
|
data/lib/propono/version.rb
CHANGED
@@ -26,8 +26,12 @@ module Propono
|
|
26
26
|
|
27
27
|
TopicCreator.stubs(find_or_create: Topic.new("1123"))
|
28
28
|
|
29
|
+
queue_name = subscription.send(:queue_name)
|
30
|
+
|
29
31
|
sqs = mock()
|
30
|
-
sqs.expects(:create_queue).with(
|
32
|
+
sqs.expects(:create_queue).with(queue_name).returns(mock(body: {'QueueUrl' => Fog::AWS::SQS::Mock::QueueUrl}))
|
33
|
+
sqs.expects(:create_queue).with(queue_name + '-failed').returns(mock(body: {'QueueUrl' => Fog::AWS::SQS::Mock::QueueUrl}))
|
34
|
+
sqs.expects(:create_queue).with(queue_name + '-corrupt').returns(mock(body: {'QueueUrl' => Fog::AWS::SQS::Mock::QueueUrl}))
|
31
35
|
QueueCreator.any_instance.stubs(sqs: sqs)
|
32
36
|
|
33
37
|
subscription.create
|
@@ -81,11 +85,18 @@ module Propono
|
|
81
85
|
|
82
86
|
def test_create_saves_queue
|
83
87
|
queue = Queue.new(Fog::AWS::SQS::Mock::QueueUrl)
|
88
|
+
failed_queue = Queue.new(Fog::AWS::SQS::Mock::QueueUrl)
|
89
|
+
corrupt_queue = Queue.new(Fog::AWS::SQS::Mock::QueueUrl)
|
84
90
|
|
85
|
-
QueueCreator.expects(:find_or_create).returns(queue)
|
86
|
-
|
91
|
+
QueueCreator.expects(:find_or_create).with('MyApp-SomeTopic-suf').returns(queue)
|
92
|
+
QueueCreator.expects(:find_or_create).with('MyApp-SomeTopic-suf-failed').returns(failed_queue)
|
93
|
+
QueueCreator.expects(:find_or_create).with('MyApp-SomeTopic-suf-corrupt').returns(corrupt_queue)
|
94
|
+
subscription = QueueSubscription.new("SomeTopic")
|
87
95
|
subscription.create
|
96
|
+
|
88
97
|
assert_equal queue, subscription.queue
|
98
|
+
assert_equal failed_queue, subscription.failed_queue
|
99
|
+
assert_equal corrupt_queue, subscription.corrupt_queue
|
89
100
|
end
|
90
101
|
|
91
102
|
def test_create_raises_with_nil_topic
|
data/test/configuration_test.rb
CHANGED
@@ -105,6 +105,16 @@ module Propono
|
|
105
105
|
Propono.config.application_name
|
106
106
|
end
|
107
107
|
end
|
108
|
+
|
109
|
+
def test_default_max_retries
|
110
|
+
assert_equal 0, Propono.config.max_retries
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_max_retries
|
114
|
+
val = 5
|
115
|
+
Propono.config.max_retries = val
|
116
|
+
assert_equal 5, Propono.config.max_retries
|
117
|
+
end
|
108
118
|
end
|
109
119
|
end
|
110
120
|
|
@@ -38,5 +38,61 @@ module Propono
|
|
38
38
|
ensure
|
39
39
|
thread.terminate
|
40
40
|
end
|
41
|
+
|
42
|
+
=begin
|
43
|
+
|
44
|
+
|
45
|
+
def test_failed_messge_is_transferred_to_failed_channel
|
46
|
+
topic = "test-topic"
|
47
|
+
text = "This is my message #{DateTime.now} #{rand()}"
|
48
|
+
flunks = []
|
49
|
+
message_received = false
|
50
|
+
|
51
|
+
Propono.subscribe_by_queue(topic)
|
52
|
+
|
53
|
+
thread = Thread.new do
|
54
|
+
begin
|
55
|
+
Propono.listen_to_queue(topic) do |message, context|
|
56
|
+
raise StandardError.new 'BOOM'
|
57
|
+
end
|
58
|
+
rescue => e
|
59
|
+
flunks << e.message
|
60
|
+
ensure
|
61
|
+
thread.terminate
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
failure_listener = Thread.new do
|
66
|
+
begin
|
67
|
+
Propono.listen_to_queue(topic, channel: :failed) do |message, context|
|
68
|
+
flunks << "Wrong message" unless message == text
|
69
|
+
flunks << "Wrong id" unless context[:id] =~ Regexp.new("[a-z0-9]{6}")
|
70
|
+
message_received = true
|
71
|
+
end
|
72
|
+
rescue => e
|
73
|
+
flunks << e.message
|
74
|
+
ensure
|
75
|
+
thread.terminate
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
Thread.new do
|
80
|
+
sleep(1) while !message_received
|
81
|
+
sleep(5) # Make sure all the message deletion clear up in the thread has happened
|
82
|
+
thread.terminate
|
83
|
+
failure_listener.terminate
|
84
|
+
end
|
85
|
+
|
86
|
+
sleep(1) # Make sure the listener has started
|
87
|
+
|
88
|
+
Propono.publish(topic, text)
|
89
|
+
flunks << "Test Timeout" unless wait_for_thread(thread)
|
90
|
+
flunk(flunks.join("\n")) unless flunks.empty?
|
91
|
+
ensure
|
92
|
+
thread.terminate
|
93
|
+
end
|
94
|
+
|
95
|
+
=end
|
96
|
+
|
41
97
|
end
|
42
98
|
end
|
@@ -25,6 +25,8 @@ module Propono
|
|
25
25
|
|
26
26
|
@listener = QueueListener.new(@topic_id) {}
|
27
27
|
@listener.stubs(sqs: @sqs)
|
28
|
+
|
29
|
+
Propono.config.max_retries = 0
|
28
30
|
end
|
29
31
|
|
30
32
|
def test_listen_should_loop
|
@@ -129,16 +131,31 @@ module Propono
|
|
129
131
|
@sqs.expects(:delete_message).with(queue_url, @receipt_handle1)
|
130
132
|
@sqs.expects(:delete_message).with(queue_url, @receipt_handle2)
|
131
133
|
|
132
|
-
|
134
|
+
exception = StandardError.new("Test Error")
|
135
|
+
@listener = QueueListener.new(@topic_id) { raise exception }
|
133
136
|
@listener.stubs(queue_url: queue_url)
|
134
137
|
@listener.stubs(sqs: @sqs)
|
138
|
+
@listener.stubs(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message1), exception)
|
139
|
+
@listener.stubs(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message2), exception)
|
140
|
+
@listener.send(:read_messages)
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_messages_are_retried_or_abandoned_on_failure
|
144
|
+
exception = StandardError.new("Test Error")
|
145
|
+
@listener = QueueListener.new(@topic_id) { raise exception }
|
146
|
+
@listener.expects(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message1), exception)
|
147
|
+
@listener.expects(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message2), exception)
|
148
|
+
@listener.stubs(sqs: @sqs)
|
135
149
|
@listener.send(:read_messages)
|
136
150
|
end
|
137
151
|
|
138
|
-
def
|
139
|
-
|
140
|
-
@listener.
|
141
|
-
@listener.
|
152
|
+
def test_failed_on_moving_to_failed_queue_does_not_delete
|
153
|
+
exception = StandardError.new("Test Error")
|
154
|
+
@listener = QueueListener.new(@topic_id) { raise exception }
|
155
|
+
@listener.stubs(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message1), exception).raises(StandardError.new("failed to move"))
|
156
|
+
@listener.stubs(:requeue_message_on_failure).with(SqsMessage.new(@sqs_message2), exception).raises(StandardError.new("failed to move"))
|
157
|
+
@listener.expects(:delete_message).with(@sqs_message1).never
|
158
|
+
@listener.expects(:delete_message).with(@sqs_message2).never
|
142
159
|
@listener.stubs(sqs: @sqs)
|
143
160
|
@listener.send(:read_messages)
|
144
161
|
end
|
@@ -154,15 +171,29 @@ module Propono
|
|
154
171
|
@listener.send(:read_messages)
|
155
172
|
end
|
156
173
|
|
157
|
-
def
|
158
|
-
|
159
|
-
|
160
|
-
|
174
|
+
def test_message_moved_to_failed_queue_if_there_is_an_exception_and_retry_count_is_zero
|
175
|
+
@sqs.expects(:send_message).with(regexp_matches(/https:\/\/queue.amazonaws.com\/[0-9]+\/MyApp-some-topic-failed/), anything)
|
176
|
+
@listener.send(:requeue_message_on_failure, SqsMessage.new(@sqs_message1), StandardError.new)
|
177
|
+
end
|
178
|
+
|
179
|
+
def test_message_requeued_if_there_is_an_exception_but_failure_count_less_than_retry_count
|
180
|
+
Propono.config.max_retries = 5
|
181
|
+
message = SqsMessage.new(@sqs_message1)
|
182
|
+
message.stubs(failure_count: 4)
|
183
|
+
@sqs.expects(:send_message).with(regexp_matches(/https:\/\/queue.amazonaws.com\/[0-9]+\/MyApp-some-topic$/), anything)
|
184
|
+
@listener.send(:requeue_message_on_failure, message, StandardError.new)
|
185
|
+
end
|
186
|
+
|
187
|
+
def test_message_requeued_if_there_is_an_exception_but_failure_count_exceeds_than_retry_count
|
188
|
+
Propono.config.max_retries = 5
|
189
|
+
message = SqsMessage.new(@sqs_message1)
|
190
|
+
message.stubs(failure_count: 5)
|
191
|
+
@sqs.expects(:send_message).with(regexp_matches(/https:\/\/queue.amazonaws.com\/[0-9]+\/MyApp-some-topic-failed/), anything)
|
192
|
+
@listener.send(:requeue_message_on_failure, message, StandardError.new)
|
161
193
|
end
|
162
194
|
|
163
195
|
def test_move_to_corrupt_queue
|
164
|
-
|
165
|
-
Propono.expects(:publish).with("#{@topic_id}-corrupt", @sqs_message1)
|
196
|
+
@sqs.expects(:send_message).with(regexp_matches(/https:\/\/queue.amazonaws.com\/[0-9]+\/MyApp-some-topic-corrupt/), anything)
|
166
197
|
@listener.send(:move_to_corrupt_queue, @sqs_message1)
|
167
198
|
end
|
168
199
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: propono
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- MalcyL
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-12-
|
12
|
+
date: 2013-12-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: fog
|