sidekiq-sqs 0.0.8 → 0.0.9

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/lib/sidekiq-sqs.rb CHANGED
@@ -7,6 +7,7 @@ require 'sidekiq-sqs/fetcher'
7
7
  require 'sidekiq-sqs/client'
8
8
  require 'sidekiq-sqs/processor'
9
9
  require 'sidekiq-sqs/worker'
10
+ require 'sidekiq-sqs/aws-sdk/batch_send_failure_patch'
10
11
 
11
12
  # TODO The retry server middleware directly writes to a retry zset.
12
13
  # TODO Need a queue-prefix option to support multiple rails envs
@@ -25,6 +26,7 @@ module Sidekiq
25
26
  Sidekiq::Client.send :include, Sidekiq::Sqs::Client
26
27
  Sidekiq::Processor.send :include, Sidekiq::Sqs::Processor
27
28
  Sidekiq::Worker::ClassMethods.send :include, Sidekiq::Sqs::Worker
29
+ AWS::SQS::Queue.send :include, Sidekiq::Sqs::AwsSdk::BatchSendFailurePatch
28
30
 
29
31
  # Can't figure how to include/extend and not get a private method...
30
32
  def Sidekiq.sqs
@@ -0,0 +1,34 @@
1
+ module Sidekiq
2
+ module Sqs
3
+ module AwsSdk
4
+ module BatchSendFailurePatch
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ remove_method :batch_failures
9
+ end
10
+
11
+ def batch_failures entries, response
12
+ response[:failed].inject([]) do |failures, failure|
13
+
14
+ entry = entries.find{|e| e[:id] == failure[:id] }
15
+
16
+ details = {
17
+ :error_code => failure[:code],
18
+ :error_message => failure[:message],
19
+ :sender_fault => failure[:sender_fault],
20
+ :message => entry[:message_body] # We need this for retrying
21
+ }
22
+
23
+ if handle = entry[:receipt_handle]
24
+ details[:receipt_handle] = handle
25
+ end
26
+
27
+ failures << details
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
34
+ end
@@ -6,6 +6,25 @@ module Sidekiq
6
6
  module Client
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ RETRYABLE_ERRORS = %w(
10
+ AWS.SimpleQueueService.InternalError
11
+ InternalError
12
+ RequestThrottled
13
+ ServiceUnavailable
14
+ )
15
+
16
+ class BulkInsertionError < StandardError
17
+ attr_reader :failed
18
+
19
+ def initialize(message, failed = [])
20
+ super(message)
21
+ @failed = failed
22
+ end
23
+ end
24
+
25
+ class Retryable < StandardError
26
+ end
27
+
9
28
  included do
10
29
  class << self
11
30
  remove_method :push
@@ -31,22 +50,71 @@ module Sidekiq
31
50
  pushed ? normed['jid'] : nil
32
51
  end
33
52
 
53
+ MAX_BULK_RETRIES = 5
34
54
  def push_bulk(items)
55
+ queue_name, payloads = format_items(items)
56
+ queue = queue_or_create(queue_name)
57
+
58
+ failures, can_retry = bulk_send_to_sqs(queue, payloads)
59
+
60
+ retries = 0
61
+ begin
62
+ if can_retry.size > 0
63
+ failed, can_retry = bulk_send_to_sqs(queue, can_retry)
64
+ failures.concat failed
65
+
66
+ raise Retryable if can_retry.size > 0
67
+ end
68
+ rescue Retryable
69
+ sleep retries ** 2
70
+ retry if (retries += 1) < MAX_BULK_RETRIES
71
+ end
72
+
73
+ if failures.size > 0
74
+ raise BulkInsertionError.new("Some messages failed to insert", failed)
75
+ end
76
+
77
+ failures.empty? ? payloads.size : nil
78
+ end
79
+
80
+ def format_items(items)
35
81
  normed = normalize_item(items)
36
82
  payloads = items['args'].map do |args|
37
83
  _, payload = process_single(items['class'], normed.merge('args' => args, 'jid' => SecureRandom.hex(12)))
38
84
  payload
39
85
  end.compact
40
86
 
41
- pushed = false
42
- queue = queue_or_create(normed['queue'])
87
+ [normed['queue'], payloads]
88
+ end
43
89
 
44
- payloads.each_slice(10) do |items|
45
- ## FIXME error handling
46
- pushed = queue.batch_send( items )
90
+ def bulk_send_to_sqs(queue, formatted_items)
91
+ failures = []
92
+ can_retry = []
93
+ formatted_items.each_slice(10) do |items|
94
+ failed, retryable = send_batch_to_sqs(queue, items)
95
+
96
+ failures.concat failed
97
+ can_retry.concat retryable
47
98
  end
48
99
 
49
- pushed ? payloads.size : nil
100
+ [failures, can_retry]
101
+ end
102
+
103
+ def send_batch_to_sqs(queue, formatted_items)
104
+ failures, retryables = [], []
105
+
106
+ begin
107
+ queue.batch_send(formatted_items)
108
+ rescue AWS::SQS::Errors::BatchSendError => error
109
+ retryable, failed = error.failures.partition do |failure|
110
+ RETRYABLE_ERRORS.include?(failure[:error_code])
111
+ end
112
+
113
+ failures.concat failed
114
+ retryables.concat retryable
115
+ end
116
+
117
+ [failures, retryables]
50
118
  end
