aws_sqs_moniter 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/aws_sqs_moniter.rb +26 -2
- data/lib/aws_sqs_moniter/aws/arns.rb +29 -0
- data/lib/aws_sqs_moniter/aws/builder.rb +48 -0
- data/lib/aws_sqs_moniter/aws/builder/application_policy_builder.rb +73 -0
- data/lib/aws_sqs_moniter/aws/builder/queue_builder.rb +100 -0
- data/lib/aws_sqs_moniter/aws/builder/subscription_builder.rb +48 -0
- data/lib/aws_sqs_moniter/aws/builder/topic_builder.rb +27 -0
- data/lib/aws_sqs_moniter/aws/environmental_name.rb +13 -0
- data/lib/aws_sqs_moniter/configuration.rb +110 -0
- data/lib/aws_sqs_moniter/configuration/queue_configuration.rb +49 -0
- data/lib/aws_sqs_moniter/configuration/redrive_policy_configuration.rb +33 -0
- data/lib/aws_sqs_moniter/configuration/validatable.rb +15 -0
- data/lib/aws_sqs_moniter/dead_letters/retrier.rb +23 -0
- data/lib/aws_sqs_moniter/dead_letters/worker.rb +34 -0
- data/lib/aws_sqs_moniter/logging.rb +66 -0
- data/lib/aws_sqs_moniter/middleware/server/active_record/connection_pool.rb +15 -0
- data/lib/aws_sqs_moniter/middleware/server/active_record/idempotence.rb +24 -0
- data/lib/aws_sqs_moniter/middleware/server/active_record/retrier.rb +23 -0
- data/lib/aws_sqs_moniter/middleware/server/active_record/transaction.rb +36 -0
- data/lib/aws_sqs_moniter/middleware/server/airbrake.rb +24 -0
- data/lib/aws_sqs_moniter/monkey_patches/forbid_implicit_active_record_connection_checkout.rb +34 -0
- data/lib/aws_sqs_moniter/railtie.rb +10 -0
- data/lib/aws_sqs_moniter/typed_message.rb +37 -0
- data/lib/aws_sqs_moniter/version.rb +1 -1
- data/lib/aws_sqs_moniter/worker_registries/typed_message_registry.rb +99 -0
- data/lib/generators/aws_sqs_moniter/install_generator.rb +30 -0
- data/lib/generators/aws_sqs_moniter/templates/create_dead_letters_migration.rb +11 -0
- data/lib/generators/aws_sqs_moniter/templates/create_processed_messages_migration.rb +13 -0
- data/lib/generators/aws_sqs_moniter/templates/create_published_messages_migration.rb +20 -0
- data/lib/generators/aws_sqs_moniter/templates/dead_letter.rb +14 -0
- data/lib/generators/aws_sqs_moniter/templates/initializer.rb +32 -0
- data/lib/generators/aws_sqs_moniter/templates/processed_message.rb +12 -0
- data/lib/generators/aws_sqs_moniter/templates/published_message.rb +14 -0
- data/lib/generators/aws_sqs_moniter/templates/shoryuken.yml +16 -0
- data/lib/tasks/aws_sqs_moniter.rake +83 -0
- data/lib/tasks/aws_sqs_setup.rake +10 -0
- metadata +52 -12
- data/.gitignore +0 -14
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -22
- data/README.md +0 -31
- data/aws_sqs_moniter-0.0.1.gem +0 -0
- data/aws_sqs_moniter.gemspec +0 -36
- data/test/test_helper.rb +0 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
class Configuration
|
3
|
+
class QueueConfiguration
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
def initialize name
|
7
|
+
@name = name
|
8
|
+
@delay_seconds = 0
|
9
|
+
@message_retention_period = 1_209_600
|
10
|
+
@visibility_timeout = 30
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :delay_seconds,
|
14
|
+
:message_retention_period,
|
15
|
+
:visibility_timeout
|
16
|
+
|
17
|
+
attr_reader :name
|
18
|
+
|
19
|
+
def redrive_policy
|
20
|
+
@redrive_policy ||= RedrivePolicyConfiguration.new self
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
unless (0..900).include? delay_seconds
|
25
|
+
errors << "#{name}.delay_seconds must be in the range 0..900"
|
26
|
+
end
|
27
|
+
|
28
|
+
unless (60..1_209_600).include? message_retention_period
|
29
|
+
errors << "#{name}.message_retention_period must be in the range 60..1209600"
|
30
|
+
end
|
31
|
+
|
32
|
+
unless (0..43_200).include? visibility_timeout
|
33
|
+
errors << "#{name}.visibility_timeout must be in the range 0..43200"
|
34
|
+
end
|
35
|
+
|
36
|
+
redrive_policy.valid?
|
37
|
+
errors.push *(redrive_policy.errors)
|
38
|
+
end
|
39
|
+
|
40
|
+
def copy_onto queue
|
41
|
+
queue.delay_seconds = delay_seconds
|
42
|
+
queue.message_retention_period = message_retention_period
|
43
|
+
queue.visibility_timeout = visibility_timeout
|
44
|
+
|
45
|
+
redrive_policy.copy_onto queue.redrive_policy if queue.respond_to? :redrive_policy
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
class Configuration
|
3
|
+
class RedrivePolicyConfiguration
|
4
|
+
include Validatable
|
5
|
+
|
6
|
+
def initialize owner
|
7
|
+
@owner = owner
|
8
|
+
@enabled = true
|
9
|
+
@max_receive_count = 10
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :enabled,
|
13
|
+
:max_receive_count,
|
14
|
+
:dead_letter_queue
|
15
|
+
|
16
|
+
def validate
|
17
|
+
unless (1..1000).include? max_receive_count
|
18
|
+
errors << "#{@owner.name}.redrive_policy.max_receive_count must be in the range 1..1000"
|
19
|
+
end
|
20
|
+
|
21
|
+
if enabled && dead_letter_queue.blank?
|
22
|
+
errors << "#{@owner.name}.redrive_policy.dead_letter_queue is required"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def copy_onto redrive_policy
|
27
|
+
redrive_policy.enabled = enabled
|
28
|
+
redrive_policy.max_receive_count = max_receive_count
|
29
|
+
redrive_policy.dead_letter_queue = dead_letter_queue
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
module DeadLetters
|
3
|
+
class Retrier
|
4
|
+
def initialize logger = nil
|
5
|
+
@publisher = AwsSqsMoniter::MessagePublisher.new
|
6
|
+
@logger = logger || Shoryuken::Logging.logger
|
7
|
+
end
|
8
|
+
|
9
|
+
def retry scope
|
10
|
+
return if scope.count == 0
|
11
|
+
|
12
|
+
count = 0
|
13
|
+
scope.each do |message|
|
14
|
+
count += 1
|
15
|
+
@publisher.publish message.message
|
16
|
+
message.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
@logger.info "Retried #{count} dead letter(s)."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'aws_sqs_moniter/middleware/server/airbrake'
|
2
|
+
require 'aws_sqs_moniter/middleware/server/active_record/connection_pool'
|
3
|
+
require 'aws_sqs_moniter/middleware/server/active_record/transaction'
|
4
|
+
|
5
|
+
module AwsSqsMoniter
|
6
|
+
module DeadLetters
|
7
|
+
class Worker
|
8
|
+
include Shoryuken::Worker
|
9
|
+
|
10
|
+
shoryuken_options(
|
11
|
+
auto_delete: true,
|
12
|
+
body_parser: :json,
|
13
|
+
subscriptions: {
|
14
|
+
financials_dlq: '*' })
|
15
|
+
|
16
|
+
server_middleware do |chain|
|
17
|
+
chain.remove Middleware::Server::Airbrake
|
18
|
+
chain.add Middleware::Server::ActiveRecord::ConnectionPool
|
19
|
+
chain.add Middleware::Server::ActiveRecord::Transaction, isolation: :repeatable_read
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform sqs_message, payload
|
23
|
+
typed_message = TypedMessage.new sqs_message
|
24
|
+
|
25
|
+
return if DeadLetter.exists? message_id: typed_message.id
|
26
|
+
|
27
|
+
DeadLetter.create!(
|
28
|
+
sqs_id: sqs_message.message_id,
|
29
|
+
message_id: typed_message.id,
|
30
|
+
message: payload)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module AwsSqsMoniter
|
4
|
+
module Logging
|
5
|
+
class Formatter < Logger::Formatter
|
6
|
+
def call severity, time, _program_name, message
|
7
|
+
data_hash = message.is_a?(Hash) ? message : { message: message }
|
8
|
+
error_hash = {}
|
9
|
+
|
10
|
+
%i(timestamp pid thread severity).each { |key| data_hash.delete key }
|
11
|
+
|
12
|
+
if (error_object = data_hash.delete(:error))
|
13
|
+
error_hash[:error] = error_object.to_s
|
14
|
+
error_hash[:backtrace] = format_backtrace(error_object.backtrace) if error_object.backtrace
|
15
|
+
end
|
16
|
+
|
17
|
+
timestamp = time.utc.iso8601
|
18
|
+
severity = severity.downcase
|
19
|
+
data = format_hash data_hash
|
20
|
+
error = format_hash error_hash
|
21
|
+
text = %(timestamp="#{timestamp}" pid="#{pid}" thread="#{thread}" severity="#{severity}" #{data} #{error}).strip
|
22
|
+
|
23
|
+
"#{text}\n"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def escape string
|
29
|
+
string.gsub(/"/, '"').gsub("\n", ' ')
|
30
|
+
end
|
31
|
+
|
32
|
+
def format_backtrace backtrace
|
33
|
+
backtrace.map { |line| %("#{line}") }.join(', ')
|
34
|
+
end
|
35
|
+
|
36
|
+
def format_hash hash
|
37
|
+
hash.map do |k, v|
|
38
|
+
v = escape v.to_s
|
39
|
+
%(#{k}="#{v}")
|
40
|
+
end.join ' '
|
41
|
+
end
|
42
|
+
|
43
|
+
def pid
|
44
|
+
Process.pid
|
45
|
+
end
|
46
|
+
|
47
|
+
def thread
|
48
|
+
Thread.current.object_id.to_s 36
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
attr_accessor :default_log_device
|
54
|
+
attr_writer :logger
|
55
|
+
|
56
|
+
def logger
|
57
|
+
@logger ||= begin
|
58
|
+
Logger.new(default_log_device || STDOUT).tap do |l|
|
59
|
+
l.level = Logger::INFO
|
60
|
+
l.formatter = Formatter.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
module ActiveRecord
|
5
|
+
class Idempotence
|
6
|
+
def initialize logger: Shoryuken::Logging.logger
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def call _worker, queue, sqs_msg, _body
|
11
|
+
message = TypedMessage.new sqs_msg
|
12
|
+
|
13
|
+
if ProcessedMessage.exists? message_id: message.id, queue: queue
|
14
|
+
@logger.info middleware: 'idempotence', ignored_message_id: message.id
|
15
|
+
else
|
16
|
+
yield
|
17
|
+
ProcessedMessage.log message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'retryable'
|
2
|
+
|
3
|
+
module AwsSqsMoniter
|
4
|
+
module Middleware
|
5
|
+
module Server
|
6
|
+
module ActiveRecord
|
7
|
+
class Retrier
|
8
|
+
def initialize(options = {})
|
9
|
+
@options = { tries: 10, sleep: 0 }.merge options
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(&block)
|
13
|
+
Retryable.retryable(@options.merge(on: [::ActiveRecord::RecordNotUnique])) do
|
14
|
+
Retryable.retryable(@options.merge(matching: /TRDeadlockDetected|TRSerializationFailure/)) do
|
15
|
+
yield block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'retrier'
|
2
|
+
|
3
|
+
module AwsSqsMoniter
|
4
|
+
module Middleware
|
5
|
+
module Server
|
6
|
+
module ActiveRecord
|
7
|
+
class Transaction
|
8
|
+
RETRIER_OPTIONS = %i(tries sleep on_retriable_error)
|
9
|
+
TRANSACTION_OPTIONS = %i(requires_new joinable isolation)
|
10
|
+
|
11
|
+
def initialize options = {}
|
12
|
+
@transaction_options = options.select { |k, _| TRANSACTION_OPTIONS.include? k }
|
13
|
+
@retrier_default_options = options.select { |k, _| RETRIER_OPTIONS.include? k }
|
14
|
+
end
|
15
|
+
|
16
|
+
def call worker, _queue, _sqs_msg, _body
|
17
|
+
Retrier.new(retrier_options(worker)).call do
|
18
|
+
::ActiveRecord::Base.transaction(@transaction_options) do
|
19
|
+
yield
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def retrier_options worker
|
27
|
+
options = @retrier_default_options.dup
|
28
|
+
on_error = options.delete :on_retriable_error
|
29
|
+
options[:exception_cb] = worker.method(on_error) unless on_error.to_s.empty?
|
30
|
+
options
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
module Middleware
|
3
|
+
module Server
|
4
|
+
class Airbrake
|
5
|
+
def call(_worker, _queue, sqs_msg, body)
|
6
|
+
yield
|
7
|
+
rescue => e
|
8
|
+
parameters = {}
|
9
|
+
|
10
|
+
begin
|
11
|
+
message = TypedMessage.new sqs_msg
|
12
|
+
parameters.store :message, message.headers
|
13
|
+
rescue
|
14
|
+
parameters.store :unknown_message_format, body
|
15
|
+
end
|
16
|
+
|
17
|
+
::Airbrake.notify_or_ignore e, parameters: parameters
|
18
|
+
|
19
|
+
raise e
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
def forbid_implicit_checkout!
|
5
|
+
Thread.current[:active_record_forbid_implicit_connections] = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def implicit_checkout_forbidden?
|
9
|
+
!!Thread.current[:active_record_forbid_implicit_connections]
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection_with_forbid_implicit(*args, &block)
|
13
|
+
if implicit_checkout_forbidden? && !connection_handler.retrieve_connection_pool(self).active_connection?
|
14
|
+
message = 'Implicit ActiveRecord checkout attempted when Thread :force_explicit_connections set!'
|
15
|
+
|
16
|
+
# I want to make SURE I see this error in test output, even though
|
17
|
+
# in some cases my code is swallowing the exception.
|
18
|
+
$stderr.puts(message) if Rails.env.test?
|
19
|
+
|
20
|
+
fail ImplicitConnectionForbiddenError, message
|
21
|
+
end
|
22
|
+
|
23
|
+
connection_without_forbid_implicit(*args, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
alias_method_chain :connection, :forbid_implicit
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# We're refusing to give a connection when asked for. Same outcome
|
31
|
+
# as if the pool timed out on checkout, so let's subclass the exception
|
32
|
+
# used for that.
|
33
|
+
ImplicitConnectionForbiddenError = Class.new(::ActiveRecord::ConnectionTimeoutError)
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module AwsSqsMoniter
|
2
|
+
class TypedMessage
|
3
|
+
def initialize(message)
|
4
|
+
@message = message
|
5
|
+
end
|
6
|
+
|
7
|
+
def id
|
8
|
+
headers['id']
|
9
|
+
end
|
10
|
+
|
11
|
+
def type
|
12
|
+
headers['type']
|
13
|
+
end
|
14
|
+
|
15
|
+
def version
|
16
|
+
headers['version']
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
hash['body']
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
hash
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def hash
|
30
|
+
@hash ||= JSON.parse(@message.body)
|
31
|
+
end
|
32
|
+
|
33
|
+
def headers
|
34
|
+
hash['header']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|