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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/aws_sqs_moniter.rb +26 -2
  3. data/lib/aws_sqs_moniter/aws/arns.rb +29 -0
  4. data/lib/aws_sqs_moniter/aws/builder.rb +48 -0
  5. data/lib/aws_sqs_moniter/aws/builder/application_policy_builder.rb +73 -0
  6. data/lib/aws_sqs_moniter/aws/builder/queue_builder.rb +100 -0
  7. data/lib/aws_sqs_moniter/aws/builder/subscription_builder.rb +48 -0
  8. data/lib/aws_sqs_moniter/aws/builder/topic_builder.rb +27 -0
  9. data/lib/aws_sqs_moniter/aws/environmental_name.rb +13 -0
  10. data/lib/aws_sqs_moniter/configuration.rb +110 -0
  11. data/lib/aws_sqs_moniter/configuration/queue_configuration.rb +49 -0
  12. data/lib/aws_sqs_moniter/configuration/redrive_policy_configuration.rb +33 -0
  13. data/lib/aws_sqs_moniter/configuration/validatable.rb +15 -0
  14. data/lib/aws_sqs_moniter/dead_letters/retrier.rb +23 -0
  15. data/lib/aws_sqs_moniter/dead_letters/worker.rb +34 -0
  16. data/lib/aws_sqs_moniter/logging.rb +66 -0
  17. data/lib/aws_sqs_moniter/middleware/server/active_record/connection_pool.rb +15 -0
  18. data/lib/aws_sqs_moniter/middleware/server/active_record/idempotence.rb +24 -0
  19. data/lib/aws_sqs_moniter/middleware/server/active_record/retrier.rb +23 -0
  20. data/lib/aws_sqs_moniter/middleware/server/active_record/transaction.rb +36 -0
  21. data/lib/aws_sqs_moniter/middleware/server/airbrake.rb +24 -0
  22. data/lib/aws_sqs_moniter/monkey_patches/forbid_implicit_active_record_connection_checkout.rb +34 -0
  23. data/lib/aws_sqs_moniter/railtie.rb +10 -0
  24. data/lib/aws_sqs_moniter/typed_message.rb +37 -0
  25. data/lib/aws_sqs_moniter/version.rb +1 -1
  26. data/lib/aws_sqs_moniter/worker_registries/typed_message_registry.rb +99 -0
  27. data/lib/generators/aws_sqs_moniter/install_generator.rb +30 -0
  28. data/lib/generators/aws_sqs_moniter/templates/create_dead_letters_migration.rb +11 -0
  29. data/lib/generators/aws_sqs_moniter/templates/create_processed_messages_migration.rb +13 -0
  30. data/lib/generators/aws_sqs_moniter/templates/create_published_messages_migration.rb +20 -0
  31. data/lib/generators/aws_sqs_moniter/templates/dead_letter.rb +14 -0
  32. data/lib/generators/aws_sqs_moniter/templates/initializer.rb +32 -0
  33. data/lib/generators/aws_sqs_moniter/templates/processed_message.rb +12 -0
  34. data/lib/generators/aws_sqs_moniter/templates/published_message.rb +14 -0
  35. data/lib/generators/aws_sqs_moniter/templates/shoryuken.yml +16 -0
  36. data/lib/tasks/aws_sqs_moniter.rake +83 -0
  37. data/lib/tasks/aws_sqs_setup.rake +10 -0
  38. metadata +52 -12
  39. data/.gitignore +0 -14
  40. data/Gemfile +0 -4
  41. data/LICENSE.txt +0 -22
  42. data/README.md +0 -31
  43. data/aws_sqs_moniter-0.0.1.gem +0 -0
  44. data/aws_sqs_moniter.gemspec +0 -36
  45. 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,15 @@
1
+ module AwsSqsMoniter
2
+ class Configuration
3
+ module Validatable
4
+ def errors
5
+ @errors ||= []
6
+ end
7
+
8
+ def valid?
9
+ @errors = []
10
+ validate
11
+ @errors.empty?
12
+ end
13
+ end
14
+ end
15
+ 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,15 @@
1
+ module AwsSqsMoniter
2
+ module Middleware
3
+ module Server
4
+ module ActiveRecord
5
+ class ConnectionPool
6
+ def call(*_args)
7
+ ::ActiveRecord::Base.connection_pool.with_connection do
8
+ yield
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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,10 @@
1
+ require 'rails'
2
+
3
+ module AwsSqsMoniter
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path('../../tasks/aws_sqs_moniter.rake', __FILE__)
7
+ load File.expand_path('../../tasks/aws_sqs_setup.rake', __FILE__)
8
+ end
9
+ end
10
+ 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