propono 1.0.0.rc2 → 1.0.0.rc3
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.
- 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
|