51
119
 
52
120
  def queue_or_create(queue)
@@ -1,5 +1,5 @@
1
1
  module Sidekiq
2
2
  module Sqs
3
- VERSION = "0.0.8"
3
+ VERSION = "0.0.9"
4
4
  end
5
5
  end
@@ -10,6 +10,10 @@ class StubClient
10
10
  def self.process_single(*args)
11
11
  end
12
12
 
13
+ def self.normalize_item(*args)
14
+ Hash.new
15
+ end
16
+
13
17
  include Sidekiq::Sqs::Client
14
18
  end
15
19
 
@@ -24,6 +28,104 @@ describe Sidekiq::Sqs::Client do
24
28
  end
25
29
  end
26
30
 
31
+ describe ".bulk_send_to_sqs" do
32
+ let(:retryable) do
33
+ {:error_code => 'ServiceUnavailable', :message_body => "blarg"}
34
+ end
35
+ let(:failed) do
36
+ {:error_code => "GFYS", :message_body => "and your little dog, too"}
37
+ end
38
+ it "dispatches to .send_batch_to_sqs in groups of 10" do
39
+ items = 1.upto(20).to_a
40
+ subject.expects(:send_batch_to_sqs).with(:queue, 1.upto(10).to_a).returns([[], []])
41
+ subject.expects(:send_batch_to_sqs).with(:queue, 11.upto(20).to_a).returns([[], []])
42
+
43
+ subject.bulk_send_to_sqs(:queue, items)
44
+ end
45
+
46
+ it "aggregates failed and retryable messages" do
47
+ items = 1.upto(20).to_a
48
+ subject.expects(:send_batch_to_sqs).with(:queue, 1.upto(10).to_a).returns([[failed], []])
49
+ subject.expects(:send_batch_to_sqs).with(:queue, 11.upto(20).to_a).returns([[], [retryable]])
50
+
51
+ subject.bulk_send_to_sqs(:queue, items).should eq([[failed], [retryable]])
52
+ end
53
+ end
54
+
55
+ describe ".send_batch_to_sqs" do
56
+ let(:queue) { stub }
57
+ let(:retryable) do
58
+ {:error_code => 'ServiceUnavailable', :message_body => "blarg"}
59
+ end
60
+ let(:failed) do
61
+ {:error_code => "GFYS", :message_body => "and your little dog, too"}
62
+ end
63
+
64
+ it "dispatches to the queue" do
65
+ queue.expects(:batch_send).with(:items)
66
+
67
+ subject.send_batch_to_sqs(queue, :items).should eq([[], []])
68
+ end
69
+
70
+ it "aggregates errors correctly" do
71
+ queue.expects(:batch_send).with(:items).raises(
72
+ AWS::SQS::Errors::BatchSendError.new(
73
+ [:sent],
74
+ [failed, retryable]
75
+ )
76
+ )
77
+
78
+ subject.send_batch_to_sqs(queue, :items).should eq([
79
+ [failed], [retryable]
80
+ ])
81
+ end
82
+ end
83
+
84
+ describe ".push_bulk" do
85
+ let(:queue) { stub }
86
+ let(:retries) { [] }
87
+ let(:fails) { [] }
88
+
89
+ before do
90
+ subject.stubs(queue_or_create: queue)
91
+ end
92
+
93
+
94
+ it "needs more tests"
95
+
96
+ context "when some messages fail to insert" do
97
+ before do
98
+ subject.expects(:bulk_send_to_sqs).with(queue, :payloads).returns([fails, retries])
99
+ end
100
+
101
+ context "and all are retryable" do
102
+ before do
103
+ retries.push :error
104
+
105
+ subject.expects(:format_items).with(:items).returns([:queue, :payloads])
106
+ end
107
+
108
+ it "retries" do
109
+ subject.expects(:bulk_send_to_sqs).with(queue, [:error]).returns([[], []])
110
+
111
+ subject.push_bulk(:items)
112
+ end
113
+ end
114
+
115
+ context "and there are non-tryable failures" do
116
+ before do
117
+ fails.push :fail
118
+
119
+ subject.expects(:format_items).with(:items).returns([:queue, :payloads])
120
+ end
121
+
122
+ it "raises an error" do
123
+ expect { subject.push_bulk :items }.to raise_error(Sidekiq::Sqs::Client::BulkInsertionError)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
27
129
  describe ".queue_or_create" do
28
130
  let(:queue) { 'queue' }
29
131
  let(:queues) { stub }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidekiq-sqs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-09 00:00:00.000000000 Z
12
+ date: 2012-10-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -154,6 +154,7 @@ files:
154
154
  - README.md
155
155
  - Rakefile
156
156
  - lib/sidekiq-sqs.rb
157
+ - lib/sidekiq-sqs/aws-sdk/batch_send_failure_patch.rb
157
158
  - lib/sidekiq-sqs/client.rb
158
159
  - lib/sidekiq-sqs/fetcher.rb
159
160
  - lib/sidekiq-sqs/manager.rb