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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3871cae55ac78a6aca5d6af4da0a2cf01b55b615
4
- data.tar.gz: 04d9247f52f05800498896ae54df812e359ea982
3
+ metadata.gz: 87002e015e85197a83721a25009e5a941535ac1a
4
+ data.tar.gz: b9e14c3b2b00450d0accb003e6a48907c6e74a5b
5
5
  SHA512:
6
- metadata.gz: 9a0e0cc2ab9b18be7a1c1b1c737803362d79a9a777e8402656a1153f007ec12394dfff9bf3dc3eac4b61fdd1850f406f2fec0aa76d288ce20ab789ddb09bd5a2
7
- data.tar.gz: e7bff5c5e17d08fb007578c7a1c981c53d5fd3e0c290a723a78de9d005cca0396e9d8abed128e421d06d0f50695bff650e01632b5102bbbae0f9ccf69f1f4d2c
6
+ metadata.gz: 0a8351c78c0b9c97aa88fcbdc617ebcbc6b858c9746e6d57c21e2e1d6d660018eb52de04de61bf3a8ba1deb11c034fd8a954a2252189f8faef42b9903ce718ef
7
+ data.tar.gz: 7f1f640ddff9e72c7aaf4142bb7dca54c976ff33e184e99d329ede85087651c23e7e2e512166304c9871a1fb6772d89e163da84b948bd84c16a8ff7557521949
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ # 1.0.0.rc3 / 2013-12-20
2
+ * [FEATURE] Create failed and corrupt queues when subscribe.
3
+
1
4
  # 1.0.0.rc2 / 2013-12-15
2
5
  * [FEATURE] Make queue_suffix optional
3
6
 
@@ -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}#{options[:queue_name_suffix]}"
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
- body = JSON.parse(raw_message["Body"])["Message"]
8
- body = JSON.parse(body)
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
- begin
48
- sqs_message = SqsMessage.new(raw_sqs_message)
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
- QueueSubscription.create(@topic_id, queue_name_suffix: "-corrupt")
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 move_to_failed_queue(sqs_message)
74
- QueueSubscription.create(@topic_id, queue_name_suffix: "-failed")
75
- Propono.publish("#{@topic_id}-failed", sqs_message.message, id: sqs_message.context[:id])
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
@@ -1,3 +1,3 @@
1
1
  module Propono
2
- VERSION = "1.0.0.rc2"
2
+ VERSION = "1.0.0.rc3"
3
3
  end
@@ -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(subscription.send(:queue_name)).returns(mock(body: {'QueueUrl' => Fog::AWS::SQS::Mock::QueueUrl}))
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
- subscription = QueueSubscription.new("Some Topic")
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
@@ -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
- @listener = QueueListener.new(@topic_id) { raise StandardError.new("Test Error") }
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 test_messages_are_moved_to_failed_queue_if_there_is_an_exception
139
- @listener = QueueListener.new(@topic_id) { raise StandardError.new("Test Error") }
140
- @listener.expects(:move_to_failed_queue).with(SqsMessage.new(@sqs_message1))
141
- @listener.expects(:move_to_failed_queue).with(SqsMessage.new(@sqs_message2))
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 test_move_to_failed_queue
158
- QueueSubscription.expects(:create).with(@topic_id, queue_name_suffix: "-failed")
159
- Propono.expects(:publish).with("#{@topic_id}-failed", @message1, id: @message1_id)
160
- @listener.send(:move_to_failed_queue, SqsMessage.new(@sqs_message1))
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
- QueueSubscription.expects(:create).with(@topic_id, queue_name_suffix: "-corrupt")
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.rc2
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-16 00:00:00.000000000 Z
12
+ date: 2013-12-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: fog