aws_sqs_moniter 0.0.1 → 0.0.3
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/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
|