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